413 lines
16 KiB
C++
413 lines
16 KiB
C++
// enemy.cpp - Implementación de enemigos (ORNIs)
|
|
// © 2026 JailDesigner
|
|
|
|
#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"
|
|
#include "game/entities/enemy_config.hpp"
|
|
#include "game/entities/enemy_registry.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).
|
|
auto velocityToAngle(const Vec2& velocity) -> float {
|
|
if (velocity.lengthSquared() < 0.0001F) {
|
|
return 0.0F;
|
|
}
|
|
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Enemy::Enemy(Rendering::Renderer* renderer)
|
|
: Entity(renderer) {
|
|
brightness_ = Defaults::Brightness::ENEMIC;
|
|
|
|
// Cuerpo físico — defaults comuns; init() ajusta mass/radius segons el tipus.
|
|
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS);
|
|
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
|
|
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
|
|
body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING;
|
|
body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING;
|
|
}
|
|
|
|
void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
|
type_ = type;
|
|
config_ = &EnemyRegistry::get(type);
|
|
const EnemyConfig& cfg = *config_;
|
|
|
|
collision_radius_ = cfg.physics.collision_radius;
|
|
|
|
// Cas Square: resetejar tracking timer al spawn.
|
|
if (type_ == EnemyType::SQUARE) {
|
|
tracking_timer_ = 0.0F;
|
|
tracking_strength_ = cfg.behavior.tracking_strength;
|
|
}
|
|
|
|
body_.setMass(cfg.physics.mass);
|
|
body_.radius = collision_radius_;
|
|
|
|
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
|
if (!shape_ || !shape_->isValid()) {
|
|
std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
|
|
}
|
|
|
|
// Posición aleatoria con comprobación de seguridad
|
|
float min_x;
|
|
float max_x;
|
|
float min_y;
|
|
float max_y;
|
|
Constants::getSafePlayAreaBounds(collision_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, collision_radius_, 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 zone 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, cfg.physics.speed);
|
|
|
|
body_.position = center_;
|
|
body_.angle = 0.0F;
|
|
body_.angular_velocity = 0.0F;
|
|
body_.clearAccumulators();
|
|
|
|
// Rotación visual aleatoria dins del rang del tipus
|
|
const float ROTATION_RANGE = cfg.physics.rotation_delta_max - cfg.physics.rotation_delta_min;
|
|
rotation_delta_ = cfg.physics.rotation_delta_min +
|
|
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_RANGE);
|
|
rotation_ = 0.0F;
|
|
|
|
animation_ = EnemyAnimation();
|
|
animation_.rotation_delta_base = rotation_delta_;
|
|
animation_.rotation_delta_target = rotation_delta_;
|
|
animation_.rotation_delta_t = 1.0F;
|
|
|
|
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
|
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
|
|
|
|
direction_change_timer_ = 0.0F;
|
|
|
|
is_active_ = true;
|
|
}
|
|
|
|
void Enemy::update(float delta_time) {
|
|
if (!is_active_) {
|
|
return;
|
|
}
|
|
|
|
wound_expired_this_frame_ = false;
|
|
if (wounded_timer_ > 0.0F) {
|
|
wounded_timer_ -= delta_time;
|
|
if (wounded_timer_ <= 0.0F) {
|
|
wounded_timer_ = 0.0F;
|
|
wound_expired_this_frame_ = true;
|
|
}
|
|
}
|
|
|
|
if (invulnerability_timer_ > 0.0F) {
|
|
invulnerability_timer_ -= delta_time;
|
|
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
|
|
|
|
const float T_INV = invulnerability_timer_ / 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);
|
|
}
|
|
|
|
if (!isWounded()) {
|
|
switch (type_) {
|
|
case EnemyType::PENTAGON:
|
|
behaviorPentagon(delta_time);
|
|
break;
|
|
case EnemyType::SQUARE:
|
|
behaviorSquare(delta_time);
|
|
break;
|
|
case EnemyType::PINWHEEL:
|
|
behaviorPinwheel(delta_time);
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateAnimation(delta_time);
|
|
|
|
rotation_ += rotation_delta_ * delta_time;
|
|
}
|
|
|
|
void Enemy::postUpdate(float /*delta_time*/) {
|
|
if (is_active_) {
|
|
center_ = body_.position;
|
|
}
|
|
}
|
|
|
|
void Enemy::draw() const {
|
|
if (!is_active_ || !shape_) {
|
|
return;
|
|
}
|
|
const float SCALE = computeCurrentScale();
|
|
SDL_Color color = config_->colors.normal;
|
|
|
|
// Parpadeo dorado mientras está herido.
|
|
if (wounded_timer_ > 0.0F) {
|
|
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ;
|
|
const float T = std::fmod(wounded_timer_, CYCLE);
|
|
if (T < (CYCLE / 2.0F)) {
|
|
color = config_->colors.wounded;
|
|
}
|
|
}
|
|
|
|
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color);
|
|
}
|
|
|
|
void Enemy::destroy() {
|
|
is_active_ = false;
|
|
body_.velocity = Vec2{};
|
|
body_.angular_velocity = 0.0F;
|
|
body_.radius = 0.0F;
|
|
wounded_timer_ = 0.0F;
|
|
wound_expired_this_frame_ = false;
|
|
last_hit_by_ = 0xFF;
|
|
}
|
|
|
|
void Enemy::hurt(uint8_t shooter_id) {
|
|
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
|
|
last_hit_by_ = shooter_id;
|
|
}
|
|
|
|
void Enemy::applyImpulse(const Vec2& impulse) {
|
|
body_.applyImpulse(impulse);
|
|
}
|
|
|
|
void Enemy::setVelocity(float speed) {
|
|
const float CURRENT_SPEED = body_.velocity.length();
|
|
if (CURRENT_SPEED > 0.0F) {
|
|
body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
|
|
} else {
|
|
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;
|
|
|
|
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
|
if (RAND_VAL < config_->behavior.zigzag_prob_per_second * delta_time) {
|
|
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
|
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
|
|
config_->behavior.angle_change_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;
|
|
}
|
|
}
|
|
|
|
// SQUARE: tracking discreto cada tracking_interval. Ajusta dirección
|
|
// hacia el ship mezclando con tracking_strength_.
|
|
void Enemy::behaviorSquare(float delta_time) {
|
|
tracking_timer_ += delta_time;
|
|
|
|
if (tracking_timer_ >= config_->behavior.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;
|
|
|
|
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
|
|
(DESIRED_VEL * tracking_strength_);
|
|
|
|
const float NEW_SPEED = body_.velocity.length();
|
|
if (NEW_SPEED > 0.0F) {
|
|
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship.
|
|
void Enemy::behaviorPinwheel(float /*delta_time*/) {
|
|
if (ship_position_ != nullptr) {
|
|
const Vec2 TO_SHIP = *ship_position_ - center_;
|
|
const float DIST = TO_SHIP.length();
|
|
if (DIST < config_->behavior.proximity_distance) {
|
|
rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier;
|
|
} else {
|
|
rotation_delta_ = animation_.rotation_delta_base;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Enemy::updateAnimation(float delta_time) {
|
|
updatePulse(delta_time);
|
|
updateRotationAcceleration(delta_time);
|
|
}
|
|
|
|
void Enemy::updatePulse(float delta_time) {
|
|
if (animation_.pulse_active) {
|
|
animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time;
|
|
animation_.pulse_time_remaining -= delta_time;
|
|
if (animation_.pulse_time_remaining <= 0.0F) {
|
|
animation_.pulse_active = false;
|
|
}
|
|
} else {
|
|
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
|
const float TRIGGER_PROB = Defaults::Enemies::Animation::PULSE_TRIGGER_PROB * delta_time;
|
|
if (RAND_VAL < TRIGGER_PROB) {
|
|
animation_.pulse_active = true;
|
|
animation_.pulse_phase = 0.0F;
|
|
|
|
const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX -
|
|
Defaults::Enemies::Animation::PULSE_FREQ_MIN;
|
|
animation_.pulse_frequency = Defaults::Enemies::Animation::PULSE_FREQ_MIN +
|
|
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
|
|
|
|
const float AMP_RANGE = Defaults::Enemies::Animation::PULSE_AMPLITUD_MAX -
|
|
Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN;
|
|
animation_.pulse_amplitude = Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN +
|
|
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
|
|
|
|
const float DUR_RANGE = Defaults::Enemies::Animation::PULSE_DURATION_MAX -
|
|
Defaults::Enemies::Animation::PULSE_DURATION_MIN;
|
|
animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURATION_MIN +
|
|
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Enemy::updateRotationAcceleration(float delta_time) {
|
|
if (animation_.rotation_delta_t < 1.0F) {
|
|
animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration;
|
|
if (animation_.rotation_delta_t >= 1.0F) {
|
|
animation_.rotation_delta_t = 1.0F;
|
|
animation_.rotation_delta_base = animation_.rotation_delta_target;
|
|
rotation_delta_ = animation_.rotation_delta_base;
|
|
} else {
|
|
const float T = animation_.rotation_delta_t;
|
|
const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
|
|
const float INITIAL = animation_.rotation_delta_base;
|
|
const float TARGET = animation_.rotation_delta_target;
|
|
rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
|
|
}
|
|
} else {
|
|
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
|
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTATION_ACCEL_TRIGGER_PROB * delta_time;
|
|
if (RAND_VAL < TRIGGER_PROB) {
|
|
animation_.rotation_delta_t = 0.0F;
|
|
|
|
const float MULT_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MAX -
|
|
Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN;
|
|
const float MULTIPLIER = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN +
|
|
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE);
|
|
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
|
|
|
|
const float DUR_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MAX -
|
|
Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN;
|
|
animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN +
|
|
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto Enemy::computeCurrentScale() const -> float {
|
|
float scale = 1.0F;
|
|
if (invulnerability_timer_ > 0.0F) {
|
|
const float T_INV = invulnerability_timer_ / 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 (animation_.pulse_active) {
|
|
scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase);
|
|
}
|
|
return scale;
|
|
}
|
|
|
|
auto Enemy::getBaseVelocity() const -> float {
|
|
return EnemyRegistry::get(type_).physics.speed;
|
|
}
|
|
|
|
auto Enemy::getBaseRotation() const -> float {
|
|
return animation_.rotation_delta_base != 0.0F ? animation_.rotation_delta_base : rotation_delta_;
|
|
}
|
|
|
|
void Enemy::setTrackingStrength(float strength) {
|
|
if (type_ == EnemyType::SQUARE) {
|
|
tracking_strength_ = strength;
|
|
}
|
|
}
|
|
|
|
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float& out_x, float& out_y) -> bool {
|
|
float min_x;
|
|
float max_x;
|
|
float min_y;
|
|
float max_y;
|
|
Constants::getSafePlayAreaBounds(collision_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;
|
|
}
|