Files
orni-attack/source/game/entities/enemy.cpp
T
JailDesigner 27242f54fe 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>
2026-05-19 13:41:05 +02:00

451 lines
18 KiB
C++

// enemy.cpp - Implementación de enemigos (ORNIs)
// © 1999 Visente i Sergi (versión Pascal)
// © 2025 Port a C++20 con SDL3
#include "game/entities/enemy.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/types.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)
: Entity(renderer),
drotacio_(0.0F),
rotacio_(0.0F),
esta_(false),
type_(EnemyType::PENTAGON),
tracking_timer_(0.0F),
ship_position_(nullptr),
tracking_strength_(0.5F),
direction_change_timer_(0.0F),
timer_invulnerabilitat_(0.0F) {
brightness_ = Defaults::Brightness::ENEMIC;
// Configuración del cuerpo físico — defaults para enemy genérico.
// 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) {
type_ = type;
const char* shape_file = nullptr;
float base_speed = 0.0F;
float drotacio_min = 0.0F;
float drotacio_max = 0.0F;
float type_mass = 5.0F;
switch (type_) {
case EnemyType::PENTAGON:
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
type_mass = 5.0F;
break;
case EnemyType::QUADRAT:
shape_file = Defaults::Enemies::Cuadrado::SHAPE_FILE;
base_speed = Defaults::Enemies::Cuadrado::VELOCITAT;
drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX;
type_mass = 8.0F; // Más pesado, "tanque"
tracking_timer_ = 0.0F;
break;
case EnemyType::MOLINILLO:
shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE;
base_speed = Defaults::Enemies::Molinillo::VELOCITAT;
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX;
type_mass = 4.0F; // Más liviano, ágil
break;
default:
std::cerr << "[Enemy] Error: tipo desconocido ("
<< static_cast<int>(type_) << "), usando PENTAGON\n";
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
break;
}
body_.setMass(type_mass);
body_.radius = Defaults::Entities::ENEMY_RADIUS;
// Cargar shape
shape_ = Graphics::ShapeLoader::load(shape_file);
if (!shape_ || !shape_->isValid()) {
std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n';
}
// Posición aleatoria con comprobación de seguridad
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);
if (ship_pos != nullptr) {
bool found_safe_position = false;
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x;
float candidate_y;
if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) {
center_.x = candidate_x;
center_.y = candidate_y;
found_safe_position = true;
break;
}
}
if (!found_safe_position) {
const int RANGE_X = static_cast<int>(max_x - min_x);
const 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_.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";
}
} else {
const int RANGE_X = static_cast<int>(max_x - min_x);
const 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_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
}
// Dirección inicial aleatoria, velocidad escalar según tipo
const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(ANGLE_INICIAL, base_speed);
// Sincronizar body_ con posición inicial
body_.position = center_;
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;
// Estado de animación
animacio_ = EnemyAnimation();
animacio_.drotacio_base = drotacio_;
animacio_.drotacio_objetivo = drotacio_;
animacio_.drotacio_t = 1.0F;
// Invulnerabilidad post-spawn
timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
// Timer para próximo cambio de dirección (Pentagon)
direction_change_timer_ = 0.0F;
esta_ = true;
}
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_) {
center_ = body_.position;
}
}
void Enemy::draw() const {
if (!esta_ || !shape_) {
return;
}
const float SCALE = computeCurrentScale();
Rendering::render_shape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_);
}
void Enemy::destruir() {
esta_ = false;
body_.velocity = Vec2{};
body_.angular_velocity = 0.0F;
body_.radius = 0.0F; // No colisiona mientras está inactivo
}
void Enemy::setVelocity(float speed) {
// Mantener la dirección actual del body, cambiar solo la magnitud.
const float CURRENT_SPEED = body_.velocity.length();
if (CURRENT_SPEED > 0.0F) {
body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
} else {
// Sin dirección actual: usar ángulo aleatorio
const float A = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(A, speed);
}
}
void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
body_.velocity = angleToDirection(angle_movement) * speed;
}
// 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;
if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL && ship_position_ != nullptr) {
tracking_timer_ = 0.0F;
const Vec2 TO_SHIP = *ship_position_ - center_;
const float DIST = TO_SHIP.length();
if (DIST > 0.0F) {
const Vec2 DESIRED_DIR = TO_SHIP / DIST;
const float SPEED = body_.velocity.length();
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
// Mezcla LERP: velocidad actual con la deseada según tracking_strength_.
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
(DESIRED_VEL * tracking_strength_);
// Renormalizar a la velocidad escalar original
const float NEW_SPEED = body_.velocity.length();
if (NEW_SPEED > 0.0F) {
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
}
}
}
}
// MOLINILLO: movimiento recto + boost de rotación visual cerca del ship.
// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
// con probabilidad mucho menor).
void Enemy::behaviorMolinillo(float /*delta_time*/) {
// Boost de rotación visual por proximidad al ship
if (ship_position_ != nullptr) {
const Vec2 TO_SHIP = *ship_position_ - center_;
const float DIST = TO_SHIP.length();
if (DIST < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) {
drotacio_ = animacio_.drotacio_base * Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER;
} else {
drotacio_ = animacio_.drotacio_base;
}
}
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
}
void Enemy::updateAnimation(float delta_time) {
updatePalpitation(delta_time);
updateRotationAcceleration(delta_time);
}
void Enemy::updatePalpitation(float delta_time) {
if (animacio_.palpitacio_activa) {
animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time;
animacio_.palpitacio_temps_restant -= delta_time;
if (animacio_.palpitacio_temps_restant <= 0.0F) {
animacio_.palpitacio_activa = false;
}
} else {
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
const float TRIGGER_PROB = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) {
animacio_.palpitacio_activa = true;
animacio_.palpitacio_fase = 0.0F;
const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * FREQ_RANGE);
const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * AMP_RANGE);
const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX -
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);
}
}
}
void Enemy::updateRotationAcceleration(float delta_time) {
if (animacio_.drotacio_t < 1.0F) {
animacio_.drotacio_t += delta_time / animacio_.drotacio_duracio;
if (animacio_.drotacio_t >= 1.0F) {
animacio_.drotacio_t = 1.0F;
animacio_.drotacio_base = animacio_.drotacio_objetivo;
drotacio_ = animacio_.drotacio_base;
} else {
const float T = animacio_.drotacio_t;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
const float INITIAL = animacio_.drotacio_base;
const float TARGET = animacio_.drotacio_objetivo;
drotacio_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
}
} else {
const float RAND_VAL = static_cast<float>(std::rand()) / RAND_MAX;
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) {
animacio_.drotacio_t = 0.0F;
const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN;
const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * MULT_RANGE);
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;
animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN +
((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
}
}
}
auto Enemy::computeCurrentScale() const -> float {
float scale = 1.0F;
if (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_SCALE_START;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END;
scale = START + ((END - START) * SMOOTH_T);
} else if (animacio_.palpitacio_activa) {
scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase);
}
return scale;
}
auto Enemy::getBaseVelocity() const -> float {
switch (type_) {
case EnemyType::PENTAGON:
return Defaults::Enemies::Pentagon::VELOCITAT;
case EnemyType::QUADRAT:
return Defaults::Enemies::Cuadrado::VELOCITAT;
case EnemyType::MOLINILLO:
return Defaults::Enemies::Molinillo::VELOCITAT;
default:
return Defaults::Enemies::Pentagon::VELOCITAT;
}
}
auto Enemy::getBaseRotation() const -> float {
return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_;
}
void Enemy::setTrackingStrength(float strength) {
if (type_ == EnemyType::QUADRAT) {
tracking_strength_ = strength;
}
}
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool {
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);
const int RANGE_X = static_cast<int>(max_x - min_x);
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));
const float DX = out_x - ship_pos.x;
const float DY = out_y - ship_pos.y;
const float DISTANCE = std::sqrt((DX * DX) + (DY * DY));
return DISTANCE >= Defaults::Enemies::Spawn::SAFETY_DISTANCE;
}