feat(enemy): els enemics poden disparar bales via tick d'IA

This commit is contained in:
2026-05-25 20:05:01 +02:00
parent 5ad433e63a
commit 14295ce859
18 changed files with 361 additions and 92 deletions
@@ -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
+14 -4
View File
@@ -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
View File
@@ -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
+15 -1
View File
@@ -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
+20 -1
View File
@@ -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;
+5 -1
View File
@@ -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;
+32 -13
View File
@@ -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);
} }
+16 -8
View File
@@ -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;
}; };
+7
View File
@@ -57,6 +57,13 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
ai_state_ = EnemyAiState{}; ai_state_ = EnemyAiState{};
ai_state_.tracking_strength = cfg.ai.movement.tracking_strength; ai_state_.tracking_strength = cfg.ai.movement.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);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
+7
View File
@@ -6,6 +6,7 @@
#include <array> #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"
@@ -81,6 +82,7 @@ class Enemy : public Entities::Entity {
// Accessors per al sistema d'IA (Systems::EnemyAi). // Accessors per al sistema d'IA (Systems::EnemyAi).
[[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; } [[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; } [[nodiscard]] auto getRotationBase() const -> float { return animation_.rotation_delta_base; }
void setRotationDelta(float rot) { rotation_delta_ = rot; } void setRotationDelta(float rot) { rotation_delta_ = rot; }
// Public: el sistema d'IA reorienta la velocitat des d'un angle. // Public: el sistema d'IA reorienta la velocitat des d'un angle.
@@ -136,6 +138,11 @@ class Enemy : public Entities::Entity {
// Estat per-instància que la primitiva de moviment manté entre frames. // Estat per-instància que la primitiva de moviment manté entre frames.
EnemyAiState ai_state_; EnemyAiState ai_state_;
// Timers paral·lels a config_->ai.tick: timers_[i] és el temps restant
// (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. // Referències als 2 ships per a AI de tracking/proximity/chase/flee.
std::array<const Ship*, 2> ships_{nullptr, nullptr}; std::array<const Ship*, 2> ships_{nullptr, nullptr};
+2 -1
View File
@@ -65,7 +65,8 @@ struct AiTickAction {
float interval{1.0F}; float interval{1.0F};
AimMode aim_mode{AimMode::RANDOM}; AimMode aim_mode{AimMode::RANDOM};
float jitter_rad{0.0F}; float jitter_rad{0.0F};
std::string bullet_config_name; // referit per nom (Fase C); buit a Fase A/B 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 { struct EnemyAiConfig {
+3
View File
@@ -308,6 +308,9 @@ namespace {
if (item.contains("jitter_rad")) { if (item.contains("jitter_rad")) {
action.jitter_rad = item["jitter_rad"].get_value<float>(); 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")) { if (item.contains("bullet")) {
action.bullet_config_name = item["bullet"].get_value<std::string>(); action.bullet_config_name = item["bullet"].get_value<std::string>();
} }
+13 -8
View File
@@ -354,7 +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::tick(enemy, delta_time); Systems::EnemyAi::move(enemy, delta_time);
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
@@ -381,7 +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::tick(enemy, delta_time); Systems::EnemyAi::move(enemy, delta_time);
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
@@ -431,7 +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::tick(enemy, delta_time); Systems::EnemyAi::move(enemy, delta_time);
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
@@ -531,9 +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();
Systems::EnemyAi::tick(enemy, delta_time); for (std::size_t i = 0; i < enemies_.size(); ++i) {
enemy.update(delta_time); 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
@@ -567,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_,
@@ -581,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);
} }
+6 -1
View File
@@ -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();
}; };
+59 -4
View File
@@ -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;
+6 -2
View File
@@ -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
+99 -20
View File
@@ -6,8 +6,12 @@
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include "core/defaults.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.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.hpp"
#include "game/entities/enemy_ai.hpp" #include "game/entities/enemy_ai.hpp"
#include "game/entities/enemy_config.hpp" #include "game/entities/enemy_config.hpp"
@@ -165,31 +169,106 @@ namespace Systems::EnemyAi {
} }
} }
// 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 } // namespace
void tick(Enemy& enemy, float delta_time) { void move(Enemy& enemy, float delta_time) {
if (!enemy.isActive() || enemy.isWounded()) { if (!enemy.isActive() || enemy.isWounded()) {
return; return;
} }
switch (enemy.getConfig().ai.movement.type) { runMovement(enemy, delta_time);
case MovementType::ZIGZAG: }
case MovementType::WANDER:
// WANDER reusa la mecànica de canvi de direcció probabilístic; void tick(Systems::Collision::Context& ctx, Enemy& enemy, std::size_t enemy_index, float delta_time) {
// l'única diferència és semàntica i el tunning dels paràmetres. if (!enemy.isActive() || enemy.isWounded()) {
moveZigzag(enemy, delta_time); return;
break; }
case MovementType::TRACKING: runMovement(enemy, delta_time);
moveTracking(enemy, delta_time);
break; // Accions periòdiques: decrementa timer, dispara quan ≤0.
case MovementType::RECTILINEAR_PROXIMITY: auto& timers = enemy.getAiTickTimers();
moveRectilinearProximity(enemy, delta_time); const auto& actions = enemy.getConfig().ai.tick;
break; for (std::size_t i = 0; i < actions.size() && i < timers.size(); ++i) {
case MovementType::CHASE: timers[i] -= delta_time;
moveChase(enemy, delta_time); if (timers[i] > 0.0F) {
break; continue;
case MovementType::FLEE: }
moveFlee(enemy, delta_time); timers[i] = actions[i].interval;
break; switch (actions[i].type) {
case AiActionType::SHOOT:
doShoot(ctx, enemy, enemy_index, actions[i]);
break;
}
} }
} }
+18 -12
View File
@@ -1,23 +1,29 @@
// enemy_ai_system.hpp - Executa la primitiva de moviment d'un enemic // enemy_ai_system.hpp - Executa la IA d'un enemic (moviment + accions tick)
// © 2026 JailDesigner // © 2026 JailDesigner
// //
// Llegeix `enemy.getConfig().ai.movement` i aplica la primitiva corresponent // Llegeix `enemy.getConfig().ai` i aplica la primitiva de moviment activa i,
// (ZIGZAG, TRACKING, RECTILINEAR_PROXIMITY...). El sistema reemplaça el switch // opcionalment, les accions periòdiques declarades (tick). Reemplaça el switch
// per type que vivia dins Enemy::update. // per type hardcoded de l'antic Enemy::update.
//
// A Fase C s'ampliarà per dispatchar també `ai.tick` (accions periòdiques tipus
// SHOOT), moment en què caldrà passar el Systems::Collision::Context per
// accedir a les bales i als ships.
#pragma once #pragma once
#include <cstddef>
#include "game/systems/collision_system.hpp"
class Enemy; class Enemy;
namespace Systems::EnemyAi { namespace Systems::EnemyAi {
// Aplica la primitiva de moviment activa de l'enemic durant aquest frame. // Aplica només la primitiva de moviment activa de l'enemic (sense disparar
// No fa res si l'enemic està actualment wounded (manté el comportament // ni cap altra acció tick). S'usa als steps secundaris (continue/gameover/
// legacy del switch original). // death) on els enemics han de continuar movent-se de fons però no actuar.
void tick(Enemy& enemy, float delta_time); 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 } // namespace Systems::EnemyAi