feat(enemy): els enemics poden disparar bales via tick d'IA
This commit is contained in:
1 parent
5ad433e63a
commit
14295ce859
18 files changed
+361
-92
No files matched your search
@@ -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<uint8_t>(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<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) {
|
||||
float min_x;
|
||||
float max_x;
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Systems::Collision {
|
||||
struct Context {
|
||||
std::array<Ship, 2>& ships;
|
||||
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<int, 2>& score_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.
|
||||
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<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);
|
||||
|
||||
} // namespace Systems::Collision
|
||||
@@ -6,8 +6,12 @@
|
||||
#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"
|
||||
@@ -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
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <cstddef>
|
||||
|
||||
#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
|
||||
Reference in new issue
Block a user