feat(demo): attract mode amb pilot IA, escenaris curats i música contínua del títol
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// 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
|
||||
|
||||
// [-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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto DemoPilot::compute(const Ship& ship,
|
||||
const std::array<Enemy, Constants::MAX_ORNIS>& enemies,
|
||||
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 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;
|
||||
}
|
||||
|
||||
// Centro de la zona de juego (sesgo anti-pared).
|
||||
const Vec2 PLAY_CENTRE{
|
||||
.x = play_area.x + (play_area.w / 2.0F),
|
||||
.y = play_area.y + (play_area.h / 2.0F)};
|
||||
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) {
|
||||
// Esquiva: alejarse del enemigo, con sesgo hacia el centro para no
|
||||
// quedar atrapada contra la pared. Empuje activo para crear espacio.
|
||||
const Vec2 AWAY = (SHIP_POS - TARGET.center).normalized();
|
||||
const Vec2 TO_CENTRE = (PLAY_CENTRE - SHIP_POS).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();
|
||||
}
|
||||
|
||||
// Ángulo deseado de la nariz; la nariz apunta hacia (angle - PI/2).
|
||||
float desired_angle = std::atan2(desired_dir.y, desired_dir.x) + aim_jitter_;
|
||||
const float NOSE_ANGLE = ship.getAngle() - (PI / 2.0F);
|
||||
const float ERROR = wrapPi(desired_angle - NOSE_ANGLE);
|
||||
|
||||
// RIGHT incrementa ship.angle (→ nariz); LEFT lo decrementa.
|
||||
if (ERROR > ROTATE_DEADZONE) {
|
||||
ctrl.right = true;
|
||||
} else if (ERROR < -ROTATE_DEADZONE) {
|
||||
ctrl.left = true;
|
||||
}
|
||||
|
||||
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 &&
|
||||
(PLAY_CENTRE - SHIP_POS).normalized().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.
|
||||
if (std::fabs(ERROR) < FIRE_TOLERANCE && fire_cooldown_ <= 0.0F) {
|
||||
ctrl.shoot = true;
|
||||
fire_cooldown_ = FIRE_COOLDOWN;
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
} // namespace Systems::Demo
|
||||
@@ -0,0 +1,58 @@
|
||||
// demo_pilot.hpp - IA de piloto para el attract mode (demo jugándose sola)
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Calcula, cada frame, los controles de la nave (rotar/empujar/disparar) leyendo
|
||||
// el estado de juego en solo-lectura. No lee Input ni muta entidades: GameScene
|
||||
// aplica el Control resultante via Ship::applyMovement + fireBullet.
|
||||
//
|
||||
// El comportamiento busca parecer "humano", no óptimo: apuntado con lead y un
|
||||
// pequeño error, cadencia de disparo con cooldown, esquiva por radio de peligro
|
||||
// y sesgo hacia el centro para no pegarse a las paredes.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "game/constants.hpp"
|
||||
#include "game/entities/enemy.hpp"
|
||||
#include "game/entities/ship.hpp"
|
||||
|
||||
namespace Systems::Demo {
|
||||
|
||||
// Stages de arranque de cada escenario curado (se ciclan en attract mode).
|
||||
// Elegidos por variedad visual: pinwheels, stars, squares/mix.
|
||||
inline constexpr std::array<std::uint8_t, SceneManager::SceneContext::DEMO_SCENARIO_COUNT>
|
||||
SCENARIO_STAGES = {2, 5, 7};
|
||||
|
||||
// Stage de arranque para el índice de escenario dado.
|
||||
[[nodiscard]] inline auto scenarioStage(std::uint8_t index) -> std::uint8_t {
|
||||
return SCENARIO_STAGES.at(index % SCENARIO_STAGES.size());
|
||||
}
|
||||
|
||||
// Control de la nave para un frame.
|
||||
struct Control {
|
||||
bool left{false};
|
||||
bool right{false};
|
||||
bool thrust{false};
|
||||
bool shoot{false};
|
||||
};
|
||||
|
||||
class DemoPilot {
|
||||
public:
|
||||
[[nodiscard]] auto compute(const Ship& ship,
|
||||
const std::array<Enemy, Constants::MAX_ORNIS>& enemies,
|
||||
const SDL_FRect& play_area,
|
||||
float delta_time) -> Control;
|
||||
|
||||
private:
|
||||
float retarget_timer_{0.0F}; // re-evalúa objetivo cada RETARGET_INTERVAL
|
||||
float fire_cooldown_{0.0F}; // tiempo restante hasta poder disparar
|
||||
float aim_jitter_{0.0F}; // error de apuntado, refrescado al retargetar
|
||||
int target_idx_{-1}; // índice del enemigo objetivo (o -1)
|
||||
};
|
||||
|
||||
} // namespace Systems::Demo
|
||||
Reference in New Issue
Block a user