diff --git a/data/entities/bullet_long/bullet_long.yaml b/data/entities/bullet_long/bullet_long.yaml new file mode 100644 index 0000000..7950779 --- /dev/null +++ b/data/entities/bullet_long/bullet_long.yaml @@ -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 diff --git a/data/entities/star/star.yaml b/data/entities/star/star.yaml index 00b6fb0..4ed165e 100644 --- a/data/entities/star/star.yaml +++ b/data/entities/star/star.yaml @@ -15,10 +15,20 @@ physics: linear_damping: 0.0 angular_damping: 0.0 -behavior: - # Hereta el comportament de Pentagon (zigzag esquivador). - angle_change_max: 1.0 - zigzag_prob_per_second: 0.8 +ai: + # Movement: zigzag esquivador (com Pentagon). + movement: + 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: pulse: diff --git a/data/shapes/bullet_long.shp b/data/shapes/bullet_long.shp index 03581c8..f377e94 100644 --- a/data/shapes/bullet_long.shp +++ b/data/shapes/bullet_long.shp @@ -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 # -# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units -# per una línia horitzontal superior i una d'inferior. La silueta resultant -# és una càpsula amb la separació visible dels dos cercles al centre. +# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle +# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol. +# 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: -# Centre octàgon esquerre: (-3, 0) -# Centre octàgon dret: ( 3, 0) -# Punt de tangència: ( 0, 0) -# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6). +# Mig-octàgon superior (radi 3) centrat a (0, -3) +# Mig-octàgon inferior (radi 3) centrat a (0, 3) +# Punt extrem superior: (0, -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 scale: 1.0 center: 0, 0 -# Octàgon esquerre (centre x=-3, radi 3) -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 +# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre) +polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3 -# Octàgon dret (centre x=3, radi 3) -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 +# Mig-octàgon inferior +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 -polyline: -3,-3 3,-3 +# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior) +polyline: 3,-3 3,3 -# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret -polyline: -3,3 3,3 +# Costat esquerre +polyline: -3,-3 -3,3 diff --git a/source/core/defaults/entities.hpp b/source/core/defaults/entities.hpp index 44f0c5a..230e8d8 100644 --- a/source/core/defaults/entities.hpp +++ b/source/core/defaults/entities.hpp @@ -3,10 +3,24 @@ #pragma once +#include + namespace Defaults::Entities { 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 // calcula el seu collision_radius com a diff --git a/source/game/entities/bullet.cpp b/source/game/entities/bullet.cpp index f85fdaa..ed92684 100644 --- a/source/game/entities/bullet.cpp +++ b/source/game/entities/bullet.cpp @@ -54,10 +54,29 @@ void Bullet::init() { 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; 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; prev_position_ = position; // spawn: swept degenera a punt-cercle angle_ = angle; diff --git a/source/game/entities/bullet.hpp b/source/game/entities/bullet.hpp index c63c08c..ac98014 100644 --- a/source/game/entities/bullet.hpp +++ b/source/game/entities/bullet.hpp @@ -19,7 +19,11 @@ class Bullet : public Entities::Entity { explicit Bullet(Rendering::Renderer* renderer); 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 postUpdate(float delta_time) override; void draw() const override; diff --git a/source/game/entities/bullet_registry.cpp b/source/game/entities/bullet_registry.cpp index f76bd6d..3755e06 100644 --- a/source/game/entities/bullet_registry.cpp +++ b/source/game/entities/bullet_registry.cpp @@ -1,4 +1,4 @@ -// bullet_registry.cpp - Implementació del registre de bala +// bullet_registry.cpp - Implementació del registre de bales // © 2026 JailDesigner #include "game/entities/bullet_registry.hpp" @@ -8,23 +8,34 @@ #include "core/entities/entity_loader.hpp" -BulletConfig BulletRegistry::config; +std::unordered_map BulletRegistry::configs; 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& 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 yaml = Entities::EntityLoader::load("bullet"); - if (!yaml) { - std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar bullet.yaml\n"; + if (loadInto(configs, "bullet") == nullptr) { 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; - std::cout << "[BulletRegistry] Configuració de bala carregada.\n"; + std::cout << "[BulletRegistry] Configuració de bala 'bullet' carregada.\n"; return true; } @@ -33,5 +44,13 @@ auto BulletRegistry::get() -> const BulletConfig& { std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n"; 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); } diff --git a/source/game/entities/bullet_registry.hpp b/source/game/entities/bullet_registry.hpp index 410c2c7..35478a7 100644 --- a/source/game/entities/bullet_registry.hpp +++ b/source/game/entities/bullet_registry.hpp @@ -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 // -// Una única instància per a tota la sessió. Es manté el patró registry -// (paral·lel a EnemyRegistry) tot i ser una sola entitat: si el dia de demà -// hi ha més tipus de bala (laser/plasma/etc.) només cal estendre-ho. +// Diverses configuracions per nom (data/entities//.yaml). El +// config "bullet" es manté com a default històric (player) i es carrega via +// 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 +#include +#include + #include "game/entities/bullet_config.hpp" class BulletRegistry { public: 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; - // Accés a la configuració. Avorta amb log fatal si load() no s'ha cridat - // o ha fallat. + // Accés a la configuració per defecte ("bullet"). Avorta amb log fatal si + // load() no s'ha cridat o ha fallat. Mantingut per a backwards compat del Bullet ctor. static auto get() -> const BulletConfig&; + // Accés a una configuració per nom. Lazy-load: si no està al map, intenta + // carregar data/entities//.yaml. Retorna nullptr si no es pot. + static auto get(const std::string& name) -> const BulletConfig*; + private: - static BulletConfig config; + static std::unordered_map configs; static bool loaded; }; diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index f83b3eb..b45d782 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -57,6 +57,13 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { ai_state_ = EnemyAiState{}; 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); if (!shape_ || !shape_->isValid()) { std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index aa59066..3250925 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -6,6 +6,7 @@ #include #include +#include #include "core/entities/entity.hpp" #include "core/types.hpp" @@ -81,6 +82,7 @@ class Enemy : public Entities::Entity { // Accessors per al sistema d'IA (Systems::EnemyAi). [[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; } + [[nodiscard]] auto getAiTickTimers() -> std::vector& { 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. @@ -136,6 +138,11 @@ class Enemy : public Entities::Entity { // Estat per-instància que la primitiva de moviment manté entre frames. 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 ai_tick_timers_; + // Referències als 2 ships per a AI de tracking/proximity/chase/flee. std::array ships_{nullptr, nullptr}; diff --git a/source/game/entities/enemy_ai.hpp b/source/game/entities/enemy_ai.hpp index d61439c..b735ff0 100644 --- a/source/game/entities/enemy_ai.hpp +++ b/source/game/entities/enemy_ai.hpp @@ -65,7 +65,8 @@ struct AiTickAction { float interval{1.0F}; AimMode aim_mode{AimMode::RANDOM}; 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 { diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp index 08d1e7e..0b91fee 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -308,6 +308,9 @@ namespace { if (item.contains("jitter_rad")) { action.jitter_rad = item["jitter_rad"].get_value(); } + if (item.contains("bullet_speed")) { + action.bullet_speed = item["bullet_speed"].get_value(); + } if (item.contains("bullet")) { action.bullet_config_name = item["bullet"].get_value(); } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 6a7cf8d..7aab904 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -354,7 +354,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool { // Enemies, bullets y efectos siguen moviéndose en background. for (auto& enemy : enemies_) { - Systems::EnemyAi::tick(enemy, delta_time); + Systems::EnemyAi::move(enemy, delta_time); enemy.update(delta_time); } for (auto& bullet : bullets_) { @@ -381,7 +381,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool { // Enemies, bullets y efectos siguen moviéndose como fondo. for (auto& enemy : enemies_) { - Systems::EnemyAi::tick(enemy, delta_time); + Systems::EnemyAi::move(enemy, delta_time); enemy.update(delta_time); } for (auto& bullet : bullets_) { @@ -431,7 +431,7 @@ void GameScene::stepDeathSequence(float delta_time) { // aunque otros jugadores aún jueguen. if (algun_mort) { for (auto& enemy : enemies_) { - Systems::EnemyAi::tick(enemy, delta_time); + Systems::EnemyAi::move(enemy, delta_time); enemy.update(delta_time); } for (auto& bullet : bullets_) { @@ -531,9 +531,10 @@ void GameScene::runStagePlaying(float delta_time) { ships_[i].update(delta_time); } } - for (auto& enemy : enemies_) { - Systems::EnemyAi::tick(enemy, delta_time); - enemy.update(delta_time); + auto ai_ctx = buildCollisionContext(); + 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 @@ -567,8 +568,8 @@ void GameScene::runStageLevelCompleted(float delta_time) { floating_score_manager_.update(delta_time); } -void GameScene::runCollisionDetections() { - Systems::Collision::Context col_ctx{ +auto GameScene::buildCollisionContext() -> Systems::Collision::Context { + return Systems::Collision::Context{ .ships = ships_, .enemies = enemies_, .bullets = bullets_, @@ -581,6 +582,10 @@ void GameScene::runCollisionDetections() { .match_config = match_config_, .on_player_hit = [this](uint8_t pid) { tocado(pid); }, }; +} + +void GameScene::runCollisionDetections() { + auto col_ctx = buildCollisionContext(); Systems::Collision::detectAll(col_ctx); } diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index d634f81..44b8c85 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -28,6 +28,7 @@ #include "game/entities/ship.hpp" #include "game/stage_system/stage_config.hpp" #include "game/stage_system/stage_manager.hpp" +#include "game/systems/collision_system.hpp" // Game over state machine enum class GameOverState : uint8_t { @@ -67,7 +68,7 @@ class GameScene final : public Scene { std::array enemies_; // 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. - std::array(Constants::MAX_BULLETS) * 2> bullets_; + std::array(Defaults::Entities::MAX_BULLETS_TOTAL)> bullets_; std::array hit_timer_per_player_; // Death timers per player (seconds) // Lives and game over system @@ -144,6 +145,10 @@ class GameScene final : public Scene { void runStageLevelStart(float delta_time); void runStagePlaying(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. void runCollisionDetections(); }; diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index dd5f302..ea3d4d3 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -41,20 +41,32 @@ namespace Systems::Collision { void detectBulletEnemy(Context& ctx) { 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) { if (!bullet.isActive()) { 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(ENEMY_BASE + i)) { + continue; + } if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) { continue; } // *** COLISIÓN bullet → enemy *** // La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu - // al YAML de l'enemic via la secció `events:`. Aquí només dispatchem. - Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet); + // al YAML de l'enemic via la secció `events:`. Si la bala és d'enemic, + // 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); 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) { processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame detectBulletEnemy(ctx); detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo detectShipEnemy(ctx); detectBulletPlayer(ctx); + detectEnemyBulletShip(ctx); } void desactivateOutOfBoundsBullets( - std::array(Defaults::Entities::MAX_BULLETS) * 2>& bullets, + std::array(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets, Effects::DebrisManager& debris_manager) { float min_x; float max_x; diff --git a/source/game/systems/collision_system.hpp b/source/game/systems/collision_system.hpp index 1503603..e2cd644 100644 --- a/source/game/systems/collision_system.hpp +++ b/source/game/systems/collision_system.hpp @@ -32,7 +32,7 @@ namespace Systems::Collision { struct Context { std::array& ships; std::array& enemies; - std::array(Defaults::Entities::MAX_BULLETS) * 2>& bullets; + std::array(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets; std::array& hit_timer_per_player; std::array& score_per_player; std::array& lives_per_player; @@ -67,6 +67,10 @@ namespace Systems::Collision { // atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet. 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. 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() // perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte. void desactivateOutOfBoundsBullets( - std::array(Defaults::Entities::MAX_BULLETS) * 2>& bullets, + std::array(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets, Effects::DebrisManager& debris_manager); } // namespace Systems::Collision diff --git a/source/game/systems/enemy_ai_system.cpp b/source/game/systems/enemy_ai_system.cpp index 2c0b147..a5fef30 100644 --- a/source/game/systems/enemy_ai_system.cpp +++ b/source/game/systems/enemy_ai_system.cpp @@ -6,8 +6,12 @@ #include #include +#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" @@ -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(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 tick(Enemy& enemy, float delta_time) { + void move(Enemy& enemy, float delta_time) { if (!enemy.isActive() || enemy.isWounded()) { return; } - 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; + 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; + } } } diff --git a/source/game/systems/enemy_ai_system.hpp b/source/game/systems/enemy_ai_system.hpp index 89b8915..95de212 100644 --- a/source/game/systems/enemy_ai_system.hpp +++ b/source/game/systems/enemy_ai_system.hpp @@ -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 // -// Llegeix `enemy.getConfig().ai.movement` i aplica la primitiva corresponent -// (ZIGZAG, TRACKING, RECTILINEAR_PROXIMITY...). El sistema reemplaça el switch -// per type que vivia dins 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. +// 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 + +#include "game/systems/collision_system.hpp" + class Enemy; namespace Systems::EnemyAi { - // Aplica la primitiva de moviment activa de l'enemic durant aquest frame. - // No fa res si l'enemic està actualment wounded (manté el comportament - // legacy del switch original). - void tick(Enemy& enemy, float delta_time); + // 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