Fase 6d: migrar Enemy al sistema de fisica vectorial

Segunda entidad migrada. Los enemigos (Pentagon, Quadrat, Molinillo)
ahora viven en el PhysicsWorld con velocidad vectorial. Las colisiones
entre enemigos quedan habilitadas automaticamente (novedad: antes no
se chocaban).

Cambios en enemy.hpp:
- Eliminado: float velocity_ (escalar)
- Eliminado: void mou() (lo hace el world)
- Anadido: override postUpdate()
- Anadido: helper privado setVelocityFromAngle(angle, speed)
- Anadido: direction_change_timer_ para zigzag periodico del Pentagon

Cambios en enemy.cpp:
- Constructor configura body_ (mass=5 default, radius=0 inactivo,
  restitution=1.0 elastico, sin damping)
- init() ajusta masa por tipo:
  * Pentagon: 5.0 (esquivador ligero)
  * Quadrat: 8.0 (tanque pesado)
  * Molinillo: 4.0 (agil rapido)
- init() setea body_.radius = ENEMY_RADIUS al spawn
- behaviorPentagon: zigzag por probabilidad temporal (0.8/s) en lugar
  de detectar paredes; el rebote contra muros lo hace PhysicsWorld
- behaviorQuadrat: tracking discreto cada TRACKING_INTERVAL — mezcla
  velocity actual con direccion al ship (LERP por tracking_strength)
- behaviorMolinillo: solo boost de rotacion visual cerca del ship;
  movimiento puramente lineal integrado por el world
- destruir() pone velocity=0, angular=0, radius=0
- postUpdate() sincroniza center_ desde body_.position
- setVelocity(speed) mantiene la direccion, cambia solo la magnitud

Renames a camelBack (.clang-tidy del proyecto):
- get_drotacio -> getRotationDelta
- get_base_velocity -> getBaseVelocity, get_base_rotation -> getBaseRotation
- set_ship_position -> setShipPosition
- set_velocity -> setVelocity, set_rotation -> setRotation
- set_tracking_strength -> setTrackingStrength
- get_temps_invulnerabilitat -> getInvulnerabilityTime
- actualitzar_animacio -> updateAnimation
- actualitzar_palpitacio -> updatePalpitation
- actualitzar_rotacio_accelerada -> updateRotationAcceleration
- comportament_pentagon/quadrat/molinillo -> behaviorPentagon/Quadrat/Molinillo
- calcular_escala_actual -> computeCurrentScale
- intent_spawn_safe -> attemptSafeSpawn
(callsites actualizados en spawn_controller y game_scene)

Cambios en GameScene:
- En init(): physics_world_.addBody(&enemy.getBody()) por cada slot
  (los inactivos tienen radius=0, no estorban)
- En update(): postUpdate() de cada enemy tras physics_world_.update

Cambios de comportamiento visibles esperados:
- Enemigos rebotan elasticamente contra paredes (restitution=1.0)
- Enemigos se chocan entre si (impulsos elasticos con masas distintas
  por tipo: Quadrat empuja mas, Molinillo rebota mas)
- Pentagon zigzag periodico en lugar de solo al chocar pared
- Molinillo: comportamiento mas predecible (linea recta)

Aviso: Bullet sigue con su movimiento ad-hoc (Fase 6e pendiente).

