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:
+248
-349
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user