"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; } }; })();