Smoke test xvfb OK. Validacion gameplay del usuario pendiente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 13:41:05 +02:00
parent 2fe22ff911
commit 27242f54fe
5 changed files with 325 additions and 423 deletions
+248 -349
View File
@@ -1,4 +1,4 @@
// enemy.cpp - Implementació de enemigos (ORNIs) // enemy.cpp - Implementación de enemigos (ORNIs)
// © 1999 Visente i Sergi (versión Pascal) // © 1999 Visente i Sergi (versión Pascal)
// © 2025 Port a C++20 con SDL3 // © 2025 Port a C++20 con SDL3
@@ -16,488 +16,399 @@
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
namespace {
// Velocidad inicial vectorial a partir de un ángulo (rad).
// angle=0 apunta hacia arriba (eje Y negativo SDL), como el resto del juego.
auto angleToDirection(float angle) -> Vec2 {
return Vec2{
.x = std::cos(angle - (Constants::PI / 2.0F)),
.y = std::sin(angle - (Constants::PI / 2.0F)),
};
}
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag).
// Si está parado, devuelve 0.
auto velocityToAngle(const Vec2& velocity) -> float {
if (velocity.lengthSquared() < 0.0001F) {
return 0.0F;
}
// El movimiento (vx, vy) corresponde a angle - PI/2; invertimos.
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
}
} // namespace
Enemy::Enemy(SDL_Renderer* renderer) Enemy::Enemy(SDL_Renderer* renderer)
: Entity(renderer), : Entity(renderer),
velocity_(0.0F),
drotacio_(0.0F), drotacio_(0.0F),
rotacio_(0.0F), rotacio_(0.0F),
esta_(false), esta_(false),
type_(EnemyType::PENTAGON), type_(EnemyType::PENTAGON),
tracking_timer_(0.0F), tracking_timer_(0.0F),
ship_position_(nullptr), ship_position_(nullptr),
tracking_strength_(0.5F), // Default tracking strength tracking_strength_(0.5F),
timer_invulnerabilitat_(0.0F) { // Start vulnerable direction_change_timer_(0.0F),
// [NUEVO] Brightness específic per enemigos timer_invulnerabilitat_(0.0F) {
brightness_ = Defaults::Brightness::ENEMIC; brightness_ = Defaults::Brightness::ENEMIC;
// [NUEVO] Forma es carrega a init() segons el type // Configuración del cuerpo físico — defaults para enemy genérico.
// Constructor no carrega shape per permetre type diferents // init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo).
body_.setMass(5.0F); // Más liviano que la nave (10.0)
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
body_.restitution = 1.0F; // Rebote elástico perfecto contra paredes
body_.linear_damping = 0.0F; // Sin fricción: mantienen velocidad
body_.angular_damping = 0.0F; // Idem
} }
void Enemy::init(EnemyType type, const Vec2* ship_pos) { void Enemy::init(EnemyType type, const Vec2* ship_pos) {
// Guardar type
type_ = type; type_ = type;
// Carregar shape segons el type const char* shape_file = nullptr;
const char* shape_file; float base_speed = 0.0F;
float drotacio_min; float drotacio_min = 0.0F;
float drotacio_max; float drotacio_max = 0.0F;
float type_mass = 5.0F;
switch (type_) { switch (type_) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
velocity_ = Defaults::Enemies::Pentagon::VELOCITAT; base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
type_mass = 5.0F;
break; break;
case EnemyType::QUADRAT: case EnemyType::QUADRAT:
shape_file = Defaults::Enemies::Cuadrado::SHAPE_FILE; shape_file = Defaults::Enemies::Cuadrado::SHAPE_FILE;
velocity_ = Defaults::Enemies::Cuadrado::VELOCITAT; base_speed = Defaults::Enemies::Cuadrado::VELOCITAT;
drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN; drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX; drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX;
type_mass = 8.0F; // Más pesado, "tanque"
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
break; break;
case EnemyType::MOLINILLO: case EnemyType::MOLINILLO:
shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE; shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE;
velocity_ = Defaults::Enemies::Molinillo::VELOCITAT; base_speed = Defaults::Enemies::Molinillo::VELOCITAT;
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN; drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX; drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX;
type_mass = 4.0F; // Más liviano, ágil
break; break;
default: default:
// Fallback segur: usar valors de PENTAGON std::cerr << "[Enemy] Error: tipo desconocido ("
std::cerr << "[Enemy] Error: type desconegut (" << static_cast<int>(type_) << "), usando PENTAGON\n";
<< static_cast<int>(type_) << "), utilitzant PENTAGON\n";
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
velocity_ = Defaults::Enemies::Pentagon::VELOCITAT; base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
break; break;
} }
// Carregar shape body_.setMass(type_mass);
body_.radius = Defaults::Entities::ENEMY_RADIUS;
// Cargar shape
shape_ = Graphics::ShapeLoader::load(shape_file); shape_ = Graphics::ShapeLoader::load(shape_file);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Enemy] Error: no s'ha pogut load " << shape_file << '\n'; std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n';
} }
// [MODIFIED] Posición aleatòria con comprobación de seguretat // Posición aleatoria con comprobación de seguridad
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_y; float max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
min_x,
max_x,
min_y,
max_y);
if (ship_pos != nullptr) { if (ship_pos != nullptr) {
// [NEW] Safe spawn: attempt to find position away from ship
bool found_safe_position = false; bool found_safe_position = false;
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x; float candidate_x;
float candidate_y; float candidate_y;
if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) {
if (intent_spawn_safe(*ship_pos, candidate_x, candidate_y)) {
center_.x = candidate_x; center_.x = candidate_x;
center_.y = candidate_y; center_.y = candidate_y;
found_safe_position = true; found_safe_position = true;
break; break;
} }
} }
if (!found_safe_position) { if (!found_safe_position) {
// Fallback: spawn anywhere (user's preference) const int RANGE_X = static_cast<int>(max_x - min_x);
int range_x = static_cast<int>(max_x - min_x); const int RANGE_Y = static_cast<int>(max_y - min_y);
int range_y = static_cast<int>(max_y - min_y); center_.x = static_cast<float>((std::rand() % RANGE_X) + static_cast<int>(min_x));
center_.x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x)); center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
center_.y = static_cast<float>((std::rand() % range_y) + static_cast<int>(min_y)); std::cout << "[Enemy] Advertencia: spawn sin zona segura tras "
<< Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intentos\n";
std::cout << "[Enemy] Advertència: spawn sin zona segura después de "
<< Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intents" << '\n';
} }
} else { } else {
// [EXISTING] No ship position: spawn anywhere (backward compatibility) const int RANGE_X = static_cast<int>(max_x - min_x);
int range_x = static_cast<int>(max_x - min_x); const int RANGE_Y = static_cast<int>(max_y - min_y);
int range_y = static_cast<int>(max_y - min_y); center_.x = static_cast<float>((std::rand() % RANGE_X) + static_cast<int>(min_x));
center_.x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x)); center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
center_.y = static_cast<float>((std::rand() % range_y) + static_cast<int>(min_y));
} }
// Angle aleatori de movement // Dirección inicial aleatoria, velocidad escalar según tipo
angle_ = (std::rand() % 360) * Constants::PI / 180.0F; const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(ANGLE_INICIAL, base_speed);
// Rotación visual aleatòria (rad/s) dins del rang del type // Sincronizar body_ con posición inicial
float drotacio_range = drotacio_max - drotacio_min; body_.position = center_;
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / RAND_MAX) * drotacio_range); body_.angle = 0.0F;
body_.angular_velocity = 0.0F;
body_.clearAccumulators();
// Rotación visual aleatoria (independiente del body)
const float DROTACIO_RANGE = drotacio_max - drotacio_min;
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / RAND_MAX) * DROTACIO_RANGE);
rotacio_ = 0.0F; rotacio_ = 0.0F;
// Inicialitzar state de animación // Estado de animación
animacio_ = EnemyAnimation(); // Reset to defaults animacio_ = EnemyAnimation();
animacio_.drotacio_base = drotacio_; animacio_.drotacio_base = drotacio_;
animacio_.drotacio_objetivo = drotacio_; animacio_.drotacio_objetivo = drotacio_;
animacio_.drotacio_t = 1.0F; // Start without interpolating animacio_.drotacio_t = 1.0F;
// [NEW] Inicialitzar invulnerabilitat // Invulnerabilidad post-spawn
timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; // 3.0s timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; // 0.3f brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
// Timer para próximo cambio de dirección (Pentagon)
direction_change_timer_ = 0.0F;
// Activar
esta_ = true; esta_ = true;
} }
void Enemy::update(float delta_time) { void Enemy::update(float delta_time) {
if (!esta_) {
return;
}
// Decremento de invulnerabilidad + LERP de brightness
if (timer_invulnerabilitat_ > 0.0F) {
timer_invulnerabilitat_ -= delta_time;
timer_invulnerabilitat_ = std::max(timer_invulnerabilitat_, 0.0F);
const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
const float T = 1.0F - T_INV;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END;
brightness_ = START + ((END - START) * SMOOTH_T);
}
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición)
switch (type_) {
case EnemyType::PENTAGON:
behaviorPentagon(delta_time);
break;
case EnemyType::QUADRAT:
behaviorQuadrat(delta_time);
break;
case EnemyType::MOLINILLO:
behaviorMolinillo(delta_time);
break;
}
// Animaciones (palpitación + rotación acelerada)
updateAnimation(delta_time);
// Rotación visual (decoración, no afecta movimiento)
rotacio_ += drotacio_ * delta_time;
}
void Enemy::postUpdate(float /*delta_time*/) {
// Sincronizar mirror tras la integración del world.
if (esta_) { if (esta_) {
// [NEW] Update invulnerability timer and brightness center_ = body_.position;
if (timer_invulnerabilitat_ > 0.0F) {
timer_invulnerabilitat_ -= delta_time;
timer_invulnerabilitat_ = std::max(timer_invulnerabilitat_, 0.0F);
// [NEW] Update brightness with LERP during invulnerability
float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
float t = 1.0F - t_inv; // 0.0 → 1.0
float smooth_t = t * t * (3.0F - 2.0F * t); // smoothstep
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END;
brightness_ = START + ((END - START) * smooth_t);
}
// Moviment autònom
mou(delta_time);
// Actualitzar animaciones (palpitació, rotación accelerada)
actualitzar_animacio(delta_time);
// Rotación visual (time-based: drotacio_ está en rad/s)
rotacio_ += drotacio_ * delta_time;
} }
} }
void Enemy::draw() const { void Enemy::draw() const {
if (esta_ && shape_) { if (!esta_ || !shape_) {
// Calculate animated scale (includes invulnerability LERP) return;
float scale = calcular_escala_actual();
// brightness_ is already updated in update()
Rendering::render_shape(renderer_, shape_, center_, rotacio_, scale, 1.0F, brightness_);
} }
const float SCALE = computeCurrentScale();
Rendering::render_shape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_);
} }
void Enemy::mou(float delta_time) { void Enemy::destruir() {
// Dispatcher: crida el comportament específic segons el type esta_ = false;
switch (type_) { body_.velocity = Vec2{};
case EnemyType::PENTAGON: body_.angular_velocity = 0.0F;
comportament_pentagon(delta_time); body_.radius = 0.0F; // No colisiona mientras está inactivo
break;
case EnemyType::QUADRAT:
comportament_quadrat(delta_time);
break;
case EnemyType::MOLINILLO:
comportament_molinillo(delta_time);
break;
default:
// Fallback: comportament bàsic (Pentagon)
comportament_pentagon(delta_time);
break;
}
} }
void Enemy::comportament_pentagon(float delta_time) { void Enemy::setVelocity(float speed) {
// Pentagon: zigzag esquivador (frequent direction changes) // Mantener la dirección actual del body, cambiar solo la magnitud.
// Similar a comportament original pero con probabilitat més alta const float CURRENT_SPEED = body_.velocity.length();
if (CURRENT_SPEED > 0.0F) {
float velocitat_efectiva = velocity_ * delta_time; body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
// Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt)
float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F));
float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F));
float new_y = center_.y + dy;
float new_x = center_.x + dx;
// Obtenir límits segurs
float min_x;
float max_x;
float min_y;
float max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
max_x,
min_y,
max_y);
// Zigzag: canvi de angle més freqüent en tocar límits
if (new_y >= min_y && new_y <= max_y) {
center_.y = new_y;
} else { } else {
// Probabilitat més alta de canvi de angle // Sin dirección actual: usar ángulo aleatorio
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) { const float A = (std::rand() % 360) * Constants::PI / 180.0F;
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) * setVelocityFromAngle(A, speed);
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
if (new_x >= min_x && new_x <= max_x) {
center_.x = new_x;
} else {
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
} }
} }
void Enemy::comportament_quadrat(float delta_time) { void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
// Cuadrado: perseguidor (tracks player position) body_.velocity = angleToDirection(angle_movement) * speed;
}
// Update tracking timer // PENTAGON: zigzag esquivador. Cambios de dirección periódicos (probabilísticos)
// en lugar de detectar paredes; el rebote contra muros lo hace PhysicsWorld
// con restitution=1.0.
void Enemy::behaviorPentagon(float delta_time) {
direction_change_timer_ += delta_time;
// Probabilidad de zigzag por segundo (calibrada para sensación equivalente
// a la versión vieja que disparaba en cada toque de pared).
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F;
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
if (RAND_VAL < ZIGZAG_PROB_PER_SECOND * delta_time) {
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
const float DELTA = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
const float SPEED = body_.velocity.length();
setVelocityFromAngle(NEW_ANGLE, SPEED);
direction_change_timer_ = 0.0F;
}
}
// QUADRAT: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección
// hacia el ship mezclando con tracking_strength_.
void Enemy::behaviorQuadrat(float delta_time) {
tracking_timer_ += delta_time; tracking_timer_ += delta_time;
// Periodically update angle toward ship if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL && ship_position_ != nullptr) {
if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL) {
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
if (ship_position_ != nullptr) { const Vec2 TO_SHIP = *ship_position_ - center_;
// Calculate angle to ship const float DIST = TO_SHIP.length();
float dx = ship_position_->x - center_.x; if (DIST > 0.0F) {
float dy = ship_position_->y - center_.y; const Vec2 DESIRED_DIR = TO_SHIP / DIST;
float target_angle = std::atan2(dy, dx) + (Constants::PI / 2.0F); const float SPEED = body_.velocity.length();
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
// Interpolate toward target angle // Mezcla LERP: velocidad actual con la deseada según tracking_strength_.
float angle_diff = target_angle - angle_; body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
(DESIRED_VEL * tracking_strength_);
// Normalize angle difference to [-π, π] // Renormalizar a la velocidad escalar original
while (angle_diff > Constants::PI) { const float NEW_SPEED = body_.velocity.length();
angle_diff -= 2.0F * Constants::PI; if (NEW_SPEED > 0.0F) {
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
} }
while (angle_diff < -Constants::PI) {
angle_diff += 2.0F * Constants::PI;
}
// Apply tracking strength (uses member variable, defaults to 0.5)
angle_ += angle_diff * tracking_strength_;
} }
} }
// Move in current direction
float velocitat_efectiva = velocity_ * delta_time;
float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F));
float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F));
float new_y = center_.y + dy;
float new_x = center_.x + dx;
// Obtenir límits segurs
float min_x;
float max_x;
float min_y;
float max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
max_x,
min_y,
max_y);
// Bounce on walls (simple reflection)
if (new_y >= min_y && new_y <= max_y) {
center_.y = new_y;
} else {
angle_ = -angle_; // Vertical reflection
}
if (new_x >= min_x && new_x <= max_x) {
center_.x = new_x;
} else {
angle_ = Constants::PI - angle_; // Horizontal reflection
}
} }
void Enemy::comportament_molinillo(float delta_time) { // MOLINILLO: movimiento recto + boost de rotación visual cerca del ship.
// Molinillo: agressiu (fast, straight lines, proximity spin-up) // Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
// con probabilidad mucho menor).
// Check proximity to ship for spin-up effect void Enemy::behaviorMolinillo(float /*delta_time*/) {
// Boost de rotación visual por proximidad al ship
if (ship_position_ != nullptr) { if (ship_position_ != nullptr) {
float dx = ship_position_->x - center_.x; const Vec2 TO_SHIP = *ship_position_ - center_;
float dy = ship_position_->y - center_.y; const float DIST = TO_SHIP.length();
float distance = std::sqrt((dx * dx) + (dy * dy)); if (DIST < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) {
drotacio_ = animacio_.drotacio_base * Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER;
if (distance < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) {
// Temporarily boost rotation speed when near ship
float boost = Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER;
drotacio_ = animacio_.drotacio_base * boost;
} else { } else {
// Normal rotation speed
drotacio_ = animacio_.drotacio_base; drotacio_ = animacio_.drotacio_base;
} }
} }
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
// Fast straight-line movement
float velocitat_efectiva = velocity_ * delta_time;
float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F));
float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F));
float new_y = center_.y + dy;
float new_x = center_.x + dx;
// Obtenir límits segurs
float min_x;
float max_x;
float min_y;
float max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
max_x,
min_y,
max_y);
// Rare angle changes on wall hits
if (new_y >= min_y && new_y <= max_y) {
center_.y = new_y;
} else {
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
if (new_x >= min_x && new_x <= max_x) {
center_.x = new_x;
} else {
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
} }
void Enemy::actualitzar_animacio(float delta_time) { void Enemy::updateAnimation(float delta_time) {
actualitzar_palpitacio(delta_time); updatePalpitation(delta_time);
actualitzar_rotacio_accelerada(delta_time); updateRotationAcceleration(delta_time);
} }
void Enemy::actualitzar_palpitacio(float delta_time) { void Enemy::updatePalpitation(float delta_time) {
if (animacio_.palpitacio_activa) { if (animacio_.palpitacio_activa) {
// Advance phase (2π * frequency * dt)
animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time; animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time;
// Decrement timer
animacio_.palpitacio_temps_restant -= delta_time; animacio_.palpitacio_temps_restant -= delta_time;
// Deactivate when timer expires
if (animacio_.palpitacio_temps_restant <= 0.0F) { if (animacio_.palpitacio_temps_restant <= 0.0F) {
animacio_.palpitacio_activa = false; animacio_.palpitacio_activa = false;
} }
} else { } else {
// Random trigger (probability per second) const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
float rand_val = static_cast<float>(std::rand()) / RAND_MAX; const float TRIGGER_PROB = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
float trigger_prob = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time; if (RAND_VAL < TRIGGER_PROB) {
if (rand_val < trigger_prob) {
// Activate palpitation
animacio_.palpitacio_activa = true; animacio_.palpitacio_activa = true;
animacio_.palpitacio_fase = 0.0F; animacio_.palpitacio_fase = 0.0F;
// Randomize parameters const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
float freq_range = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX - Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN + animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * freq_range); ((static_cast<float>(std::rand()) / RAND_MAX) * FREQ_RANGE);
float amp_range = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX - const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN; Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN + animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * amp_range); ((static_cast<float>(std::rand()) / RAND_MAX) * AMP_RANGE);
float dur_range = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX - const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX -
Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN; Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN;
animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN + animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * dur_range); ((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
} }
} }
} }
void Enemy::actualitzar_rotacio_accelerada(float delta_time) { void Enemy::updateRotationAcceleration(float delta_time) {
if (animacio_.drotacio_t < 1.0F) { if (animacio_.drotacio_t < 1.0F) {
// Transitioning to new target
animacio_.drotacio_t += delta_time / animacio_.drotacio_duracio; animacio_.drotacio_t += delta_time / animacio_.drotacio_duracio;
if (animacio_.drotacio_t >= 1.0F) { if (animacio_.drotacio_t >= 1.0F) {
animacio_.drotacio_t = 1.0F; animacio_.drotacio_t = 1.0F;
animacio_.drotacio_base = animacio_.drotacio_objetivo; // Reached target animacio_.drotacio_base = animacio_.drotacio_objetivo;
drotacio_ = animacio_.drotacio_base; drotacio_ = animacio_.drotacio_base;
} else { } else {
// Smoothstep interpolation: t² * (3 - 2t) const float T = animacio_.drotacio_t;
float t = animacio_.drotacio_t; const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
float smooth_t = t * t * (3.0F - 2.0F * t); const float INITIAL = animacio_.drotacio_base;
const float TARGET = animacio_.drotacio_objetivo;
// Interpolate between base and target drotacio_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
float initial = animacio_.drotacio_base;
float target = animacio_.drotacio_objetivo;
drotacio_ = initial + ((target - initial) * smooth_t);
} }
} else { } else {
// Random trigger for new acceleration const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
float rand_val = static_cast<float>(std::rand()) / RAND_MAX; const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
float trigger_prob = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time; if (RAND_VAL < TRIGGER_PROB) {
if (rand_val < trigger_prob) {
// Start new transition
animacio_.drotacio_t = 0.0F; animacio_.drotacio_t = 0.0F;
// Randomize target speed (multiplier * base) const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
float mult_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX - Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN;
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN; const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN +
float multiplier = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN + ((static_cast<float>(std::rand()) / RAND_MAX) * MULT_RANGE);
((static_cast<float>(std::rand()) / RAND_MAX) * mult_range); animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER;
animacio_.drotacio_objetivo = animacio_.drotacio_base * multiplier; const float DUR_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN;
// Randomize duration
float dur_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN;
animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN + animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * dur_range); ((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
} }
} }
} }
float Enemy::calcular_escala_actual() const { auto Enemy::computeCurrentScale() const -> float {
float scale = 1.0F; float scale = 1.0F;
// [NEW] Invulnerability LERP prioritza sobre palpitació
if (timer_invulnerabilitat_ > 0.0F) { if (timer_invulnerabilitat_ > 0.0F) {
// Calculate t: 0.0 at spawn → 1.0 at end const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T = 1.0F - T_INV;
float t = 1.0F - t_inv; // 0.0 → 1.0 const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
// Apply smoothstep: t² * (3 - 2t)
float smooth_t = t * t * (3.0F - 2.0F * t);
// LERP scale from 0.0 to 1.0
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START; constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END;
scale = START + ((END - START) * smooth_t); scale = START + ((END - START) * SMOOTH_T);
} else if (animacio_.palpitacio_activa) { } else if (animacio_.palpitacio_activa) {
// [EXISTING] Palpitació solo cuando no invulnerable
scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase); scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase);
} }
return scale; return scale;
} }
// [NEW] Stage system API implementations auto Enemy::getBaseVelocity() const -> float {
float Enemy::get_base_velocity() const {
switch (type_) { switch (type_) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
return Defaults::Enemies::Pentagon::VELOCITAT; return Defaults::Enemies::Pentagon::VELOCITAT;
@@ -506,46 +417,34 @@ float Enemy::get_base_velocity() const {
case EnemyType::MOLINILLO: case EnemyType::MOLINILLO:
return Defaults::Enemies::Molinillo::VELOCITAT; return Defaults::Enemies::Molinillo::VELOCITAT;
default: default:
return Defaults::Enemies::Pentagon::VELOCITAT; // Fallback segur return Defaults::Enemies::Pentagon::VELOCITAT;
} }
} }
float Enemy::get_base_rotation() const { auto Enemy::getBaseRotation() const -> float {
// Return the base rotation speed (drotacio_base if available, otherwise current drotacio_)
return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_; return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_;
} }
void Enemy::set_tracking_strength(float strength) { void Enemy::setTrackingStrength(float strength) {
// Only applies to QUADRAT type
if (type_ == EnemyType::QUADRAT) { if (type_ == EnemyType::QUADRAT) {
tracking_strength_ = strength; tracking_strength_ = strength;
} }
} }
// [NEW] Safe spawn helper - checks if position is away from ship auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool {
bool Enemy::intent_spawn_safe(const Vec2& ship_pos, float& out_x, float& out_y) {
// Generate random position within safe bounds
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_y; float max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
min_x,
max_x,
min_y,
max_y);
int range_x = static_cast<int>(max_x - min_x); const int RANGE_X = static_cast<int>(max_x - min_x);
int range_y = static_cast<int>(max_y - min_y); const int RANGE_Y = static_cast<int>(max_y - min_y);
out_x = static_cast<float>((std::rand() % RANGE_X) + static_cast<int>(min_x));
out_y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
out_x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x)); const float DX = out_x - ship_pos.x;
out_y = static_cast<float>((std::rand() % range_y) + static_cast<int>(min_y)); const float DY = out_y - ship_pos.y;
const float DISTANCE = std::sqrt((DX * DX) + (DY * DY));
// Check Euclidean distance to ship return DISTANCE >= Defaults::Enemies::Spawn::SAFETY_DISTANCE;
float dx = out_x - ship_pos.x;
float dy = out_y - ship_pos.y;
float distance = std::sqrt((dx * dx) + (dy * dy));
// Return true if position is safe (>= 36px from ship)
return distance >= Defaults::Enemies::Spawn::SAFETY_DISTANCE;
} }
+61 -64
View File
@@ -1,39 +1,37 @@
// enemy.hpp - Clase para enemigos (ORNIs pentágonos) // enemy.hpp - Clase para enemigos (ORNIs)
// © 1999 Visente i Sergi (versión Pascal) // © 1999 Visente i Sergi (versión Pascal)
// © 2025 Port a C++20 con SDL3 // © 2025 Port a C++20 con SDL3
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cmath>
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp"
// Tipo de enemy // Tipo de enemy
enum class EnemyType : uint8_t { enum class EnemyType : uint8_t {
PENTAGON = 0, // Pentágono esquivador (zigzag) PENTAGON = 0, // Pentágono esquivador (zigzag)
QUADRAT = 1, // Cuadrado perseguidor (tracks ship) QUADRAT = 1, // Cuadrado perseguidor (tracks ship)
MOLINILLO = 2 // Molinillo agressiu (fast, spinning) MOLINILLO = 2 // Molinillo agresivo (rápido, girando)
}; };
// Estat de animación (palpitació i rotación accelerada) // Estado de animación (palpitación + rotación acelerada)
struct EnemyAnimation { struct EnemyAnimation {
// Palpitation (breathing effect) // Palpitación (efecto respiración)
bool palpitacio_activa = false; bool palpitacio_activa = false;
float palpitacio_fase = 0.0F; // Phase in cycle (0.0-2π) float palpitacio_fase = 0.0F;
float palpitacio_frequencia = 2.0F; // Hz (cycles per second) float palpitacio_frequencia = 2.0F;
float palpitacio_amplitud = 0.15F; // Scale variation (±15%) float palpitacio_amplitud = 0.15F;
float palpitacio_temps_restant = 0.0F; // Time remaining (seconds) float palpitacio_temps_restant = 0.0F;
// Rotation acceleration (long-term spin modulation) // Aceleración de rotación visual (modulación a largo plazo)
float drotacio_base = 0.0F; // Base rotation speed (rad/s) float drotacio_base = 0.0F;
float drotacio_objetivo = 0.0F; // Target rotation speed (rad/s) float drotacio_objetivo = 0.0F;
float drotacio_t = 0.0F; // Interpolation progress (0.0-1.0) float drotacio_t = 0.0F;
float drotacio_duracio = 0.0F; // Duration of transition (seconds) float drotacio_duracio = 0.0F;
}; };
class Enemy : public Entities::Entity { class Enemy : public Entities::Entity {
@@ -45,79 +43,78 @@ class Enemy : public Entities::Entity {
void init() override { init(EnemyType::PENTAGON, nullptr); } void init() override { init(EnemyType::PENTAGON, nullptr); }
void init(EnemyType type, const Vec2* ship_pos = nullptr); void init(EnemyType type, const Vec2* ship_pos = nullptr);
void update(float delta_time) override; void update(float delta_time) override;
void postUpdate(float delta_time) override;
void draw() const override; void draw() const override;
// Override: Interfície d'Entity // Override: Interfaz de Entity
[[nodiscard]] bool isActive() const override { return esta_; } [[nodiscard]] auto isActive() const -> bool override { return esta_; }
// Override: Interfície de colisión // Override: Interfaz de colisión
[[nodiscard]] float getCollisionRadius() const override { [[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::ENEMY_RADIUS; return Defaults::Entities::ENEMY_RADIUS;
} }
[[nodiscard]] bool isCollidable() const override { [[nodiscard]] auto isCollidable() const -> bool override {
return esta_ && timer_invulnerabilitat_ <= 0.0F; return esta_ && timer_invulnerabilitat_ <= 0.0F;
} }
// Getters (API pública sin canvis) // Marcar destruido (desactiva el cuerpo físicamente: radius=0)
void destruir() { esta_ = false; } void destruir();
[[nodiscard]] float get_drotacio() const { return drotacio_; }
[[nodiscard]] Vec2 getVelocityVector() const { // Getters
return { [[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; }
.x = velocity_ * std::cos(angle_ - (Constants::PI / 2.0F)), [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
.y = velocity_ * std::sin(angle_ - (Constants::PI / 2.0F))};
}
// Set ship position reference for tracking behavior // Set ship position reference for tracking behavior
void set_ship_position(const Vec2* ship_pos) { ship_position_ = ship_pos; } void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
// [NEW] Getters for stage system (base stats) // Stage system API (base stats)
[[nodiscard]] float get_base_velocity() const; [[nodiscard]] auto getBaseVelocity() const -> float;
[[nodiscard]] float get_base_rotation() const; [[nodiscard]] auto getBaseRotation() const -> float;
[[nodiscard]] EnemyType getType() const { return type_; } [[nodiscard]] auto getType() const -> EnemyType { return type_; }
// [NEW] Setters for difficulty multipliers (stage system) // Setters para multiplicadores de dificultad (stage system).
void set_velocity(float vel) { velocity_ = vel; } // Establecen la velocidad escalar deseada manteniendo la dirección
void set_rotation(float rot) { // actual del body_.velocity.
void setVelocity(float speed);
void setRotation(float rot) {
drotacio_ = rot; drotacio_ = rot;
animacio_.drotacio_base = rot; animacio_.drotacio_base = rot;
} }
void set_tracking_strength(float strength); void setTrackingStrength(float strength);
// [NEW] Invulnerability queries // Invulnerabilidad
[[nodiscard]] bool isInvulnerable() const { return timer_invulnerabilitat_ > 0.0F; } [[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; }
[[nodiscard]] float get_temps_invulnerabilitat() const { return timer_invulnerabilitat_; } [[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
private: private:
// Membres específics d'Enemy (heretats: renderer_, shape_, center_, angle_, brightness_) // Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_)
float velocity_; float drotacio_; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
float drotacio_; // Delta rotación visual (rad/s) float rotacio_; // Rotación visual acumulada (no afecta movimiento)
float rotacio_; // Rotación visual acumulada
bool esta_; bool esta_;
// [NEW] Enemy type and configuration
EnemyType type_; EnemyType type_;
// [NEW] Animation state
EnemyAnimation animacio_; EnemyAnimation animacio_;
// [NEW] Behavior state (type-specific) // Comportamiento type-specific
float tracking_timer_; // For Cuadrado: time since last angle update float tracking_timer_; // Quadrat: tiempo desde último update de dirección
const Vec2* ship_position_; // Pointer to ship position (for tracking) const Vec2* ship_position_; // Puntero a posición de la nave (para tracking)
float tracking_strength_; // For Cuadrado: tracking intensity (0.0-1.5), default 0.5 float tracking_strength_; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5
float direction_change_timer_; // Pentagon: tiempo para próximo cambio de dirección
// [NEW] Invulnerability state // Invulnerabilidad post-spawn
float timer_invulnerabilitat_; // Countdown timer (seconds), 0.0f = vulnerable float timer_invulnerabilitat_;
// [EXISTING] Private methods // Métodos privados
void mou(float delta_time); void updateAnimation(float delta_time);
void updatePalpitation(float delta_time);
void updateRotationAcceleration(float delta_time);
void behaviorPentagon(float delta_time);
void behaviorQuadrat(float delta_time);
void behaviorMolinillo(float delta_time);
[[nodiscard]] auto computeCurrentScale() const -> float;
auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
// [NEW] Private methods // Helper: setear body_.velocity desde un ángulo y magnitud.
void actualitzar_animacio(float delta_time); // angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
void actualitzar_palpitacio(float delta_time); void setVelocityFromAngle(float angle_movement, float speed);
void actualitzar_rotacio_accelerada(float delta_time);
void comportament_pentagon(float delta_time);
void comportament_quadrat(float delta_time);
void comportament_molinillo(float delta_time);
[[nodiscard]] float calcular_escala_actual() const; // Returns scale with palpitation applied
bool intent_spawn_safe(const Vec2& ship_pos, float& out_x, float& out_y);
}; };
+10 -4
View File
@@ -150,7 +150,7 @@ void GameScene::init() {
stage_manager_->init(); stage_manager_->init();
// [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) // [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
stage_manager_->getSpawnController().set_ship_position(&ships_[0].getCenter()); stage_manager_->getSpawnController().setShipPosition(&ships_[0].getCenter());
// Inicialitzar timers de muerte per player // Inicialitzar timers de muerte per player
hit_timer_per_player_[0] = 0.0F; hit_timer_per_player_[0] = 0.0F;
@@ -195,10 +195,13 @@ void GameScene::init() {
} }
} }
// [MODIFIED] Initialize enemies as inactive (stage system will spawn them) // [MODIFIED] Initialize enemies as inactive (stage system will spawn them).
// Registramos el body al world incluso inactivo: con radius=0 no colisiona
// ni se mueve, y al init() del stage system se activa sin re-registrar.
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
enemy = Enemy(sdl_.getRenderer()); enemy = Enemy(sdl_.getRenderer());
enemy.set_ship_position(&ships_[0].getCenter()); // Set ship reference (P1 for now) enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now)
physics_world_.addBody(&enemy.getBody());
// DON'T call enemy.init() here - stage system handles spawning // DON'T call enemy.init() here - stage system handles spawning
} }
@@ -224,6 +227,9 @@ void GameScene::update(float delta_time) {
for (auto& ship : ships_) { for (auto& ship : ships_) {
ship.postUpdate(delta_time); ship.postUpdate(delta_time);
} }
for (auto& enemy : enemies_) {
enemy.postUpdate(delta_time);
}
// Processar disparos (state-based, no event-based) // Processar disparos (state-based, no event-based)
if (game_over_state_ == GameOverState::NONE) { if (game_over_state_ == GameOverState::NONE) {
@@ -1009,7 +1015,7 @@ void GameScene::detectar_col·lisions_bales_enemics() {
VELOCITAT_EXPLOSIO, // 50 px/s (explosión suau) VELOCITAT_EXPLOSIO, // 50 px/s (explosión suau)
enemy.getBrightness(), // Heredar brightness enemy.getBrightness(), // Heredar brightness
vel_enemic, // Heredar velocity vel_enemic, // Heredar velocity
enemy.get_drotacio(), // Heredar velocity angular (trayectorias curvas) enemy.getRotationDelta(), // Heredar velocity angular (trayectorias curvas)
0.0F // Sin herencia visual (rotación aleatoria) 0.0F // Sin herencia visual (rotación aleatoria)
); );
@@ -163,15 +163,15 @@ void SpawnController::aplicar_multiplicadors(Enemy& enemy) const {
} }
// Apply velocity multiplier // Apply velocity multiplier
float base_vel = enemy.get_base_velocity(); float base_vel = enemy.getBaseVelocity();
enemy.set_velocity(base_vel * config_->multiplicadors.velocity); enemy.setVelocity(base_vel * config_->multiplicadors.velocity);
// Apply rotation multiplier // Apply rotation multiplier
float base_rot = enemy.get_base_rotation(); float base_rot = enemy.getBaseRotation();
enemy.set_rotation(base_rot * config_->multiplicadors.rotation); enemy.setRotation(base_rot * config_->multiplicadors.rotation);
// Apply tracking strength (only affects QUADRAT) // Apply tracking strength (only affects QUADRAT)
enemy.set_tracking_strength(config_->multiplicadors.tracking_strength); enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
} }
} // namespace StageSystem } // namespace StageSystem
@@ -39,7 +39,7 @@ class SpawnController {
[[nodiscard]] uint8_t get_enemics_spawnejats() const; [[nodiscard]] uint8_t get_enemics_spawnejats() const;
// [NEW] Set ship position reference for safe spawn // [NEW] Set ship position reference for safe spawn
void set_ship_position(const Vec2* ship_pos) { ship_position_ = ship_pos; } void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
private: private:
const StageConfig* config_; // Non-owning pointer to current stage config const StageConfig* config_; // Non-owning pointer to current stage config