327 lines
15 KiB
JavaScript
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; }
|
|
};
|
|
})(); |