Compare commits
6 Commits
410955de3c
...
b511740d93
| Author | SHA1 | Date | |
|---|---|---|---|
| b511740d93 | |||
| b0643b6f62 | |||
| 7e8d79222c | |||
| 14295ce859 | |||
| 5ad433e63a | |||
| 61e40e88f4 |
@@ -0,0 +1,19 @@
|
|||||||
|
name: bullet_long
|
||||||
|
|
||||||
|
# Variant de bala més llarga, pensada per a bales d'enemic: més visible per al
|
||||||
|
# jugador i amb prou marge per reaccionar. La velocitat NO viu aquí: es passa
|
||||||
|
# a Bullet::fire() i la decideix qui dispara (l'AiTickAction).
|
||||||
|
shape:
|
||||||
|
path: bullet_long.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 0.5
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 0.5
|
||||||
|
restitution: 0.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
impact_momentum_factor: 3.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 100, 100] # roig clar — diferencia visualment del verd laser del player
|
||||||
@@ -15,10 +15,11 @@ physics:
|
|||||||
linear_damping: 0.0
|
linear_damping: 0.0
|
||||||
angular_damping: 0.0
|
angular_damping: 0.0
|
||||||
|
|
||||||
behavior:
|
ai:
|
||||||
# Square: tracking discret cap a la nau cada N segons.
|
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
|
||||||
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
|
movement:
|
||||||
tracking_interval: 1.0 # segons entre updates d'angle
|
type: chase
|
||||||
|
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
|
||||||
|
|
||||||
animation:
|
animation:
|
||||||
pulse:
|
pulse:
|
||||||
|
|||||||
@@ -15,10 +15,20 @@ physics:
|
|||||||
linear_damping: 0.0
|
linear_damping: 0.0
|
||||||
angular_damping: 0.0
|
angular_damping: 0.0
|
||||||
|
|
||||||
behavior:
|
ai:
|
||||||
# Hereta el comportament de Pentagon (zigzag esquivador).
|
# Movement: zigzag esquivador (com Pentagon).
|
||||||
angle_change_max: 1.0
|
movement:
|
||||||
zigzag_prob_per_second: 0.8
|
type: zigzag
|
||||||
|
angle_change_max: 1.0
|
||||||
|
zigzag_prob_per_second: 0.8
|
||||||
|
# Accions periòdiques: cada ~2.5s dispara una bala apuntada al ship més proper.
|
||||||
|
tick:
|
||||||
|
- action: shoot
|
||||||
|
interval: 2.5
|
||||||
|
aim_mode: aimed # apunta al ship més proper (atan2)
|
||||||
|
jitter_rad: 0.0 # sense soroll: tret perfecte
|
||||||
|
bullet: bullet_long # variant més visible per al jugador
|
||||||
|
bullet_speed: 150.0 # px/s — prou lenta per reaccionar
|
||||||
|
|
||||||
animation:
|
animation:
|
||||||
pulse:
|
pulse:
|
||||||
|
|||||||
+20
-16
@@ -1,28 +1,32 @@
|
|||||||
# bullet_long.shp - Bala allargada (dos octàgons tangents + tapes superior i inferior)
|
# bullet_long.shp - Bala allargada vertical (dos mig-octàgons + dos costats)
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
#
|
#
|
||||||
# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units
|
# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle
|
||||||
# per una línia horitzontal superior i una d'inferior. La silueta resultant
|
# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol.
|
||||||
# és una càpsula amb la separació visible dels dos cercles al centre.
|
# Es dibuixen només els segments exteriors per evitar veure la unió interna
|
||||||
|
# dels dos cercles; el resultat visual són dos "mig-octàgons" separats per
|
||||||
|
# un petit gap al centre, units pels dos costats verticals.
|
||||||
#
|
#
|
||||||
# Geometria:
|
# Geometria:
|
||||||
# Centre octàgon esquerre: (-3, 0)
|
# Mig-octàgon superior (radi 3) centrat a (0, -3)
|
||||||
# Centre octàgon dret: ( 3, 0)
|
# Mig-octàgon inferior (radi 3) centrat a (0, 3)
|
||||||
# Punt de tangència: ( 0, 0)
|
# Punt extrem superior: (0, -6)
|
||||||
# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6).
|
# Punt extrem inferior: (0, 6)
|
||||||
|
# Bounding radius natiu = 6 (extrem vertical a y=±6).
|
||||||
|
# collision_factor al YAML compensa el bounding doble (0.5 → hitbox ≈ 3).
|
||||||
|
|
||||||
name: bullet_long
|
name: bullet_long
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
# Octàgon esquerre (centre x=-3, radi 3)
|
# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre)
|
||||||
polyline: -3,-3 -0.88,-2.12 0,0 -0.88,2.12 -3,3 -5.12,2.12 -6,0 -5.12,-2.12 -3,-3
|
polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3
|
||||||
|
|
||||||
# Octàgon dret (centre x=3, radi 3)
|
# Mig-octàgon inferior
|
||||||
polyline: 3,-3 5.12,-2.12 6,0 5.12,2.12 3,3 0.88,2.12 0,0 0.88,-2.12 3,-3
|
polyline: 3,3 2.12,5.12 0,6 -2.12,5.12 -3,3
|
||||||
|
|
||||||
# Tapa superior: uneix el cim de l'octàgon esquerre amb el del dret
|
# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior)
|
||||||
polyline: -3,-3 3,-3
|
polyline: 3,-3 3,3
|
||||||
|
|
||||||
# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret
|
# Costat esquerre
|
||||||
polyline: -3,3 3,3
|
polyline: -3,-3 -3,3
|
||||||
|
|||||||
@@ -3,10 +3,24 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Defaults::Entities {
|
namespace Defaults::Entities {
|
||||||
|
|
||||||
constexpr int MAX_ORNIS = 15;
|
constexpr int MAX_ORNIS = 15;
|
||||||
constexpr int MAX_BULLETS = 50;
|
constexpr int MAX_BULLETS = 50; // per jugador (P1 + P2 = 2× aquest valor)
|
||||||
|
constexpr int MAX_ENEMY_BULLETS = 50; // pool reservat per a bales d'enemic
|
||||||
|
|
||||||
|
// Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
|
||||||
|
// Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
|
||||||
|
constexpr int MAX_BULLETS_TOTAL = (MAX_BULLETS * 2) + MAX_ENEMY_BULLETS;
|
||||||
|
constexpr int ENEMY_BULLET_START_IDX = MAX_BULLETS * 2;
|
||||||
|
|
||||||
|
// Convenció d'owner_id per a Bullet::fire:
|
||||||
|
// 0..1 = players (P1, P2)
|
||||||
|
// ENEMY_OWNER_BASE + index = enemic concret (per identificar el seu autoimpacte)
|
||||||
|
// Una bala mata a qualsevol col·lisió excepte amb el seu propi creador.
|
||||||
|
constexpr uint8_t ENEMY_OWNER_BASE = 128;
|
||||||
|
|
||||||
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
||||||
// calcula el seu collision_radius com a
|
// calcula el seu collision_radius com a
|
||||||
|
|||||||
@@ -54,10 +54,29 @@ void Bullet::init() {
|
|||||||
body_.clearAccumulators();
|
body_.clearAccumulators();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) {
|
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg) {
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
owner_id_ = owner_id;
|
owner_id_ = owner_id;
|
||||||
|
|
||||||
|
// Si no es passa cfg, restaurem al config per defecte (BulletRegistry::get):
|
||||||
|
// els slots són reutilitzables i una bala que abans ha estat disparada amb
|
||||||
|
// una variant (p.ex. bullet_long d'enemic) ha de tornar al bullet.shp del
|
||||||
|
// player quan aquest la reutilitza.
|
||||||
|
const BulletConfig* effective = (cfg != nullptr) ? cfg : &BulletRegistry::get();
|
||||||
|
if (effective != config_) {
|
||||||
|
config_ = effective;
|
||||||
|
shape_ = Graphics::ShapeLoader::load(config_->shape.path);
|
||||||
|
if (!shape_ || !shape_->isValid()) {
|
||||||
|
std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
|
||||||
|
}
|
||||||
|
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
|
||||||
|
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
|
||||||
|
body_.setMass(config_->physics.mass);
|
||||||
|
body_.restitution = config_->physics.restitution;
|
||||||
|
body_.linear_damping = config_->physics.linear_damping;
|
||||||
|
body_.angular_damping = config_->physics.angular_damping;
|
||||||
|
}
|
||||||
|
|
||||||
center_ = position;
|
center_ = position;
|
||||||
prev_position_ = position; // spawn: swept degenera a punt-cercle
|
prev_position_ = position; // spawn: swept degenera a punt-cercle
|
||||||
angle_ = angle;
|
angle_ = angle;
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ class Bullet : public Entities::Entity {
|
|||||||
explicit Bullet(Rendering::Renderer* renderer);
|
explicit Bullet(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
void init() override;
|
void init() override;
|
||||||
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed);
|
// cfg = nullptr → manté el config actual (per defecte: BulletRegistry::get()).
|
||||||
|
// cfg != nullptr → substitueix el config per a aquesta bala (recarrega
|
||||||
|
// shape, recalcula collision_radius_, mass, etc.). Útil per a bales
|
||||||
|
// d'enemic amb shape pròpia.
|
||||||
|
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg = nullptr);
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
void postUpdate(float delta_time) override;
|
void postUpdate(float delta_time) override;
|
||||||
void draw() const override;
|
void draw() const override;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// bullet_registry.cpp - Implementació del registre de bala
|
// bullet_registry.cpp - Implementació del registre de bales
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#include "game/entities/bullet_registry.hpp"
|
#include "game/entities/bullet_registry.hpp"
|
||||||
@@ -8,23 +8,34 @@
|
|||||||
|
|
||||||
#include "core/entities/entity_loader.hpp"
|
#include "core/entities/entity_loader.hpp"
|
||||||
|
|
||||||
BulletConfig BulletRegistry::config;
|
std::unordered_map<std::string, BulletConfig> BulletRegistry::configs;
|
||||||
bool BulletRegistry::loaded = false;
|
bool BulletRegistry::loaded = false;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Tria comú: carrega el YAML d'un name, parseja a BulletConfig i el guarda
|
||||||
|
// al map. Retorna punter al config inserit, o nullptr si falla.
|
||||||
|
auto loadInto(std::unordered_map<std::string, BulletConfig>& configs, const std::string& name) -> const BulletConfig* {
|
||||||
|
auto yaml = Entities::EntityLoader::load(name);
|
||||||
|
if (!yaml) {
|
||||||
|
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto cfg = BulletConfig::fromYaml(*yaml);
|
||||||
|
if (!cfg) {
|
||||||
|
std::cerr << "[BulletRegistry] Error: format invàlid a " << name << ".yaml\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto [it, _] = configs.insert_or_assign(name, *cfg);
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
auto BulletRegistry::load() -> bool {
|
auto BulletRegistry::load() -> bool {
|
||||||
auto yaml = Entities::EntityLoader::load("bullet");
|
if (loadInto(configs, "bullet") == nullptr) {
|
||||||
if (!yaml) {
|
|
||||||
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar bullet.yaml\n";
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto cfg = BulletConfig::fromYaml(*yaml);
|
|
||||||
if (!cfg) {
|
|
||||||
std::cerr << "[BulletRegistry] Error: format invàlid a bullet.yaml\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
config = *cfg;
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
std::cout << "[BulletRegistry] Configuració de bala carregada.\n";
|
std::cout << "[BulletRegistry] Configuració de bala 'bullet' carregada.\n";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,5 +44,13 @@ auto BulletRegistry::get() -> const BulletConfig& {
|
|||||||
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
|
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
|
||||||
std::exit(EXIT_FAILURE);
|
std::exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
return config;
|
return configs.at("bullet");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto BulletRegistry::get(const std::string& name) -> const BulletConfig* {
|
||||||
|
auto it = configs.find(name);
|
||||||
|
if (it != configs.end()) {
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
return loadInto(configs, name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
// bullet_registry.hpp - Registre estàtic de la configuració de la bala
|
// bullet_registry.hpp - Registre estàtic de configuracions de bala
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
//
|
//
|
||||||
// Una única instància per a tota la sessió. Es manté el patró registry
|
// Diverses configuracions per nom (data/entities/<name>/<name>.yaml). El
|
||||||
// (paral·lel a EnemyRegistry) tot i ser una sola entitat: si el dia de demà
|
// config "bullet" es manté com a default històric (player) i es carrega via
|
||||||
// hi ha més tipus de bala (laser/plasma/etc.) només cal estendre-ho.
|
// load(). Els altres (ex. "bullet_long" per a bales d'enemic) es carreguen
|
||||||
|
// peresosament la primera vegada que algú els demana per nom.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "game/entities/bullet_config.hpp"
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
|
||||||
class BulletRegistry {
|
class BulletRegistry {
|
||||||
public:
|
public:
|
||||||
BulletRegistry() = delete;
|
BulletRegistry() = delete;
|
||||||
|
|
||||||
// Carrega data/entities/bullet/bullet.yaml. Retorna false si falla.
|
// Carrega data/entities/bullet/bullet.yaml com a default. Retorna false si falla.
|
||||||
static auto load() -> bool;
|
static auto load() -> bool;
|
||||||
|
|
||||||
// Accés a la configuració. Avorta amb log fatal si load() no s'ha cridat
|
// Accés a la configuració per defecte ("bullet"). Avorta amb log fatal si
|
||||||
// o ha fallat.
|
// load() no s'ha cridat o ha fallat. Mantingut per a backwards compat del Bullet ctor.
|
||||||
static auto get() -> const BulletConfig&;
|
static auto get() -> const BulletConfig&;
|
||||||
|
|
||||||
|
// Accés a una configuració per nom. Lazy-load: si no està al map, intenta
|
||||||
|
// carregar data/entities/<name>/<name>.yaml. Retorna nullptr si no es pot.
|
||||||
|
static auto get(const std::string& name) -> const BulletConfig*;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static BulletConfig config;
|
static std::unordered_map<std::string, BulletConfig> configs;
|
||||||
static bool loaded;
|
static bool loaded;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,14 +28,6 @@ namespace {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random float [0..1).
|
// Random float [0..1).
|
||||||
auto randFloat01() -> float {
|
auto randFloat01() -> float {
|
||||||
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||||
@@ -62,9 +54,14 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
|||||||
config_ = &EnemyRegistry::get(type);
|
config_ = &EnemyRegistry::get(type);
|
||||||
const EnemyConfig& cfg = *config_;
|
const EnemyConfig& cfg = *config_;
|
||||||
|
|
||||||
if (type_ == EnemyType::SQUARE) {
|
ai_state_ = EnemyAiState{};
|
||||||
tracking_timer_ = 0.0F;
|
ai_state_.tracking_strength = cfg.ai.movement.tracking_strength;
|
||||||
tracking_strength_ = cfg.behavior.tracking_strength;
|
|
||||||
|
// Timers paral·lels a tick: random [0, interval) per evitar que tots els
|
||||||
|
// enemics del mateix tipus disparin sincronitzats al spawn.
|
||||||
|
ai_tick_timers_.resize(cfg.ai.tick.size());
|
||||||
|
for (std::size_t i = 0; i < cfg.ai.tick.size(); ++i) {
|
||||||
|
ai_tick_timers_[i] = randFloat01() * cfg.ai.tick[i].interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
||||||
@@ -136,8 +133,6 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
|||||||
invulnerability_timer_ = cfg.spawn.invulnerability_duration;
|
invulnerability_timer_ = cfg.spawn.invulnerability_duration;
|
||||||
brightness_ = cfg.spawn.invulnerability_brightness_start;
|
brightness_ = cfg.spawn.invulnerability_brightness_start;
|
||||||
|
|
||||||
direction_change_timer_ = 0.0F;
|
|
||||||
|
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,22 +162,9 @@ void Enemy::update(float delta_time) {
|
|||||||
brightness_ = START + ((END - START) * SMOOTH_T);
|
brightness_ = START + ((END - START) * SMOOTH_T);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWounded()) {
|
// El moviment es delega a Systems::EnemyAi::tick, invocat des de l'scene
|
||||||
switch (type_) {
|
// ABANS d'aquest update (manté l'ordre: AI escriu velocity/rotation_delta,
|
||||||
case EnemyType::PENTAGON:
|
// després animation pot modular rotation_delta via rotation_accel).
|
||||||
case EnemyType::STAR:
|
|
||||||
// STAR reusa el zigzag esquivador de Pentagon. Si en el futur
|
|
||||||
// vol comportament propi, separa-li el cas.
|
|
||||||
behaviorPentagon(delta_time);
|
|
||||||
break;
|
|
||||||
case EnemyType::SQUARE:
|
|
||||||
behaviorSquare(delta_time);
|
|
||||||
break;
|
|
||||||
case EnemyType::PINWHEEL:
|
|
||||||
behaviorPinwheel(delta_time);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAnimation(delta_time);
|
updateAnimation(delta_time);
|
||||||
|
|
||||||
@@ -246,59 +228,6 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
|
|||||||
body_.velocity = angleToDirection(angle_movement) * speed;
|
body_.velocity = angleToDirection(angle_movement) * speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics)
|
|
||||||
// en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld.
|
|
||||||
void Enemy::behaviorPentagon(float delta_time) {
|
|
||||||
direction_change_timer_ += delta_time;
|
|
||||||
|
|
||||||
if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) {
|
|
||||||
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
|
||||||
const float DELTA = randFloat01() * 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 discret cap a la nau cada N segons.
|
|
||||||
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: movement rectilini + boost de rotació visual prop 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) {
|
void Enemy::updateAnimation(float delta_time) {
|
||||||
updatePulse(delta_time);
|
updatePulse(delta_time);
|
||||||
updateRotationAcceleration(delta_time);
|
updateRotationAcceleration(delta_time);
|
||||||
@@ -373,7 +302,7 @@ auto Enemy::getBaseRotation() const -> float {
|
|||||||
|
|
||||||
void Enemy::setTrackingStrength(float strength) {
|
void Enemy::setTrackingStrength(float strength) {
|
||||||
if (type_ == EnemyType::SQUARE) {
|
if (type_ == EnemyType::SQUARE) {
|
||||||
tracking_strength_ = strength;
|
ai_state_.tracking_strength = strength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "core/entities/entity.hpp"
|
#include "core/entities/entity.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
|
|
||||||
|
class Ship;
|
||||||
|
|
||||||
// Tipo de enemy
|
// Tipo de enemy
|
||||||
enum class EnemyType : uint8_t {
|
enum class EnemyType : uint8_t {
|
||||||
@@ -70,8 +75,18 @@ class Enemy : public Entities::Entity {
|
|||||||
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
||||||
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
||||||
|
|
||||||
// Set ship position reference for tracking behavior
|
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
// 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_; }
|
||||||
|
[[nodiscard]] auto getAiTickTimers() -> std::vector<float>& { return ai_tick_timers_; }
|
||||||
|
[[nodiscard]] auto getRotationBase() const -> float { return animation_.rotation_delta_base; }
|
||||||
|
void setRotationDelta(float rot) { rotation_delta_ = rot; }
|
||||||
|
// Public: el sistema d'IA reorienta la velocitat des d'un angle.
|
||||||
|
void setVelocityFromAngle(float angle_movement, float speed);
|
||||||
|
|
||||||
// Stage system API (base stats)
|
// Stage system API (base stats)
|
||||||
[[nodiscard]] auto getBaseVelocity() const -> float;
|
[[nodiscard]] auto getBaseVelocity() const -> float;
|
||||||
@@ -120,11 +135,16 @@ class Enemy : public Entities::Entity {
|
|||||||
EnemyType type_{EnemyType::PENTAGON};
|
EnemyType type_{EnemyType::PENTAGON};
|
||||||
EnemyAnimation animation_;
|
EnemyAnimation animation_;
|
||||||
|
|
||||||
// Comportamiento type-specific
|
// Estat per-instància que la primitiva de moviment manté entre frames.
|
||||||
float tracking_timer_{0.0F};
|
EnemyAiState ai_state_;
|
||||||
const Vec2* ship_position_{nullptr};
|
|
||||||
float tracking_strength_{0.0F};
|
// Timers paral·lels a config_->ai.tick: timers_[i] és el temps restant
|
||||||
float direction_change_timer_{0.0F};
|
// (en segons) fins a la pròxima execució de l'acció i. Re-dimensionat a
|
||||||
|
// init() segons la mida de config_->ai.tick.
|
||||||
|
std::vector<float> ai_tick_timers_;
|
||||||
|
|
||||||
|
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||||
|
std::array<const Ship*, 2> ships_{nullptr, nullptr};
|
||||||
|
|
||||||
// Invulnerabilidad post-spawn
|
// Invulnerabilidad post-spawn
|
||||||
float invulnerability_timer_{0.0F};
|
float invulnerability_timer_{0.0F};
|
||||||
@@ -138,14 +158,7 @@ class Enemy : public Entities::Entity {
|
|||||||
void updateAnimation(float delta_time);
|
void updateAnimation(float delta_time);
|
||||||
void updatePulse(float delta_time);
|
void updatePulse(float delta_time);
|
||||||
void updateRotationAcceleration(float delta_time);
|
void updateRotationAcceleration(float delta_time);
|
||||||
void behaviorPentagon(float delta_time);
|
|
||||||
void behaviorSquare(float delta_time);
|
|
||||||
void behaviorPinwheel(float delta_time);
|
|
||||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||||
// Static: passa els paràmetres com a args per no acoblar a *this.
|
// Static: passa els paràmetres com a args per no acoblar a *this.
|
||||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
|
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
|
||||||
|
|
||||||
// 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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// enemy_ai.hpp - Sistema declaratiu d'IA per a enemics
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Cada enemic declara al seu YAML quin movement primitiu fa servir i, opcional-
|
||||||
|
// ment, una llista d'accions periòdiques (tick). El motor només dispatcha; el
|
||||||
|
// comportament viu a les dades. Patró paral·lel al d'events declaratius
|
||||||
|
// (enemy_event.hpp).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// 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 agressiu (Pentagon/Star)
|
||||||
|
TRACKING, // LERP discret cap al ship cada N segons (Square)
|
||||||
|
RECTILINEAR_PROXIMITY, // Rectilini + boost rotació visual prop del ship (Pinwheel)
|
||||||
|
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):
|
||||||
|
// SHOOT amb aim_mode/jitter/bullet config.
|
||||||
|
enum class AiActionType : uint8_t {
|
||||||
|
SHOOT,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AimMode : uint8_t {
|
||||||
|
RANDOM, // Angle uniformement aleatori
|
||||||
|
AIMED, // atan2(nearest_ship - center) + soroll gaussià (jitter_rad)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Camps de tots els movements; només el subset rellevant per al type actiu
|
||||||
|
// s'usa. Els altres queden a 0.0F. Mateixa filosofia que la BehaviorCfg
|
||||||
|
// llegacy però amb el type explícit dins.
|
||||||
|
struct MovementConfig {
|
||||||
|
MovementType type{MovementType::ZIGZAG};
|
||||||
|
|
||||||
|
// ZIGZAG i WANDER (canvi de direcció probabilístic; comparteixen camps).
|
||||||
|
float angle_change_max{0.0F};
|
||||||
|
float zigzag_prob_per_second{0.0F};
|
||||||
|
|
||||||
|
// TRACKING
|
||||||
|
float tracking_strength{0.0F};
|
||||||
|
float tracking_interval{0.0F};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// timer per acció (paral·lel a aquesta llista) i dispara quan arriba a 0.
|
||||||
|
struct AiTickAction {
|
||||||
|
AiActionType type{AiActionType::SHOOT};
|
||||||
|
float interval{1.0F};
|
||||||
|
AimMode aim_mode{AimMode::RANDOM};
|
||||||
|
float jitter_rad{0.0F};
|
||||||
|
float bullet_speed{200.0F}; // px/s; la magnitud la decideix l'enemic, no el bullet config
|
||||||
|
std::string bullet_config_name; // bullet config a usar (lazy-load des de BulletRegistry)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnemyAiConfig {
|
||||||
|
MovementConfig movement;
|
||||||
|
std::vector<AiTickAction> tick;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estat per-instància que la primitiva de moviment manté entre frames (timers
|
||||||
|
// d'interval, contadors de canvi de direcció...). Es viu dins de Enemy i el
|
||||||
|
// sistema d'IA hi escriu via getAiState().
|
||||||
|
struct EnemyAiState {
|
||||||
|
float direction_change_timer{0.0F}; // ZIGZAG
|
||||||
|
float tracking_timer{0.0F}; // TRACKING
|
||||||
|
float tracking_strength{0.0F}; // TRACKING (cau de cfg, mutable per dificultat)
|
||||||
|
};
|
||||||
@@ -222,6 +222,146 @@ namespace {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto movementTypeFromString(const std::string& s) -> std::optional<MovementType> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto aiActionTypeFromString(const std::string& s) -> std::optional<AiActionType> {
|
||||||
|
if (s == "shoot") { return AiActionType::SHOOT; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto aimModeFromString(const std::string& s) -> std::optional<AimMode> {
|
||||||
|
if (s == "random") { return AimMode::RANDOM; }
|
||||||
|
if (s == "aimed") { return AimMode::AIMED; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseMovement(const fkyaml::node& mv_node, const std::string& enemy_name, MovementConfig& out) -> bool {
|
||||||
|
if (!mv_node.contains("type")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: falta 'ai.movement.type' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto TYPE_STR = mv_node["type"].get_value<std::string>();
|
||||||
|
const auto PARSED = movementTypeFromString(TYPE_STR);
|
||||||
|
if (!PARSED) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: movement type desconegut '" << TYPE_STR
|
||||||
|
<< "' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.type = *PARSED;
|
||||||
|
const auto READ_OPT = [&mv_node](const char* key, float& dst) {
|
||||||
|
if (mv_node.contains(key)) {
|
||||||
|
dst = mv_node[key].get_value<float>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
READ_OPT("angle_change_max", out.angle_change_max);
|
||||||
|
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
|
||||||
|
READ_OPT("tracking_strength", out.tracking_strength);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseTickList(const fkyaml::node& list_node, const std::string& enemy_name, std::vector<AiTickAction>& out) -> bool {
|
||||||
|
if (!list_node.is_sequence()) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: 'ai.tick' ha de ser una llista a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto& item : list_node) {
|
||||||
|
if (!item.contains("action")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: entrada sense 'action' a ai.tick ("
|
||||||
|
<< enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto STR = item["action"].get_value<std::string>();
|
||||||
|
const auto PARSED = aiActionTypeFromString(STR);
|
||||||
|
if (!PARSED) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: acció d'ai desconeguda '" << STR
|
||||||
|
<< "' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AiTickAction action;
|
||||||
|
action.type = *PARSED;
|
||||||
|
if (item.contains("interval")) {
|
||||||
|
action.interval = item["interval"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("aim_mode")) {
|
||||||
|
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
|
||||||
|
const auto AIM = aimModeFromString(AIM_STR);
|
||||||
|
if (!AIM) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR
|
||||||
|
<< "' a ai.tick (" << enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
action.aim_mode = *AIM;
|
||||||
|
}
|
||||||
|
if (item.contains("jitter_rad")) {
|
||||||
|
action.jitter_rad = item["jitter_rad"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet_speed")) {
|
||||||
|
action.bullet_speed = item["bullet_speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet")) {
|
||||||
|
action.bullet_config_name = item["bullet"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
out.push_back(action);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migració progressiva: si el YAML no porta secció `ai:`, derivem el
|
||||||
|
// movement a partir de l'ai_type i copiem els paràmetres de la BehaviorCfg
|
||||||
|
// ja parsejada. Comportament idèntic al hardcoded actual.
|
||||||
|
void fillLegacyAiDefaults(EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) {
|
||||||
|
switch (ai_type) {
|
||||||
|
case EnemyType::PENTAGON:
|
||||||
|
case EnemyType::STAR:
|
||||||
|
out.movement.type = MovementType::ZIGZAG;
|
||||||
|
out.movement.angle_change_max = legacy.angle_change_max;
|
||||||
|
out.movement.zigzag_prob_per_second = legacy.zigzag_prob_per_second;
|
||||||
|
break;
|
||||||
|
case EnemyType::SQUARE:
|
||||||
|
out.movement.type = MovementType::TRACKING;
|
||||||
|
out.movement.tracking_strength = legacy.tracking_strength;
|
||||||
|
out.movement.tracking_interval = legacy.tracking_interval;
|
||||||
|
break;
|
||||||
|
case EnemyType::PINWHEEL:
|
||||||
|
out.movement.type = MovementType::RECTILINEAR_PROXIMITY;
|
||||||
|
out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier;
|
||||||
|
out.movement.proximity_distance = legacy.proximity_distance;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseAi(const fkyaml::node& node, const std::string& name, EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) -> bool {
|
||||||
|
if (!node.contains("ai")) {
|
||||||
|
fillLegacyAiDefaults(ai_type, legacy, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto& ai = node["ai"];
|
||||||
|
if (!ai.contains("movement")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: falta 'ai.movement' a " << name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseMovement(ai["movement"], name, out.movement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ai.contains("tick") && !parseTickList(ai["tick"], name, out.tick)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
|
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
|
||||||
if (!node.contains("events")) {
|
if (!node.contains("events")) {
|
||||||
fillLegacyDefaults(out);
|
fillLegacyDefaults(out);
|
||||||
@@ -268,6 +408,7 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
|||||||
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
||||||
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
|
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
|
||||||
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
|
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
|
||||||
|
if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; }
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/enemy.hpp" // EnemyType
|
#include "game/entities/enemy.hpp" // EnemyType
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
#include "game/entities/enemy_event.hpp"
|
#include "game/entities/enemy_event.hpp"
|
||||||
|
|
||||||
struct EnemyConfig {
|
struct EnemyConfig {
|
||||||
@@ -100,6 +101,7 @@ struct EnemyConfig {
|
|||||||
ColorsCfg colors;
|
ColorsCfg colors;
|
||||||
int score;
|
int score;
|
||||||
EnemyEventConfig events;
|
EnemyEventConfig events;
|
||||||
|
EnemyAiConfig ai;
|
||||||
|
|
||||||
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
|
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
|
||||||
// YAML coincideix amb el tipus que el caller espera (segons el directori).
|
// YAML coincideix amb el tipus que el caller espera (segons el directori).
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "game/stage_system/stage_loader.hpp"
|
#include "game/stage_system/stage_loader.hpp"
|
||||||
#include "game/systems/collision_system.hpp"
|
#include "game/systems/collision_system.hpp"
|
||||||
#include "game/systems/continue_system.hpp"
|
#include "game/systems/continue_system.hpp"
|
||||||
|
#include "game/systems/enemy_ai_system.hpp"
|
||||||
#include "game/systems/init_hud_animator.hpp"
|
#include "game/systems/init_hud_animator.hpp"
|
||||||
|
|
||||||
// Using declarations per simplificar el codi
|
// Using declarations per simplificar el codi
|
||||||
@@ -185,7 +186,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
// Registramos el body al world incluso inactivo: con radius=0 no colisiona
|
// 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.
|
// ni se mueve, y al init() del stage system se activa sin re-registrar.
|
||||||
for (auto& enemy : enemies_) {
|
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());
|
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
|
||||||
}
|
}
|
||||||
@@ -353,6 +354,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool {
|
|||||||
|
|
||||||
// Enemies, bullets y efectos siguen moviéndose en background.
|
// Enemies, bullets y efectos siguen moviéndose en background.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -379,6 +381,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
|
|||||||
|
|
||||||
// Enemies, bullets y efectos siguen moviéndose como fondo.
|
// Enemies, bullets y efectos siguen moviéndose como fondo.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -428,6 +431,7 @@ void GameScene::stepDeathSequence(float delta_time) {
|
|||||||
// aunque otros jugadores aún jueguen.
|
// aunque otros jugadores aún jueguen.
|
||||||
if (algun_mort) {
|
if (algun_mort) {
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -527,8 +531,10 @@ void GameScene::runStagePlaying(float delta_time) {
|
|||||||
ships_[i].update(delta_time);
|
ships_[i].update(delta_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto& enemy : enemies_) {
|
auto ai_ctx = buildCollisionContext();
|
||||||
enemy.update(delta_time);
|
for (std::size_t i = 0; i < enemies_.size(); ++i) {
|
||||||
|
Systems::EnemyAi::tick(ai_ctx, enemies_[i], i, delta_time);
|
||||||
|
enemies_[i].update(delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
||||||
@@ -562,8 +568,8 @@ void GameScene::runStageLevelCompleted(float delta_time) {
|
|||||||
floating_score_manager_.update(delta_time);
|
floating_score_manager_.update(delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::runCollisionDetections() {
|
auto GameScene::buildCollisionContext() -> Systems::Collision::Context {
|
||||||
Systems::Collision::Context col_ctx{
|
return Systems::Collision::Context{
|
||||||
.ships = ships_,
|
.ships = ships_,
|
||||||
.enemies = enemies_,
|
.enemies = enemies_,
|
||||||
.bullets = bullets_,
|
.bullets = bullets_,
|
||||||
@@ -576,6 +582,10 @@ void GameScene::runCollisionDetections() {
|
|||||||
.match_config = match_config_,
|
.match_config = match_config_,
|
||||||
.on_player_hit = [this](uint8_t pid) { tocado(pid); },
|
.on_player_hit = [this](uint8_t pid) { tocado(pid); },
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::runCollisionDetections() {
|
||||||
|
auto col_ctx = buildCollisionContext();
|
||||||
Systems::Collision::detectAll(col_ctx);
|
Systems::Collision::detectAll(col_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "game/entities/ship.hpp"
|
#include "game/entities/ship.hpp"
|
||||||
#include "game/stage_system/stage_config.hpp"
|
#include "game/stage_system/stage_config.hpp"
|
||||||
#include "game/stage_system/stage_manager.hpp"
|
#include "game/stage_system/stage_manager.hpp"
|
||||||
|
#include "game/systems/collision_system.hpp"
|
||||||
|
|
||||||
// Game over state machine
|
// Game over state machine
|
||||||
enum class GameOverState : uint8_t {
|
enum class GameOverState : uint8_t {
|
||||||
@@ -67,7 +68,7 @@ class GameScene final : public Scene {
|
|||||||
std::array<Enemy, Constants::MAX_ORNIS> enemies_;
|
std::array<Enemy, Constants::MAX_ORNIS> enemies_;
|
||||||
// 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la
|
// 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la
|
||||||
// widening conversion implícita que detecta clang-tidy.
|
// widening conversion implícita que detecta clang-tidy.
|
||||||
std::array<Bullet, static_cast<std::size_t>(Constants::MAX_BULLETS) * 2> bullets_;
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)> bullets_;
|
||||||
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
||||||
|
|
||||||
// Lives and game over system
|
// Lives and game over system
|
||||||
@@ -144,6 +145,10 @@ class GameScene final : public Scene {
|
|||||||
void runStageLevelStart(float delta_time);
|
void runStageLevelStart(float delta_time);
|
||||||
void runStagePlaying(float delta_time);
|
void runStagePlaying(float delta_time);
|
||||||
void runStageLevelCompleted(float delta_time);
|
void runStageLevelCompleted(float delta_time);
|
||||||
|
// Construeix el Collision::Context del frame actual (referències a tots els
|
||||||
|
// pools/managers + on_player_hit). Reutilitzat tant per al tick d'IA com
|
||||||
|
// per a runCollisionDetections.
|
||||||
|
[[nodiscard]] auto buildCollisionContext() -> Systems::Collision::Context;
|
||||||
// Helper: ejecuta colisiones de gameplay con el Context preparado.
|
// Helper: ejecuta colisiones de gameplay con el Context preparado.
|
||||||
void runCollisionDetections();
|
void runCollisionDetections();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,20 +41,32 @@ namespace Systems::Collision {
|
|||||||
|
|
||||||
void detectBulletEnemy(Context& ctx) {
|
void detectBulletEnemy(Context& ctx) {
|
||||||
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
|
||||||
|
constexpr uint8_t NO_SHOOTER = 0xFF;
|
||||||
|
constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE;
|
||||||
|
|
||||||
for (auto& bullet : ctx.bullets) {
|
for (auto& bullet : ctx.bullets) {
|
||||||
if (!bullet.isActive()) {
|
if (!bullet.isActive()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (auto& enemy : ctx.enemies) {
|
const uint8_t BULLET_OWNER = bullet.getOwnerId();
|
||||||
|
const bool IS_ENEMY_BULLET = BULLET_OWNER >= ENEMY_BASE;
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < ctx.enemies.size(); ++i) {
|
||||||
|
Enemy& enemy = ctx.enemies[i];
|
||||||
|
// Self-shot: una bala d'enemic mai no impacta el seu propi creador.
|
||||||
|
if (IS_ENEMY_BULLET && BULLET_OWNER == static_cast<uint8_t>(ENEMY_BASE + i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) {
|
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// *** COLISIÓN bullet → enemy ***
|
// *** COLISIÓN bullet → enemy ***
|
||||||
// La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
|
// La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
|
||||||
// al YAML de l'enemic via la secció `events:`. Aquí només dispatchem.
|
// al YAML de l'enemic via la secció `events:`. Si la bala és d'enemic,
|
||||||
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet);
|
// no atribuïm el kill a cap player (NO_SHOOTER).
|
||||||
|
const uint8_t SHOOTER = IS_ENEMY_BULLET ? NO_SHOOTER : BULLET_OWNER;
|
||||||
|
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, SHOOTER, &bullet);
|
||||||
breakBullet(ctx.debris_manager, bullet);
|
breakBullet(ctx.debris_manager, bullet);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -200,16 +212,59 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void detectEnemyBulletShip(Context& ctx) {
|
||||||
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER;
|
||||||
|
constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE;
|
||||||
|
|
||||||
|
for (auto& bullet : ctx.bullets) {
|
||||||
|
if (!bullet.isActive() || bullet.getOwnerId() < ENEMY_BASE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (uint8_t player_id = 0; player_id < 2; player_id++) {
|
||||||
|
if (ctx.hit_timer_per_player[player_id] > 0.0F ||
|
||||||
|
!ctx.ships[player_id].isActive() ||
|
||||||
|
ctx.ships[player_id].isInvulnerable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool JUGADOR_ACTIU = (player_id == 0)
|
||||||
|
? ctx.match_config.player1_active
|
||||||
|
: ctx.match_config.player2_active;
|
||||||
|
if (!JUGADOR_ACTIU) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), ctx.ships[player_id], AMPLIFIER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *** BALA D'ENEMIC → SHIP ***
|
||||||
|
// Apliquem l'impuls de la bala abans del hurt/death perquè la
|
||||||
|
// velocitat de la nau quedi capturada per als debris.
|
||||||
|
const Vec2 IMPULSE = bullet.getBody().velocity *
|
||||||
|
(bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor);
|
||||||
|
ctx.ships[player_id].getBody().applyImpulse(IMPULSE);
|
||||||
|
if (ctx.ships[player_id].isHurt()) {
|
||||||
|
// Segon impacte durant HURT → mort.
|
||||||
|
ctx.on_player_hit(player_id);
|
||||||
|
} else {
|
||||||
|
ctx.ships[player_id].hurt();
|
||||||
|
}
|
||||||
|
breakBullet(ctx.debris_manager, bullet);
|
||||||
|
break; // una bala impacta una vegada per frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void detectAll(Context& ctx) {
|
void detectAll(Context& ctx) {
|
||||||
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
|
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
|
||||||
detectBulletEnemy(ctx);
|
detectBulletEnemy(ctx);
|
||||||
detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo
|
detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo
|
||||||
detectShipEnemy(ctx);
|
detectShipEnemy(ctx);
|
||||||
detectBulletPlayer(ctx);
|
detectBulletPlayer(ctx);
|
||||||
|
detectEnemyBulletShip(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
void desactivateOutOfBoundsBullets(
|
void desactivateOutOfBoundsBullets(
|
||||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets,
|
||||||
Effects::DebrisManager& debris_manager) {
|
Effects::DebrisManager& debris_manager) {
|
||||||
float min_x;
|
float min_x;
|
||||||
float max_x;
|
float max_x;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace Systems::Collision {
|
|||||||
struct Context {
|
struct Context {
|
||||||
std::array<Ship, 2>& ships;
|
std::array<Ship, 2>& ships;
|
||||||
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
|
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
|
||||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets;
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets;
|
||||||
std::array<float, 2>& hit_timer_per_player;
|
std::array<float, 2>& hit_timer_per_player;
|
||||||
std::array<int, 2>& score_per_player;
|
std::array<int, 2>& score_per_player;
|
||||||
std::array<int, 2>& lives_per_player;
|
std::array<int, 2>& lives_per_player;
|
||||||
@@ -67,6 +67,10 @@ namespace Systems::Collision {
|
|||||||
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
|
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
|
||||||
void detectBulletPlayer(Context& ctx);
|
void detectBulletPlayer(Context& ctx);
|
||||||
|
|
||||||
|
// Bales d'enemic (owner_id ≥ ENEMY_OWNER_BASE) impactant un ship: aplica
|
||||||
|
// impulse, primer hit → hurt(), segon hit durant HURT → on_player_hit.
|
||||||
|
void detectEnemyBulletShip(Context& ctx);
|
||||||
|
|
||||||
// Las tres en orden lógico del frame.
|
// Las tres en orden lógico del frame.
|
||||||
void detectAll(Context& ctx);
|
void detectAll(Context& ctx);
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ namespace Systems::Collision {
|
|||||||
// (8 fragments de l'octàgon) i el so HIT. Cal cridar-la després de detectAll()
|
// (8 fragments de l'octàgon) i el so HIT. Cal cridar-la després de detectAll()
|
||||||
// perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte.
|
// perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte.
|
||||||
void desactivateOutOfBoundsBullets(
|
void desactivateOutOfBoundsBullets(
|
||||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets,
|
||||||
Effects::DebrisManager& debris_manager);
|
Effects::DebrisManager& debris_manager);
|
||||||
|
|
||||||
} // namespace Systems::Collision
|
} // namespace Systems::Collision
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// enemy_ai_system.cpp - Implementació del dispatcher de moviment d'enemics
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "game/systems/enemy_ai_system.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
#include "game/constants.hpp"
|
||||||
|
#include "game/entities/bullet.hpp"
|
||||||
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
#include "game/entities/bullet_registry.hpp"
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto randFloat01() -> float {
|
||||||
|
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const auto& mv = enemy.getConfig().ai.movement;
|
||||||
|
EnemyAiState& state = enemy.getAiState();
|
||||||
|
state.direction_change_timer += delta_time;
|
||||||
|
|
||||||
|
if (randFloat01() < mv.zigzag_prob_per_second * delta_time) {
|
||||||
|
const Vec2 VEL = enemy.getBody().velocity;
|
||||||
|
const float CURRENT_ANGLE = velocityToAngle(VEL);
|
||||||
|
const float DELTA = randFloat01() * mv.angle_change_max;
|
||||||
|
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
|
||||||
|
const float SPEED = VEL.length();
|
||||||
|
enemy.setVelocityFromAngle(NEW_ANGLE, SPEED);
|
||||||
|
state.direction_change_timer = 0.0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRACKING: cada N segons, interpola la velocitat actual cap a la
|
||||||
|
// direcció del ship mantenint la mateixa magnitud. Còpia literal del
|
||||||
|
// legacy Enemy::behaviorSquare.
|
||||||
|
void moveTracking(Enemy& enemy, float delta_time) {
|
||||||
|
const auto& mv = enemy.getConfig().ai.movement;
|
||||||
|
EnemyAiState& state = enemy.getAiState();
|
||||||
|
state.tracking_timer += delta_time;
|
||||||
|
|
||||||
|
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||||
|
if (state.tracking_timer < mv.tracking_interval || ship_pos == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.tracking_timer = 0.0F;
|
||||||
|
|
||||||
|
const Vec2 TO_SHIP = *ship_pos - enemy.getCenter();
|
||||||
|
const float DIST = TO_SHIP.length();
|
||||||
|
if (DIST <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vec2 DESIRED_DIR = TO_SHIP / DIST;
|
||||||
|
const float SPEED = enemy.getBody().velocity.length();
|
||||||
|
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
|
||||||
|
const float STRENGTH = state.tracking_strength;
|
||||||
|
|
||||||
|
Vec2 new_vel = (enemy.getBody().velocity * (1.0F - STRENGTH)) +
|
||||||
|
(DESIRED_VEL * STRENGTH);
|
||||||
|
|
||||||
|
const float NEW_SPEED = new_vel.length();
|
||||||
|
if (NEW_SPEED > 0.0F) {
|
||||||
|
new_vel = new_vel * (SPEED / NEW_SPEED);
|
||||||
|
}
|
||||||
|
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 = findNearestShipPosition(enemy);
|
||||||
|
if (ship_pos == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vec2 TO_SHIP = *ship_pos - enemy.getCenter();
|
||||||
|
const float DIST = TO_SHIP.length();
|
||||||
|
const float BASE = enemy.getRotationBase();
|
||||||
|
if (DIST < mv.proximity_distance) {
|
||||||
|
enemy.setRotationDelta(BASE * mv.rotation_proximity_multiplier);
|
||||||
|
} else {
|
||||||
|
enemy.setRotationDelta(BASE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHOOT: cerca slot lliure a ctx.bullets i el dispara amb el bullet config
|
||||||
|
// referenciat per nom (lazy-load via BulletRegistry). Angle segons aim_mode +
|
||||||
|
// jitter. owner_id = ENEMY_OWNER_BASE + enemy_index per al filtre d'autoimpacte.
|
||||||
|
void doShoot(Systems::Collision::Context& ctx, const Enemy& enemy, std::size_t enemy_index, const AiTickAction& action) {
|
||||||
|
if (action.bullet_config_name.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const BulletConfig* cfg = BulletRegistry::get(action.bullet_config_name);
|
||||||
|
if (cfg == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Cerca slot dins la zona reservada per a enemics: així no robem
|
||||||
|
// slots als pools de player (que iteren [0, MAX_BULLETS) i [MAX_BULLETS, 2*MAX_BULLETS)).
|
||||||
|
Bullet* slot = nullptr;
|
||||||
|
constexpr std::size_t START = Defaults::Entities::ENEMY_BULLET_START_IDX;
|
||||||
|
constexpr std::size_t END = START + Defaults::Entities::MAX_ENEMY_BULLETS;
|
||||||
|
for (std::size_t i = START; i < END; ++i) {
|
||||||
|
if (!ctx.bullets[i].isActive()) {
|
||||||
|
slot = &ctx.bullets[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slot == nullptr) {
|
||||||
|
return; // pool d'enemic ple
|
||||||
|
}
|
||||||
|
|
||||||
|
float angle = 0.0F;
|
||||||
|
if (action.aim_mode == AimMode::AIMED) {
|
||||||
|
const Vec2* target = findNearestShipPosition(enemy);
|
||||||
|
if (target == nullptr) {
|
||||||
|
// Sense ship viu: degrada a random per no congelar el dispar.
|
||||||
|
angle = randFloat01() * 2.0F * Constants::PI;
|
||||||
|
} else {
|
||||||
|
const Vec2 TO = *target - enemy.getCenter();
|
||||||
|
// angle=0 apunta amunt (eix Y negatiu SDL): atan2 + PI/2.
|
||||||
|
angle = std::atan2(TO.y, TO.x) + (Constants::PI / 2.0F);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
angle = randFloat01() * 2.0F * Constants::PI;
|
||||||
|
}
|
||||||
|
if (action.jitter_rad > 0.0F) {
|
||||||
|
angle += (randFloat01() - 0.5F) * 2.0F * action.jitter_rad;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto OWNER = static_cast<uint8_t>(Defaults::Entities::ENEMY_OWNER_BASE + enemy_index);
|
||||||
|
slot->fire(enemy.getCenter(), angle, OWNER, action.bullet_speed, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void runMovement(Enemy& enemy, float delta_time) {
|
||||||
|
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:
|
||||||
|
moveTracking(enemy, delta_time);
|
||||||
|
break;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void move(Enemy& enemy, float delta_time) {
|
||||||
|
if (!enemy.isActive() || enemy.isWounded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runMovement(enemy, delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick(Systems::Collision::Context& ctx, Enemy& enemy, std::size_t enemy_index, float delta_time) {
|
||||||
|
if (!enemy.isActive() || enemy.isWounded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runMovement(enemy, delta_time);
|
||||||
|
|
||||||
|
// Accions periòdiques: decrementa timer, dispara quan ≤0.
|
||||||
|
auto& timers = enemy.getAiTickTimers();
|
||||||
|
const auto& actions = enemy.getConfig().ai.tick;
|
||||||
|
for (std::size_t i = 0; i < actions.size() && i < timers.size(); ++i) {
|
||||||
|
timers[i] -= delta_time;
|
||||||
|
if (timers[i] > 0.0F) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
timers[i] = actions[i].interval;
|
||||||
|
switch (actions[i].type) {
|
||||||
|
case AiActionType::SHOOT:
|
||||||
|
doShoot(ctx, enemy, enemy_index, actions[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Systems::EnemyAi
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// enemy_ai_system.hpp - Executa la IA d'un enemic (moviment + accions tick)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Llegeix `enemy.getConfig().ai` i aplica la primitiva de moviment activa i,
|
||||||
|
// opcionalment, les accions periòdiques declarades (tick). Reemplaça el switch
|
||||||
|
// per type hardcoded de l'antic Enemy::update.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include "game/systems/collision_system.hpp"
|
||||||
|
|
||||||
|
class Enemy;
|
||||||
|
|
||||||
|
namespace Systems::EnemyAi {
|
||||||
|
|
||||||
|
// Aplica només la primitiva de moviment activa de l'enemic (sense disparar
|
||||||
|
// ni cap altra acció tick). S'usa als steps secundaris (continue/gameover/
|
||||||
|
// death) on els enemics han de continuar movent-se de fons però no actuar.
|
||||||
|
void move(Enemy& enemy, float delta_time);
|
||||||
|
|
||||||
|
// Aplica moviment + accions periòdiques (SHOOT, etc.). enemy_index és
|
||||||
|
// l'índex de l'enemic dins ctx.enemies; s'usa per construir el seu
|
||||||
|
// owner_id de bala (ENEMY_OWNER_BASE + index), de manera que el sistema
|
||||||
|
// de col·lisions pugui filtrar el self-shot.
|
||||||
|
void tick(Collision::Context& ctx, Enemy& enemy, std::size_t enemy_index, float delta_time);
|
||||||
|
|
||||||
|
} // namespace Systems::EnemyAi
|
||||||
Reference in New Issue
Block a user