XGravity/Public/ecma/Strategies/BehaviorStrategies.ecma.js

327 lines
15 KiB
JavaScript

"use strict";
const BehaviorStrategies = (function(){
const STRATEGIES = {
// 1. PERSECUCIÓN DEL JUGADOR CON ROTACIÓN Y ACELERACIÓN SUAVES
track_drone : (enemy, delta_time, drone_info) => {
if(drone_info){
const dx = drone_info.cell_x - enemy.x;
const dy = drone_info.cell_y - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const accel = enemy.config.accel || 3.5;
if(dist > 0.1){
enemy.vx += (dx / dist) * accel * delta_time;
enemy.vy += (dy / dist) * accel * delta_time;
}
const max_speed = enemy.config.max_speed || 5.2;
const spd = Math.sqrt(enemy.vx * enemy.vx + enemy.vy * enemy.vy);
if(spd > max_speed){
enemy.vx = (enemy.vx / spd) * max_speed;
enemy.vy = (enemy.vy / spd) * max_speed;
}
}
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// ROTACIÓN PROGRESIVA Y SUAVE (Giro de timón realista)
const speed_sq = enemy.vx * enemy.vx + enemy.vy * enemy.vy;
if(speed_sq > 0.05){
const target_angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
let diff = target_angle - enemy.angle;
while(diff < -Math.PI) diff += Math.PI * 2;
while(diff > Math.PI) diff -= Math.PI * 2;
const turn_speed = enemy.config.turn_speed || 4.2; // Radianes/segundo
const max_step = turn_speed * delta_time;
if(Math.abs(diff) <= max_step){
enemy.angle = target_angle;
} else {
enemy.angle += Math.sign(diff) * max_step;
}
}
},
// 2. ESTÁTICO CON ROTACIÓN Y DERIVA INERCIAL
static : (enemy, delta_time) => {
enemy.angle += 0.5 * delta_time;
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
enemy.vx *= 0.98;
enemy.vy *= 0.98;
},
// 3. REBOTE EN LOS MÁRGENES DE LA CABINA
// 3. PATRULLA CON FRENADO EN BORDES, GIRO SUAVE Y ACELERACIÓN
bounce_screen : (enemy, delta_time) => {
// Inicialización de estado interno de maniobra
if(!enemy.maneuver_state){
enemy.maneuver_state = {
phase : "cruising", // "cruising" | "braking_and_turning" | "accelerating"
target_vx : enemy.vx || 3.5,
target_vy : enemy.vy || 3.5,
target_angle : enemy.angle || 0,
cruise_speed : Math.sqrt((enemy.vx * enemy.vx) + (enemy.vy * enemy.vy)) || 4.2
};
}
const m = enemy.maneuver_state;
const boundary_limit = 15.5; // Distancia límite en celdas desde el centro
const turn_speed = enemy.config.turn_speed || 4.5; // Velocidad de giro en rad/s
// FASE 1: VUELO DE CRUCERO
if(m.phase === "cruising"){
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// Comprobamos si rebasa el margen perimetral
const hit_x = (enemy.x > boundary_limit && enemy.vx > 0) || (enemy.x < -boundary_limit && enemy.vx < 0);
const hit_y = (enemy.y > boundary_limit && enemy.vy > 0) || (enemy.y < -boundary_limit && enemy.vy < 0);
if(hit_x || hit_y){
// Calculamos la nueva dirección vectorial reflejada
const new_dir_x = hit_x ? -Math.sign(enemy.vx) : (enemy.vx > 0 ? 1 : -1);
const new_dir_y = hit_y ? -Math.sign(enemy.vy) : (enemy.vy > 0 ? 1 : -1);
// Pequeña variación angular aleatoria para que no rebote siempre en el mismo bucle
const jitter = (Math.random() - 0.5) * 0.4;
const base_angle = Math.atan2(new_dir_y, new_dir_x) + jitter;
m.target_vx = Math.cos(base_angle) * m.cruise_speed;
m.target_vy = Math.sin(base_angle) * m.cruise_speed;
m.target_angle = base_angle + (Math.PI / 2);
m.phase = "braking_and_turning";
} else {
// Mantiene la orientación alineada con la dirección de avance
if(Math.abs(enemy.vx) > 0.05 || Math.abs(enemy.vy) > 0.05){
enemy.angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
}
}
}
// FASE 2: FRENADO Y GIRO SUAVE EN EL MARGEN
else if(m.phase === "braking_and_turning"){
// Desaceleración progresiva (retrocohetes)
enemy.vx *= Math.pow(0.08, delta_time);
enemy.vy *= Math.pow(0.08, delta_time);
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// Viraje suave hacia el nuevo ángulo objetivo
let diff = m.target_angle - enemy.angle;
while(diff < -Math.PI) diff += Math.PI * 2;
while(diff > Math.PI) diff -= Math.PI * 2;
const max_step = turn_speed * delta_time;
if(Math.abs(diff) <= max_step){
enemy.angle = m.target_angle;
} else {
enemy.angle += Math.sign(diff) * max_step;
}
const current_speed = Math.sqrt((enemy.vx * enemy.vx) + (enemy.vy * enemy.vy));
// Si ha frenado a casi cero y ya completó el giro, iniciamos aceleración
if(current_speed < 0.35 && Math.abs(diff) < 0.25){
enemy.vx = 0;
enemy.vy = 0;
m.phase = "accelerating";
}
}
// FASE 3: ACELERACIÓN HACIA LA NUEVA DIRECCIÓN
else if(m.phase === "accelerating"){
const forward_angle = enemy.angle - (Math.PI / 2);
const accel_rate = enemy.config.accel || 5.0; // Tasa de aceleración
enemy.vx += Math.cos(forward_angle) * accel_rate * delta_time;
enemy.vy += Math.sin(forward_angle) * accel_rate * delta_time;
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
const current_speed = Math.sqrt((enemy.vx * enemy.vx) + (enemy.vy * enemy.vy));
// Al alcanzar la velocidad de crucero, vuelve al ciclo normal
if(current_speed >= m.cruise_speed){
const factor = m.cruise_speed / current_speed;
enemy.vx *= factor;
enemy.vy *= factor;
m.phase = "cruising";
}
}
},
// 4. BALÍSTICO / CRUCE DIRECTO
cross_screen : (enemy, delta_time) => {
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
enemy.angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
const dist_center = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(enemy.age > 1.5 && dist_center > 24.0){
enemy.alive = false;
}
},
// 5. IA LANZADOR DE ASTEROIDES (Confinado en el área jugable)
thrower_ai : (enemy, delta_time, drone_info, all_enemies) => {
const turn_speed = enemy.config.turn_speed || 4.2;
const cruise_speed = enemy.config.max_speed || 4.8;
const flee_distance = enemy.config.flee_distance || 7.5;
const max_patrol_radius = 16.5; // Radio límite donde el Dron siempre puede alcanzarlo
let target_vx = enemy.vx;
let target_vy = enemy.vy;
// 1. EVALUAR AMENAZA DEL DRON (Prioridad 1: Huir sin salirse del mapa)
let is_fleeing = false;
if(drone_info){
const dx_drone = enemy.x - drone_info.cell_x;
const dy_drone = enemy.y - drone_info.cell_y;
const dist_drone = Math.sqrt(dx_drone * dx_drone + dy_drone * dy_drone);
if(dist_drone < flee_distance && dist_drone > 0.05){
is_fleeing = true;
// Dirección de alejamiento puro
let flee_x = dx_drone / dist_drone;
let flee_y = dy_drone / dist_drone;
// Si está cerca del borde, no puede seguir retrocediendo: se desvía en ángulo tangente
const dist_center = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(dist_center > 13.5){
// Vector hacia el centro
const to_center_x = -enemy.x / dist_center;
const to_center_y = -enemy.y / dist_center;
// Mezcla huida con escape lateral (tangente) para rodear al dron hacia el centro
const tangent_x = -flee_y;
const tangent_y = flee_x;
// Elegir el sentido tangencial que más se acerque al centro
const dot = (tangent_x * to_center_x) + (tangent_y * to_center_y);
const side = dot >= 0 ? 1 : -1;
flee_x = (flee_x * 0.3) + (tangent_x * side * 0.7) + (to_center_x * 0.5);
flee_y = (flee_y * 0.3) + (tangent_y * side * 0.7) + (to_center_y * 0.5);
const norm = Math.sqrt(flee_x * flee_x + flee_y * flee_y) || 1;
flee_x /= norm;
flee_y /= norm;
}
target_vx = flee_x * cruise_speed * 1.2;
target_vy = flee_y * cruise_speed * 1.2;
}
}
// 2. SI NO ESTÁ HUYENDO: BUSCAR Y DISPARAR ASTEROIDES
if(!is_fleeing){
let closest_ast = null;
let min_ast_dist = Infinity;
if(Array.isArray(all_enemies)){
for(let i = 0; i < all_enemies.length; i++){
const other = all_enemies[i];
if(other && other.is_alive() && other.is_asteroid && !other.is_targeted_towards_mothership()){
const st = other.get_state();
// Ignora asteroides que ya se hayan salido
const d_center = Math.sqrt(st.x * st.x + st.y * st.y);
if(d_center > 18.5) continue;
const dx = st.x - enemy.x;
const dy = st.y - enemy.y;
const d = Math.sqrt(dx * dx + dy * dy);
if(d < min_ast_dist){
min_ast_dist = d;
closest_ast = other;
}
}
}
}
if(closest_ast){
const ast_st = closest_ast.get_state();
const dx = ast_st.x - enemy.x;
const dy = ast_st.y - enemy.y;
// Si está pegado al asteroide: lo dispara hacia el centro
if(min_ast_dist < 2.2){
const to_center_dist = Math.sqrt(ast_st.x * ast_st.x + ast_st.y * ast_st.y);
if(to_center_dist > 0.1){
const launch_speed = 7.5;
const launch_vx = -(ast_st.x / to_center_dist) * launch_speed;
const launch_vy = -(ast_st.y / to_center_dist) * launch_speed;
closest_ast.launch_towards_mothership(launch_vx, launch_vy);
}
// Rebote inercial tras el tiro
target_vx = (dx / (min_ast_dist || 1)) * -cruise_speed;
target_vy = (dy / (min_ast_dist || 1)) * -cruise_speed;
} else {
target_vx = (dx / min_ast_dist) * cruise_speed;
target_vy = (dy / min_ast_dist) * cruise_speed;
}
} else {
// Si no hay asteroides y está muy lejos, patrulla de vuelta hacia el radio seguro
const cur_dist = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(cur_dist > 14.0){
target_vx = (-enemy.x / cur_dist) * (cruise_speed * 0.7);
target_vy = (-enemy.y / cur_dist) * (cruise_speed * 0.7);
} else {
enemy.angle += 0.8 * delta_time;
}
}
}
// Aplicación de aceleración
const accel = 4.2;
enemy.vx += (target_vx - enemy.vx) * Math.min(1.0, accel * delta_time);
enemy.vy += (target_vy - enemy.vy) * Math.min(1.0, accel * delta_time);
enemy.x += enemy.vx * delta_time;
enemy.y += enemy.vy * delta_time;
// Freno y clamp perimetral absoluto: el lanzador NUNCA puede rebasar las 17.0 celdas
const dist_edge = Math.sqrt(enemy.x * enemy.x + enemy.y * enemy.y);
if(dist_edge > max_patrol_radius){
const factor = max_patrol_radius / dist_edge;
enemy.x *= factor;
enemy.y *= factor;
// Anular componente de velocidad que lo empuja hacia afuera
const dot_out = (enemy.vx * (enemy.x / max_patrol_radius)) + (enemy.vy * (enemy.y / max_patrol_radius));
if(dot_out > 0){
enemy.vx -= dot_out * (enemy.x / max_patrol_radius);
enemy.vy -= dot_out * (enemy.y / max_patrol_radius);
}
}
// Giro hacia donde avanza
const spd = Math.sqrt(enemy.vx * enemy.vx + enemy.vy * enemy.vy);
if(spd > 0.1){
const target_angle = Math.atan2(enemy.vy, enemy.vx) + (Math.PI / 2);
let diff = target_angle - enemy.angle;
while(diff < -Math.PI) diff += Math.PI * 2;
while(diff > Math.PI) diff -= Math.PI * 2;
const max_step = turn_speed * delta_time;
enemy.angle += Math.sign(diff) * Math.min(Math.abs(diff), max_step);
}
},
};
return {
get : (name) => STRATEGIES[name] || STRATEGIES.static,
register : (name, strategy) => { STRATEGIES[name] = strategy; }
};
})();