Files
orni-attack/source/game/systems/demo_pilot.cpp
T

261 lines
11 KiB
C++

// demo_pilot.cpp - Implementación de la IA del attract mode
// © 2026 JailDesigner
#include "game/systems/demo_pilot.hpp"
#include <cmath>
#include <cstdlib>
#include "core/types.hpp"
namespace Systems::Demo {
namespace {
constexpr float PI = Constants::PI;
constexpr float TWO_PI = 2.0F * PI;
// Cadencia y reacción (segundos).
constexpr float RETARGET_INTERVAL = 0.15F; // re-evaluar objetivo (reacción humana)
constexpr float FIRE_COOLDOWN = 0.32F; // entre disparos
// Apuntado.
constexpr float ROTATE_DEADZONE = 0.05F; // rad: zona muerta para no oscilar
constexpr float FIRE_TOLERANCE = 0.18F; // rad: error máximo para disparar
constexpr float AIM_JITTER_MAX = 0.10F; // rad: error de apuntado humano
constexpr float LEAD_TIME = 0.30F; // s: anticipación sobre el enemigo
// Distancias (px).
constexpr float DANGER_RADIUS = 95.0F; // por debajo: maniobra de esquiva
constexpr float APPROACH_RADIUS = 250.0F; // por encima: acercarse al objetivo
constexpr float WALL_MARGIN = 60.0F; // px desde el borde: sesgo al centro
constexpr float WALL_BIAS = 0.6F; // peso del empuje hacia el centro al esquivar
// Esquiva de balas enemigas.
constexpr float DODGE_SCAN_RADIUS = 190.0F; // px: distancia a la que reacciona
constexpr float DODGE_HEADING_MIN = 0.25F; // dot mínimo: la bala viene hacia la nave
// Foc amic: retindre el tret si el company està en la línia de tir.
constexpr float FRIENDLY_BLOCK_RANGE = 1200.0F; // px endavant que es vigilen
constexpr float FRIENDLY_BLOCK_MARGIN = 22.0F; // px: marge sobre el radi del company
// [-1, 1] aleatorio (estética: jitter de apuntado; no afecta a la simulación).
auto randSigned() -> float {
return (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX) * 2.0F) - 1.0F;
}
// Envuelve a [-PI, PI].
auto wrapPi(float angle) -> float {
return std::remainder(angle, TWO_PI);
}
struct Nearest {
int index{-1};
float distance{0.0F};
Vec2 center{};
Vec2 velocity{};
};
auto findNearest(const Vec2& from,
const std::array<Enemy, Constants::MAX_ORNIS>& enemies) -> Nearest {
Nearest best;
float best_d2 = 0.0F;
for (std::size_t i = 0; i < enemies.size(); ++i) {
if (!enemies[i].isActive()) {
continue;
}
const Vec2 C = enemies[i].getCenter();
const float D2 = (C - from).lengthSquared();
if (best.index == -1 || D2 < best_d2) {
best.index = static_cast<int>(i);
best_d2 = D2;
best.center = C;
best.velocity = enemies[i].getVelocityVector();
}
}
if (best.index != -1) {
best.distance = std::sqrt(best_d2);
}
return best;
}
struct BulletThreat {
bool found{false};
Vec2 position{};
Vec2 velocity{};
};
// Bala enemiga más cercana que viene hacia la nave (dentro del radio de
// reacción). Solo balas de enemic (owner_id >= ENEMY_OWNER_BASE).
auto findBulletThreat(const Vec2& ship_pos,
const std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets)
-> BulletThreat {
BulletThreat threat;
float best_d2 = 0.0F;
for (const auto& bullet : bullets) {
if (!bullet.isActive() ||
bullet.getOwnerId() < Defaults::Entities::ENEMY_OWNER_BASE) {
continue;
}
const Vec2 BPOS = bullet.getCenter();
const Vec2 TO_SHIP = ship_pos - BPOS;
const float D2 = TO_SHIP.lengthSquared();
if (D2 > DODGE_SCAN_RADIUS * DODGE_SCAN_RADIUS) {
continue;
}
const Vec2 BVEL = bullet.getBody().velocity;
if (BVEL.lengthSquared() < 1.0F) {
continue;
}
// ¿La bala se dirige hacia la nave?
if (BVEL.normalized().dot(TO_SHIP.normalized()) < DODGE_HEADING_MIN) {
continue;
}
if (!threat.found || D2 < best_d2) {
threat.found = true;
best_d2 = D2;
threat.position = BPOS;
threat.velocity = BVEL;
}
}
return threat;
}
// Error angular (rad, [-PI,PI]) entre la nariz de la nave y desired_dir,
// con el jitter de apuntado aplicado. La nariz apunta hacia (angle - PI/2).
auto steerError(const Ship& ship, const Vec2& desired_dir, float jitter) -> float {
const float DESIRED = std::atan2(desired_dir.y, desired_dir.x) + jitter;
const float NOSE = ship.getAngle() - (PI / 2.0F);
return wrapPi(DESIRED - NOSE);
}
// Setea rotación según el error (RIGHT incrementa ship.angle; LEFT lo
// decrementa), con zona muerta para no oscilar.
void applyRotation(Control& ctrl, float error) {
if (error > ROTATE_DEADZONE) {
ctrl.right = true;
} else if (error < -ROTATE_DEADZONE) {
ctrl.left = true;
}
}
// ¿El company està en la línia de tir? (davant del morro, dins de l'abast i
// a prou poca distància perpendicular de la trajectòria recta de la bala).
// La bala ix en la direcció del morro: (cos, sin)(angle - PI/2).
auto teammateInLineOfFire(const Ship& ship, const Ship& mate) -> bool {
const float NOSE = ship.getAngle() - (PI / 2.0F);
const Vec2 FORWARD{.x = std::cos(NOSE), .y = std::sin(NOSE)};
const Vec2 TO_MATE = mate.getCenter() - ship.getCenter();
const float ALONG = TO_MATE.dot(FORWARD); // distància al llarg del tret
if (ALONG <= 0.0F || ALONG > FRIENDLY_BLOCK_RANGE) {
return false; // darrere de la nau o massa lluny
}
const float PERP2 = TO_MATE.lengthSquared() - (ALONG * ALONG);
const float CLEAR = mate.getCollisionRadius() + FRIENDLY_BLOCK_MARGIN;
return PERP2 < CLEAR * CLEAR;
}
} // namespace
auto DemoPilot::compute(const Ship& ship,
const Ship* teammate,
const std::array<Enemy, Constants::MAX_ORNIS>& enemies,
const std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets,
const SDL_FRect& play_area,
float delta_time) -> Control {
Control ctrl;
if (fire_cooldown_ > 0.0F) {
fire_cooldown_ -= delta_time;
}
retarget_timer_ -= delta_time;
if (retarget_timer_ <= 0.0F) {
retarget_timer_ = RETARGET_INTERVAL;
aim_jitter_ = randSigned() * AIM_JITTER_MAX;
}
if (!ship.isActive()) {
return ctrl; // nave muerta: sin control
}
const Vec2 SHIP_POS = ship.getCenter();
const Vec2 PLAY_CENTRE{
.x = play_area.x + (play_area.w / 2.0F),
.y = play_area.y + (play_area.h / 2.0F)};
const Vec2 TO_CENTRE = (PLAY_CENTRE - SHIP_POS).normalized();
// Prioridad 1: esquivar una bala enemiga entrante. Se mueve perpendicular
// a la trayectoria de la bala (con sesgo al centro) y no dispara.
const BulletThreat THREAT = findBulletThreat(SHIP_POS, bullets);
if (THREAT.found) {
const Vec2 BV = THREAT.velocity.normalized();
Vec2 perp{.x = -BV.y, .y = BV.x};
if ((SHIP_POS - THREAT.position).dot(perp) < 0.0F) {
perp = -perp; // hacia el lado en que ya está la nave
}
const Vec2 ESCAPE = (perp + (TO_CENTRE * WALL_BIAS)).normalized();
applyRotation(ctrl, steerError(ship, ESCAPE, aim_jitter_));
ctrl.thrust = true;
return ctrl;
}
const Nearest TARGET = findNearest(SHIP_POS, enemies);
target_idx_ = TARGET.index;
// Sin enemigos: deriva tranquila (gira despacio, sin empuje ni disparo).
if (TARGET.index == -1) {
ctrl.right = true;
return ctrl;
}
const bool NEAR_WALL =
SHIP_POS.x < play_area.x + WALL_MARGIN ||
SHIP_POS.x > play_area.x + play_area.w - WALL_MARGIN ||
SHIP_POS.y < play_area.y + WALL_MARGIN ||
SHIP_POS.y > play_area.y + play_area.h - WALL_MARGIN;
Vec2 desired_dir;
const bool DANGER = TARGET.distance < DANGER_RADIUS;
if (DANGER) {
// Prioridad 2: alejarse del enemigo pegado, con sesgo al centro.
const Vec2 AWAY = (SHIP_POS - TARGET.center).normalized();
desired_dir = (AWAY + (TO_CENTRE * WALL_BIAS)).normalized();
ctrl.thrust = true;
} else {
// Combate: apuntar al enemigo con lead + jitter.
const Vec2 PREDICTED = TARGET.center + (TARGET.velocity * LEAD_TIME);
desired_dir = (PREDICTED - SHIP_POS).normalized();
}
const float ERROR = steerError(ship, desired_dir, aim_jitter_);
applyRotation(ctrl, ERROR);
if (!DANGER) {
// Acercarse si el objetivo está lejos (mantiene la nave cazando),
// pero no empujar de cara a una pared.
const bool FACING_WALL = NEAR_WALL && TO_CENTRE.dot(desired_dir) < 0.0F;
if (TARGET.distance > APPROACH_RADIUS && !FACING_WALL &&
std::fabs(ERROR) < FIRE_TOLERANCE) {
ctrl.thrust = true;
}
// Disparar cuando está bien encarada y el cooldown lo permite, però NO
// si el company està en la línia de tir (i seria víctima vàlida de foc
// amic): es retén el tret sense gastar cooldown, així dispara tan prompte
// com l'altra nau isca de la línia.
if (std::fabs(ERROR) < FIRE_TOLERANCE && fire_cooldown_ <= 0.0F) {
const bool FRIENDLY_BLOCK = (teammate != nullptr) &&
teammate->isActive() && !teammate->isInvulnerable() &&
teammateInLineOfFire(ship, *teammate);
if (!FRIENDLY_BLOCK) {
ctrl.shoot = true;
fire_cooldown_ = FIRE_COOLDOWN;
}
}
}
return ctrl;
}
} // namespace Systems::Demo