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)
// © 2025 Port a C++20 con SDL3
@@ -16,488 +16,399 @@
#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),
velocity_(0.0F),
drotacio_(0.0F),
rotacio_(0.0F),
esta_(false),
type_(EnemyType::PENTAGON),
tracking_timer_(0.0F),
ship_position_(nullptr),
tracking_strength_(0.5F), // Default tracking strength
timer_invulnerabilitat_(0.0F) { // Start vulnerable
// [NUEVO] Brightness específic per enemigos
tracking_strength_(0.5F),
direction_change_timer_(0.0F),
timer_invulnerabilitat_(0.0F) {
brightness_ = Defaults::Brightness::ENEMIC;
// [NUEVO] Forma es carrega a init() segons el type
// Constructor no carrega shape per permetre type diferents
// 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) {
// Guardar type
type_ = type;
// Carregar shape segons el type
const char* shape_file;
float drotacio_min;
float drotacio_max;
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;
velocity_ = Defaults::Enemies::Pentagon::VELOCITAT;
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;
velocity_ = Defaults::Enemies::Cuadrado::VELOCITAT;
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;
velocity_ = Defaults::Enemies::Molinillo::VELOCITAT;
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:
// Fallback segur: usar valors de PENTAGON
std::cerr << "[Enemy] Error: type desconegut ("
<< static_cast<int>(type_) << "), utilitzant PENTAGON\n";
std::cerr << "[Enemy] Error: tipo desconocido ("
<< static_cast<int>(type_) << "), usando PENTAGON\n";
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_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
break;
}
// Carregar shape
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 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 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);
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
if (ship_pos != nullptr) {
// [NEW] Safe spawn: attempt to find position away from ship
bool found_safe_position = false;
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x;
float candidate_y;
if (intent_spawn_safe(*ship_pos, candidate_x, 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) {
// Fallback: spawn anywhere (user's preference)
int range_x = static_cast<int>(max_x - min_x);
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] Advertència: spawn sin zona segura después de "
<< Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intents" << '\n';
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 {
// [EXISTING] No ship position: spawn anywhere (backward compatibility)
int range_x = static_cast<int>(max_x - min_x);
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));
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));
}
// Angle aleatori de movement
angle_ = (std::rand() % 360) * Constants::PI / 180.0F;
// 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);
// Rotación visual aleatòria (rad/s) dins del rang del type
float drotacio_range = drotacio_max - drotacio_min;
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / RAND_MAX) * drotacio_range);
// 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;
// Inicialitzar state de animación
animacio_ = EnemyAnimation(); // Reset to defaults
// Estado de animación
animacio_ = EnemyAnimation();
animacio_.drotacio_base = drotacio_;
animacio_.drotacio_objetivo = drotacio_;
animacio_.drotacio_t = 1.0F; // Start without interpolating
animacio_.drotacio_t = 1.0F;
// [NEW] Inicialitzar invulnerabilitat
timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; // 3.0s
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; // 0.3f
// 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;
// Activar
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_) {
// [NEW] Update invulnerability timer and brightness
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;
center_ = body_.position;
}
}
void Enemy::draw() const {
if (esta_ && shape_) {
// Calculate animated scale (includes invulnerability LERP)
float scale = calcular_escala_actual();
// brightness_ is already updated in update()
Rendering::render_shape(renderer_, shape_, center_, rotacio_, scale, 1.0F, brightness_);
if (!esta_ || !shape_) {
return;
}
const float SCALE = computeCurrentScale();
Rendering::render_shape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_);
}
void Enemy::mou(float delta_time) {
// Dispatcher: crida el comportament específic segons el type
switch (type_) {
case EnemyType::PENTAGON:
comportament_pentagon(delta_time);
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::destruir() {
esta_ = false;
body_.velocity = Vec2{};
body_.angular_velocity = 0.0F;
body_.radius = 0.0F; // No colisiona mientras está inactivo
}
void Enemy::comportament_pentagon(float delta_time) {
// Pentagon: zigzag esquivador (frequent direction changes)
// Similar a comportament original pero con probabilitat més alta
float velocitat_efectiva = velocity_ * delta_time;
// 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;
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 {
// Probabilitat més alta de canvi de angle
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;
}
}
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;
}
// Sin dirección actual: usar ángulo aleatorio
const float A = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(A, speed);
}
}
void Enemy::comportament_quadrat(float delta_time) {
// Cuadrado: perseguidor (tracks player position)
void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
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;
// Periodically update angle toward ship
if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL) {
if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL && ship_position_ != nullptr) {
tracking_timer_ = 0.0F;
if (ship_position_ != nullptr) {
// Calculate angle to ship
float dx = ship_position_->x - center_.x;
float dy = ship_position_->y - center_.y;
float target_angle = std::atan2(dy, dx) + (Constants::PI / 2.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;
// Interpolate toward target angle
float angle_diff = target_angle - angle_;
// Mezcla LERP: velocidad actual con la deseada según tracking_strength_.
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
(DESIRED_VEL * tracking_strength_);
// Normalize angle difference to [-π, π]
while (angle_diff > Constants::PI) {
angle_diff -= 2.0F * Constants::PI;
// 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);
}
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: agressiu (fast, straight lines, proximity spin-up)
// Check proximity to ship for spin-up effect
// 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) {
float dx = ship_position_->x - center_.x;
float dy = ship_position_->y - center_.y;
float distance = std::sqrt((dx * dx) + (dy * dy));
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;
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 {
// Normal rotation speed
drotacio_ = animacio_.drotacio_base;
}
}
// 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;
}
}
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
}
void Enemy::actualitzar_animacio(float delta_time) {
actualitzar_palpitacio(delta_time);
actualitzar_rotacio_accelerada(delta_time);
void Enemy::updateAnimation(float delta_time) {
updatePalpitation(delta_time);
updateRotationAcceleration(delta_time);
}
void Enemy::actualitzar_palpitacio(float delta_time) {
void Enemy::updatePalpitation(float delta_time) {
if (animacio_.palpitacio_activa) {
// Advance phase (2π * frequency * dt)
animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time;
// Decrement timer
animacio_.palpitacio_temps_restant -= delta_time;
// Deactivate when timer expires
if (animacio_.palpitacio_temps_restant <= 0.0F) {
animacio_.palpitacio_activa = false;
}
} else {
// Random trigger (probability per second)
float rand_val = static_cast<float>(std::rand()) / RAND_MAX;
float trigger_prob = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
if (rand_val < trigger_prob) {
// Activate palpitation
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;
// Randomize parameters
float freq_range = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
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);
((static_cast<float>(std::rand()) / RAND_MAX) * FREQ_RANGE);
float amp_range = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
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);
((static_cast<float>(std::rand()) / RAND_MAX) * AMP_RANGE);
float dur_range = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX -
Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN;
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);
((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) {
// Transitioning to new target
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; // Reached target
animacio_.drotacio_base = animacio_.drotacio_objetivo;
drotacio_ = animacio_.drotacio_base;
} else {
// Smoothstep interpolation: t² * (3 - 2t)
float t = animacio_.drotacio_t;
float smooth_t = t * t * (3.0F - 2.0F * t);
// Interpolate between base and target
float initial = animacio_.drotacio_base;
float target = animacio_.drotacio_objetivo;
drotacio_ = initial + ((target - initial) * smooth_t);
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 {
// Random trigger for new acceleration
float rand_val = static_cast<float>(std::rand()) / RAND_MAX;
float trigger_prob = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
if (rand_val < trigger_prob) {
// Start new transition
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;
// Randomize target speed (multiplier * base)
float mult_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
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);
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;
animacio_.drotacio_objetivo = animacio_.drotacio_base * multiplier;
// Randomize duration
float dur_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN;
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);
((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
}
}
}
float Enemy::calcular_escala_actual() const {
auto Enemy::computeCurrentScale() const -> float {
float scale = 1.0F;
// [NEW] Invulnerability LERP prioritza sobre palpitació
if (timer_invulnerabilitat_ > 0.0F) {
// Calculate t: 0.0 at spawn → 1.0 at end
float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
float t = 1.0F - t_inv; // 0.0 → 1.0
// Apply smoothstep: t² * (3 - 2t)
float smooth_t = t * t * (3.0F - 2.0F * t);
// LERP scale from 0.0 to 1.0
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);
scale = START + ((END - START) * SMOOTH_T);
} else if (animacio_.palpitacio_activa) {
// [EXISTING] Palpitació solo cuando no invulnerable
scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase);
}
return scale;
}
// [NEW] Stage system API implementations
float Enemy::get_base_velocity() const {
auto Enemy::getBaseVelocity() const -> float {
switch (type_) {
case EnemyType::PENTAGON:
return Defaults::Enemies::Pentagon::VELOCITAT;
@@ -506,46 +417,34 @@ float Enemy::get_base_velocity() const {
case EnemyType::MOLINILLO:
return Defaults::Enemies::Molinillo::VELOCITAT;
default:
return Defaults::Enemies::Pentagon::VELOCITAT; // Fallback segur
return Defaults::Enemies::Pentagon::VELOCITAT;
}
}
float Enemy::get_base_rotation() const {
// Return the base rotation speed (drotacio_base if available, otherwise current drotacio_)
auto Enemy::getBaseRotation() const -> float {
return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_;
}
void Enemy::set_tracking_strength(float strength) {
// Only applies to QUADRAT type
void Enemy::setTrackingStrength(float strength) {
if (type_ == EnemyType::QUADRAT) {
tracking_strength_ = strength;
}
}
// [NEW] Safe spawn helper - checks if position is away from ship
bool Enemy::intent_spawn_safe(const Vec2& ship_pos, float& out_x, float& out_y) {
// Generate random position within safe bounds
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);
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
int range_x = static_cast<int>(max_x - min_x);
int range_y = static_cast<int>(max_y - min_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));
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));
// Check Euclidean distance to ship
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;
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;
}
+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)
// © 2025 Port a C++20 con SDL3
#pragma once
#include <SDL3/SDL.h>
#include <cmath>
#include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp"
#include "core/types.hpp"
#include "game/constants.hpp"
// Tipo de enemy
enum class EnemyType : uint8_t {
PENTAGON = 0, // Pentágono esquivador (zigzag)
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 {
// Palpitation (breathing effect)
// Palpitación (efecto respiración)
bool palpitacio_activa = false;
float palpitacio_fase = 0.0F; // Phase in cycle (0.0-2π)
float palpitacio_frequencia = 2.0F; // Hz (cycles per second)
float palpitacio_amplitud = 0.15F; // Scale variation (±15%)
float palpitacio_temps_restant = 0.0F; // Time remaining (seconds)
float palpitacio_fase = 0.0F;
float palpitacio_frequencia = 2.0F;
float palpitacio_amplitud = 0.15F;
float palpitacio_temps_restant = 0.0F;
// Rotation acceleration (long-term spin modulation)
float drotacio_base = 0.0F; // Base rotation speed (rad/s)
float drotacio_objetivo = 0.0F; // Target rotation speed (rad/s)
float drotacio_t = 0.0F; // Interpolation progress (0.0-1.0)
float drotacio_duracio = 0.0F; // Duration of transition (seconds)
// Aceleración de rotación visual (modulación a largo plazo)
float drotacio_base = 0.0F;
float drotacio_objetivo = 0.0F;
float drotacio_t = 0.0F;
float drotacio_duracio = 0.0F;
};
class Enemy : public Entities::Entity {
@@ -45,79 +43,78 @@ class Enemy : public Entities::Entity {
void init() override { init(EnemyType::PENTAGON, nullptr); }
void init(EnemyType type, const Vec2* ship_pos = nullptr);
void update(float delta_time) override;
void postUpdate(float delta_time) override;
void draw() const override;
// Override: Interfície d'Entity
[[nodiscard]] bool isActive() const override { return esta_; }
// Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return esta_; }
// Override: Interfície de colisión
[[nodiscard]] float getCollisionRadius() const override {
// Override: Interfaz de colisión
[[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::ENEMY_RADIUS;
}
[[nodiscard]] bool isCollidable() const override {
[[nodiscard]] auto isCollidable() const -> bool override {
return esta_ && timer_invulnerabilitat_ <= 0.0F;
}
// Getters (API pública sin canvis)
void destruir() { esta_ = false; }
[[nodiscard]] float get_drotacio() const { return drotacio_; }
[[nodiscard]] Vec2 getVelocityVector() const {
return {
.x = velocity_ * std::cos(angle_ - (Constants::PI / 2.0F)),
.y = velocity_ * std::sin(angle_ - (Constants::PI / 2.0F))};
}
// Marcar destruido (desactiva el cuerpo físicamente: radius=0)
void destruir();
// Getters
[[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; }
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
// 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)
[[nodiscard]] float get_base_velocity() const;
[[nodiscard]] float get_base_rotation() const;
[[nodiscard]] EnemyType getType() const { return type_; }
// Stage system API (base stats)
[[nodiscard]] auto getBaseVelocity() const -> float;
[[nodiscard]] auto getBaseRotation() const -> float;
[[nodiscard]] auto getType() const -> EnemyType { return type_; }
// [NEW] Setters for difficulty multipliers (stage system)
void set_velocity(float vel) { velocity_ = vel; }
void set_rotation(float rot) {
// Setters para multiplicadores de dificultad (stage system).
// Establecen la velocidad escalar deseada manteniendo la dirección
// actual del body_.velocity.
void setVelocity(float speed);
void setRotation(float rot) {
drotacio_ = rot;
animacio_.drotacio_base = rot;
}
void set_tracking_strength(float strength);
void setTrackingStrength(float strength);
// [NEW] Invulnerability queries
[[nodiscard]] bool isInvulnerable() const { return timer_invulnerabilitat_ > 0.0F; }
[[nodiscard]] float get_temps_invulnerabilitat() const { return timer_invulnerabilitat_; }
// Invulnerabilidad
[[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; }
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
private:
// Membres específics d'Enemy (heretats: renderer_, shape_, center_, angle_, brightness_)
float velocity_;
float drotacio_; // Delta rotación visual (rad/s)
float rotacio_; // Rotación visual acumulada
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_)
float drotacio_; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
float rotacio_; // Rotación visual acumulada (no afecta movimiento)
bool esta_;
// [NEW] Enemy type and configuration
EnemyType type_;
// [NEW] Animation state
EnemyAnimation animacio_;
// [NEW] Behavior state (type-specific)
float tracking_timer_; // For Cuadrado: time since last angle update
const Vec2* ship_position_; // Pointer to ship position (for tracking)
float tracking_strength_; // For Cuadrado: tracking intensity (0.0-1.5), default 0.5
// Comportamiento type-specific
float tracking_timer_; // Quadrat: tiempo desde último update de dirección
const Vec2* ship_position_; // Puntero a posición de la nave (para tracking)
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
float timer_invulnerabilitat_; // Countdown timer (seconds), 0.0f = vulnerable
// Invulnerabilidad post-spawn
float timer_invulnerabilitat_;
// [EXISTING] Private methods
void mou(float delta_time);
// Métodos privados
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
void actualitzar_animacio(float delta_time);
void actualitzar_palpitacio(float delta_time);
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);
// Helper: setear body_.velocity desde un ángulo y magnitud.
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
void setVelocityFromAngle(float angle_movement, float speed);
};
+10 -4
View File
@@ -150,7 +150,7 @@ void GameScene::init() {
stage_manager_->init();
// [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
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_) {
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
}
@@ -224,6 +227,9 @@ void GameScene::update(float delta_time) {
for (auto& ship : ships_) {
ship.postUpdate(delta_time);
}
for (auto& enemy : enemies_) {
enemy.postUpdate(delta_time);
}
// Processar disparos (state-based, no event-based)
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)
enemy.getBrightness(), // Heredar brightness
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)
);
@@ -163,15 +163,15 @@ void SpawnController::aplicar_multiplicadors(Enemy& enemy) const {
}
// Apply velocity multiplier
float base_vel = enemy.get_base_velocity();
enemy.set_velocity(base_vel * config_->multiplicadors.velocity);
float base_vel = enemy.getBaseVelocity();
enemy.setVelocity(base_vel * config_->multiplicadors.velocity);
// Apply rotation multiplier
float base_rot = enemy.get_base_rotation();
enemy.set_rotation(base_rot * config_->multiplicadors.rotation);
float base_rot = enemy.getBaseRotation();
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
// Apply tracking strength (only affects QUADRAT)
enemy.set_tracking_strength(config_->multiplicadors.tracking_strength);
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
}
} // namespace StageSystem
@@ -39,7 +39,7 @@ class SpawnController {
[[nodiscard]] uint8_t get_enemics_spawnejats() const;
// [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:
const StageConfig* config_; // Non-owning pointer to current stage config