feat(enemy): afegir behaviors WANDER/CHASE/FLEE i target multi-ship
This commit is contained in:
@@ -15,10 +15,11 @@ physics:
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
|
||||
behavior:
|
||||
# Square: tracking discret cap a la nau cada N segons.
|
||||
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
|
||||
tracking_interval: 1.0 # segons entre updates d'angle
|
||||
ai:
|
||||
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
|
||||
movement:
|
||||
type: chase
|
||||
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
|
||||
|
||||
animation:
|
||||
pulse:
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/entities/entity.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/entities/enemy_ai.hpp"
|
||||
|
||||
class Ship;
|
||||
|
||||
// Tipo de enemy
|
||||
enum class EnemyType : uint8_t {
|
||||
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
||||
@@ -71,9 +74,10 @@ class Enemy : public Entities::Entity {
|
||||
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
||||
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
||||
|
||||
// Set ship position reference for tracking behavior
|
||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
||||
[[nodiscard]] auto getShipPosition() const -> const Vec2* { return ship_position_; }
|
||||
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||
// nullptr = ship inexistent al match. El sistema d'IA filtra per ship->isActive().
|
||||
void setShips(const Ship* p1, const Ship* p2) { ships_ = {p1, p2}; }
|
||||
[[nodiscard]] auto getShips() const -> const std::array<const Ship*, 2>& { return ships_; }
|
||||
|
||||
// Accessors per al sistema d'IA (Systems::EnemyAi).
|
||||
[[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; }
|
||||
@@ -132,8 +136,8 @@ class Enemy : public Entities::Entity {
|
||||
// Estat per-instància que la primitiva de moviment manté entre frames.
|
||||
EnemyAiState ai_state_;
|
||||
|
||||
// Referència a la posició del ship per a AI de tracking/proximity.
|
||||
const Vec2* ship_position_{nullptr};
|
||||
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||
std::array<const Ship*, 2> ships_{nullptr, nullptr};
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
float invulnerability_timer_{0.0F};
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
// Primitiva de moviment activa per a un enemic. Substitueix el switch
|
||||
// hardcoded sobre EnemyType.
|
||||
enum class MovementType : uint8_t {
|
||||
ZIGZAG, // Canvi de direcció probabilístic (Pentagon/Star)
|
||||
ZIGZAG, // Canvi de direcció probabilístic agressiu (Pentagon/Star)
|
||||
TRACKING, // LERP discret cap al ship cada N segons (Square)
|
||||
RECTILINEAR_PROXIMITY, // Rectilini + boost rotació visual prop del ship (Pinwheel)
|
||||
// Futurs (Fase B):
|
||||
// WANDER, CHASE, FLEE
|
||||
WANDER, // Canvi de direcció probabilístic suau, sense target
|
||||
CHASE, // Steering continu cap al ship més proper
|
||||
FLEE, // Steering continu allunyant-se del ship més proper
|
||||
};
|
||||
|
||||
// Accions que s'executen periòdicament (un timer per acció). Futur (Fase C):
|
||||
@@ -39,7 +40,7 @@ enum class AimMode : uint8_t {
|
||||
struct MovementConfig {
|
||||
MovementType type{MovementType::ZIGZAG};
|
||||
|
||||
// ZIGZAG
|
||||
// ZIGZAG i WANDER (canvi de direcció probabilístic; comparteixen camps).
|
||||
float angle_change_max{0.0F};
|
||||
float zigzag_prob_per_second{0.0F};
|
||||
|
||||
@@ -50,6 +51,11 @@ struct MovementConfig {
|
||||
// RECTILINEAR_PROXIMITY
|
||||
float rotation_proximity_multiplier{0.0F};
|
||||
float proximity_distance{0.0F};
|
||||
|
||||
// CHASE / FLEE: força del steering per segon (LERP velocity ↔ direcció ideal).
|
||||
// 1.0 = en ~1s la velocitat queda totalment realineada cap al target.
|
||||
float chase_strength{0.0F};
|
||||
float flee_strength{0.0F};
|
||||
};
|
||||
|
||||
// Acció periòdica. interval = segons entre disparades; el dispatcher manté un
|
||||
|
||||
@@ -226,6 +226,9 @@ namespace {
|
||||
if (s == "zigzag") { return MovementType::ZIGZAG; }
|
||||
if (s == "tracking") { return MovementType::TRACKING; }
|
||||
if (s == "rectilinear_proximity") { return MovementType::RECTILINEAR_PROXIMITY; }
|
||||
if (s == "wander") { return MovementType::WANDER; }
|
||||
if (s == "chase") { return MovementType::CHASE; }
|
||||
if (s == "flee") { return MovementType::FLEE; }
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -264,6 +267,8 @@ namespace {
|
||||
READ_OPT("tracking_interval", out.tracking_interval);
|
||||
READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier);
|
||||
READ_OPT("proximity_distance", out.proximity_distance);
|
||||
READ_OPT("chase_strength", out.chase_strength);
|
||||
READ_OPT("flee_strength", out.flee_strength);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
// 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.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now)
|
||||
enemy.setShips(ships_.data(), &ships_[1]);
|
||||
physics_world_.addBody(&enemy.getBody());
|
||||
// DON'T call enemy.init() here - stage system handles spawning
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "game/entities/enemy.hpp"
|
||||
#include "game/entities/enemy_ai.hpp"
|
||||
#include "game/entities/enemy_config.hpp"
|
||||
#include "game/entities/ship.hpp"
|
||||
|
||||
namespace Systems::EnemyAi {
|
||||
|
||||
@@ -27,6 +28,28 @@ namespace Systems::EnemyAi {
|
||||
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
|
||||
}
|
||||
|
||||
// Retorna el centre del ship actiu més proper a l'enemic, o nullptr si
|
||||
// no n'hi ha cap viu. Els ships destruïts (is_hit_) i els slots nullptr
|
||||
// (player no participant al match) queden filtrats.
|
||||
auto findNearestShipPosition(const Enemy& enemy) -> const Vec2* {
|
||||
const Vec2& self = enemy.getCenter();
|
||||
const Vec2* best = nullptr;
|
||||
float best_dist_sq = 0.0F;
|
||||
for (const Ship* ship : enemy.getShips()) {
|
||||
if (ship == nullptr || !ship->isActive()) {
|
||||
continue;
|
||||
}
|
||||
const Vec2& pos = ship->getCenter();
|
||||
const Vec2 DELTA = pos - self;
|
||||
const float DIST_SQ = DELTA.lengthSquared();
|
||||
if (best == nullptr || DIST_SQ < best_dist_sq) {
|
||||
best = &pos;
|
||||
best_dist_sq = DIST_SQ;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// ZIGZAG: canvi de direcció probabilístic. Còpia literal del legacy
|
||||
// Enemy::behaviorPentagon.
|
||||
void moveZigzag(Enemy& enemy, float delta_time) {
|
||||
@@ -53,7 +76,7 @@ namespace Systems::EnemyAi {
|
||||
EnemyAiState& state = enemy.getAiState();
|
||||
state.tracking_timer += delta_time;
|
||||
|
||||
const Vec2* ship_pos = enemy.getShipPosition();
|
||||
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||
if (state.tracking_timer < mv.tracking_interval || ship_pos == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -79,12 +102,56 @@ namespace Systems::EnemyAi {
|
||||
enemy.getBody().velocity = new_vel;
|
||||
}
|
||||
|
||||
// CHASE / FLEE comparteixen lògica: steering continu cap a (o lluny de)
|
||||
// la direcció ideal, preservant la magnitud de velocitat. La força és
|
||||
// strength * dt clampejada a 1 (LERP frame-independent simple).
|
||||
void steerTowards(Enemy& enemy, const Vec2& desired_dir, float strength, float delta_time) {
|
||||
const float SPEED = enemy.getBody().velocity.length();
|
||||
if (SPEED <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
const Vec2 DESIRED_VEL = desired_dir * SPEED;
|
||||
const float T = std::min(1.0F, strength * delta_time);
|
||||
Vec2 new_vel = (enemy.getBody().velocity * (1.0F - T)) + (DESIRED_VEL * T);
|
||||
const float NEW_SPEED = new_vel.length();
|
||||
if (NEW_SPEED > 0.0F) {
|
||||
new_vel = new_vel * (SPEED / NEW_SPEED);
|
||||
}
|
||||
enemy.getBody().velocity = new_vel;
|
||||
}
|
||||
|
||||
void moveChase(Enemy& enemy, float delta_time) {
|
||||
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||
if (ship_pos == nullptr) {
|
||||
return;
|
||||
}
|
||||
const Vec2 TO_SHIP = *ship_pos - enemy.getCenter();
|
||||
const float DIST = TO_SHIP.length();
|
||||
if (DIST <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
steerTowards(enemy, TO_SHIP / DIST, enemy.getConfig().ai.movement.chase_strength, delta_time);
|
||||
}
|
||||
|
||||
void moveFlee(Enemy& enemy, float delta_time) {
|
||||
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||
if (ship_pos == nullptr) {
|
||||
return;
|
||||
}
|
||||
const Vec2 AWAY = enemy.getCenter() - *ship_pos;
|
||||
const float DIST = AWAY.length();
|
||||
if (DIST <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
steerTowards(enemy, AWAY / DIST, enemy.getConfig().ai.movement.flee_strength, delta_time);
|
||||
}
|
||||
|
||||
// RECTILINEAR_PROXIMITY: rectilini (cap modificació a velocity); boost
|
||||
// de rotació visual quan distància al ship < proximity_distance. Còpia
|
||||
// literal del legacy Enemy::behaviorPinwheel.
|
||||
void moveRectilinearProximity(Enemy& enemy, float /*delta_time*/) {
|
||||
const auto& mv = enemy.getConfig().ai.movement;
|
||||
const Vec2* ship_pos = enemy.getShipPosition();
|
||||
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||
if (ship_pos == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -106,6 +173,9 @@ namespace Systems::EnemyAi {
|
||||
}
|
||||
switch (enemy.getConfig().ai.movement.type) {
|
||||
case MovementType::ZIGZAG:
|
||||
case MovementType::WANDER:
|
||||
// WANDER reusa la mecànica de canvi de direcció probabilístic;
|
||||
// l'única diferència és semàntica i el tunning dels paràmetres.
|
||||
moveZigzag(enemy, delta_time);
|
||||
break;
|
||||
case MovementType::TRACKING:
|
||||
@@ -114,6 +184,12 @@ namespace Systems::EnemyAi {
|
||||
case MovementType::RECTILINEAR_PROXIMITY:
|
||||
moveRectilinearProximity(enemy, delta_time);
|
||||
break;
|
||||
case MovementType::CHASE:
|
||||
moveChase(enemy, delta_time);
|
||||
break;
|
||||
case MovementType::FLEE:
|
||||
moveFlee(enemy, delta_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user