"use strict"; const DroneModel = (function(){ const DroneModel = function(xgravity){ const self = this; let image = null; let is_loaded = false; // Posición y velocidad en CELDAS relativas al centro let cell_x = 0; let cell_y = 0; let vel_cell_x = 0; let vel_cell_y = 0; let angle = 0; // Objetivo en CELDAS let target_cell_x = 0; let target_cell_y = 0; let has_cursor = false; // Animación cinemática de Despliegue / Lanzamiento let is_deploying = true; let deploy_timer = 0; const deploy_duration = 1.25; // Duración en segundos de la transición const start_scale = 10.0; // Tamaño inicial x10 emergiendo de cabina let current_scale = start_scale; let deploy_start_x = 0; let deploy_start_y = 24.0; // Emerge por debajo de la pantalla let deploy_target_x = 0; let deploy_target_y = 0; // Estado del Escudo let shield = 100; const max_shield = xgravity.settings.get_effective_stat("base_drone_shield_max", "shield_capacity_mult"); let shield_energy = max_shield; let shield_active = false; let shield_pulse_timer = 0; let is_critical = false; let beacon_angle = 0; let alarm_audio_timer = 0; // Estados dinámicos de alerta let shield_risk = 0.0; // 0.0 = seguro, 1.0 = colapso inminente let perimeter_risk = 0.0; // 0.0 = seguro, 1.0 = desintegración perimetral let shield_alarm_timer = 0; let perimeter_alarm_timer = 0; this.is_deploying = () => is_deploying; this.get_perimeter_risk = () => perimeter_risk; this.get_shield_risk = () => shield_risk; this.get_mass = () => xgravity.settings.get("drone_mass", null, 3.0); this.get_shield_percentage = () => Math.max(0, shield_energy / max_shield); this.apply_impulse = (impulse_vx, impulse_vy) => { if(is_deploying) return; vel_cell_x += impulse_vx; vel_cell_y += impulse_vy; }; this.set_cell_position = (nx, ny) => { cell_x = nx; cell_y = ny; }; this.restore_shield = (amount) => { if(amount > 0){ shield = Math.min(max_shield, shield + amount); } }; const get_cell_size = (width, height) => { const cells = xgravity.settings.get("cells", null, 40); return (width < height ? width : height) / cells; }; this.set_shield_active = (state) => { if(is_deploying) return; // Solo se puede encender si le queda energía disponible if(state && shield <= 0){ shield_active = false; return; } shield_active = Boolean(state); }; this.is_shield_active = () => !is_deploying && shield_active && shield > 0; this.set_target_pixels = (pixel_x, pixel_y, canvas_w, canvas_h) => { const cell_px = get_cell_size(canvas_w, canvas_h); const center_x = canvas_w / 2; const center_y = canvas_h / 2; target_cell_x = (pixel_x - center_x) / cell_px; target_cell_y = (pixel_y - center_y) / cell_px; has_cursor = true; }; this.start_deployment = () => { is_deploying = true; deploy_timer = 0; current_scale = start_scale; // Aterrizaje aleatorio en la zona central deploy_target_x = (Math.random() - 0.5) * 6.0; // Entre -3 y +3 celdas deploy_target_y = (Math.random() - 0.5) * 5.0; // Entre -2.5 y +2.5 celdas // Punto de despegue: parte inferior de la pantalla deploy_start_x = deploy_target_x * 0.35; deploy_start_y = 23.5; cell_x = deploy_start_x; cell_y = deploy_start_y; vel_cell_x = 0; vel_cell_y = 0; // Apuntar morro en la dirección de eyección (hacia arriba) angle = Math.atan2(deploy_target_y - deploy_start_y, deploy_target_x - deploy_start_x) + (Math.PI / 2); }; this.init_position_cells = (cx = 0, cy = 0) => { this.start_deployment(); }; this.init_position = (x, y) => { this.start_deployment(); }; this.get_position = () => { return { cell_x : cell_x, cell_y : cell_y, vel_cell_x : vel_cell_x, vel_cell_y : vel_cell_y, target_cell_x : target_cell_x, target_cell_y : target_cell_y, angle : angle, shield : shield, max_shield : max_shield, is_shield_active : !is_deploying && shield_active && shield > 0, is_deploying : is_deploying, scale : current_scale }; }; this.damage_shield = (amount) => { if(is_deploying) return; shield = Math.max(0, shield - amount); if(shield <= 0) shield_active = false; }; this.update = (delta_time) => { // ---------------------------------------------------- // FASE 1: ANIMACIÓN DE LANZAMIENTO DESDE CABINA // ---------------------------------------------------- if(is_deploying){ deploy_timer += delta_time; const t = Math.min(1.0, deploy_timer / deploy_duration); // Desaceleración suave (Cubic Ease-Out) const ease = 1.0 - Math.pow(1.0 - t, 3); // Interpolar posición y escala (de x10 a x1.0) cell_x = deploy_start_x + ((deploy_target_x - deploy_start_x) * ease); cell_y = deploy_start_y + ((deploy_target_y - deploy_start_y) * ease); current_scale = start_scale + ((1.0 - start_scale) * ease); angle = Math.atan2(deploy_target_y - deploy_start_y, deploy_target_x - deploy_start_x) + (Math.PI / 2); if(t >= 1.0){ is_deploying = false; current_scale = 1.0; cell_x = deploy_target_x; cell_y = deploy_target_y; vel_cell_x = 0; vel_cell_y = 0; if(!has_cursor){ target_cell_x = deploy_target_x; target_cell_y = deploy_target_y; } } return; // Bloquea consumo de escudo y avisos durante el despegue } // ---------------------------------------------------- // FASE 2: CONTROL Y FÍSICAS DE JUEGO NORMAL // ---------------------------------------------------- if(!has_cursor){ target_cell_x = 0; target_cell_y = 0; } // Dinámica del Escudo (Consumo y Recarga) shield_pulse_timer += delta_time; const drain_rate = xgravity.settings.get("drone_shield_drain", null, 25); const recharge_time = Math.max(0.1, xgravity.settings.get("drone_shield_recharge_time", null, 8.0)); const recovery_rate = max_shield / recharge_time; const shield_pct = (shield / max_shield) * 100; const shield_th = xgravity.settings.get("drone_shield_low_threshold", null, 30); if(shield_active){ shield = Math.max(0, shield - (drain_rate * delta_time)); if(shield <= 0){ shield_active = false; } } else { shield = Math.min(max_shield, shield + (recovery_rate * delta_time)); } // Físicas orbitales hacia el cursor const dcx = target_cell_x - cell_x; const dcy = target_cell_y - cell_y; angle = Math.atan2(dcy, dcx) + (Math.PI / 2); const f = xgravity.settings.get("drone_force", null, 3.0); const [new_vx, new_vy] = DroneModel.drone_velocity( target_cell_x, target_cell_y, cell_x, cell_y, vel_cell_x, vel_cell_y, f, delta_time ); if(shield_pct <= shield_th){ shield_risk = (shield_th - shield_pct) / shield_th; is_critical = true; } else { shield_risk = 0.0; is_critical = false; } // Alarma de escudo progresiva if(shield_risk > 0.0){ const beacon_speed = 5.0 + (shield_risk * 10.0); beacon_angle += beacon_speed * delta_time; const max_int = xgravity.settings.get("drone_shield_alarm_interval_max", null, 0.65); const min_int = xgravity.settings.get("drone_shield_alarm_interval_min", null, 0.18); const current_interval = max_int - (shield_risk * (max_int - min_int)); const current_vol = 0.2 + (shield_risk * 0.8); shield_alarm_timer += delta_time; if(shield_alarm_timer >= current_interval){ shield_alarm_timer = 0; if(xgravity.audio){ xgravity.audio.play("shield_low_alarm", current_vol); } } } else { shield_alarm_timer = 0; } // Alarma de perímetro const dist_center = Math.sqrt(cell_x * cell_x + cell_y * cell_y); const safe_r = xgravity.settings.get("drone_perimeter_safe_radius", null, 15.0); const crit_r = xgravity.settings.get("drone_perimeter_critical_radius", null, 18.5); if(dist_center > safe_r){ perimeter_risk = Math.min(1.0, (dist_center - safe_r) / (crit_r - safe_r)); } else { perimeter_risk = 0.0; } if(perimeter_risk > 0.0){ const max_p_int = xgravity.settings.get("drone_perimeter_alarm_interval_max", null, 0.7); const min_p_int = xgravity.settings.get("drone_perimeter_alarm_interval_min", null, 0.2); const p_interval = max_p_int - (perimeter_risk * (max_p_int - min_p_int)); const p_vol = 0.2 + (perimeter_risk * 0.8); perimeter_alarm_timer += delta_time; if(perimeter_alarm_timer >= p_interval){ perimeter_alarm_timer = 0; if(xgravity.audio){ xgravity.audio.play("perimeter_warning", p_vol); } } } else { perimeter_alarm_timer = 0; } vel_cell_x = new_vx; vel_cell_y = new_vy; cell_x += vel_cell_x * delta_time; cell_y += vel_cell_y * delta_time; }; this.render = (ctx, width, height) => { const cell_px = get_cell_size(width, height); const center_x = width / 2; const center_y = height / 2; const px = center_x + (cell_x * cell_px); const py = center_y + (cell_y * cell_px); // Tamaño base del dron escalado según la fase de vuelo (x10 -> x1.0) const drone_cells = xgravity.settings.get("drone_size", null, 2.5); const drone_size_px = drone_cells * cell_px * current_scale; ctx.save(); ctx.translate(px, py); // 1. Burbuja del Escudo (solo en combate normal con escudo encendido) if(!is_deploying && shield_active && shield > 0){ const shield_radius_cells = xgravity.settings.get("drone_shield_radius", null, 1.7); const shield_radius_px = shield_radius_cells * cell_px; const pulse = Math.sin(shield_pulse_timer * 18) * (cell_px * 0.08); const r = shield_radius_px + pulse; ctx.save(); const shield_grad = ctx.createRadialGradient(0, 0, r * 0.5, 0, 0, r); shield_grad.addColorStop(0, "rgba(56, 189, 248, 0.05)"); shield_grad.addColorStop(0.7, "rgba(56, 189, 248, 0.2)"); shield_grad.addColorStop(1, "rgba(56, 189, 248, 0.6)"); ctx.fillStyle = shield_grad; ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.fill(); ctx.lineWidth = cell_px * 0.12; ctx.strokeStyle = "rgba(186, 230, 253, 0.9)"; ctx.shadowColor = "#38bdf8"; ctx.shadowBlur = cell_px * 0.5; ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.stroke(); ctx.lineWidth = cell_px * 0.06; ctx.setLineDash([cell_px * 0.3, cell_px * 0.2]); ctx.strokeStyle = "rgba(56, 189, 248, 0.7)"; ctx.beginPath(); ctx.arc(0, 0, r + (cell_px * 0.2), 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } // 2. Renderizado del Sprite ctx.rotate(angle); if(is_loaded && image){ const ratio = image.naturalWidth / image.naturalHeight; let w = drone_size_px; let h = drone_size_px; if(ratio >= 1){ h = drone_size_px / ratio; } else { w = drone_size_px * ratio; } ctx.drawImage(image, -w / 2, -h / 2, w, h); } else { ctx.fillStyle = "#38bdf8"; ctx.beginPath(); ctx.moveTo(0, -drone_size_px / 2); ctx.lineTo(drone_size_px / 3, drone_size_px / 2); ctx.lineTo(-drone_size_px / 3, drone_size_px / 2); ctx.closePath(); ctx.fill(); } // Girofaro crítico if(!is_deploying && is_critical){ const pulse = 0.5 + Math.sin(beacon_angle * 2) * 0.5; const halo_radius = cell_px * (2.4 + (pulse * 0.6)); ctx.save(); ctx.globalCompositeOperation = "lighter"; const halo = ctx.createRadialGradient(0, 0, cell_px * 0.3, 0, 0, halo_radius); halo.addColorStop(0.0, `rgba(239, 68, 68, ${0.45 + pulse * 0.35})`); halo.addColorStop(0.5, `rgba(220, 38, 38, ${0.2 + pulse * 0.15})`); halo.addColorStop(1.0, "rgba(239, 68, 68, 0.0)"); ctx.fillStyle = halo; ctx.beginPath(); ctx.arc(0, 0, halo_radius, 0, Math.PI * 2); ctx.fill(); ctx.save(); ctx.rotate(beacon_angle); const beam_length = cell_px * 3.6; const beam_grad = ctx.createRadialGradient(0, 0, 0, 0, 0, beam_length); beam_grad.addColorStop(0.0, "rgba(254, 202, 202, 0.7)"); beam_grad.addColorStop(0.4, "rgba(239, 68, 68, 0.45)"); beam_grad.addColorStop(1.0, "rgba(239, 68, 68, 0.0)"); ctx.fillStyle = beam_grad; ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, beam_length, -0.4, 0.4); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, beam_length, Math.PI - 0.4, Math.PI + 0.4); ctx.closePath(); ctx.fill(); ctx.restore(); ctx.restore(); } ctx.restore(); }; const constructor = () => { const src = xgravity.settings.get("drone_image", null, "images/game/Louse.webp"); image = new Image(); image.onload = () => { is_loaded = true; }; image.src = src; // Iniciar la animación de lanzamiento al crear la nave self.start_deployment(); }; constructor(); }; DroneModel.drone_velocity = (px, py, dx, dy, vx, vy, f, delta = 0) => [ vx += (px - dx) * f * (delta > 0 ? delta : 1), vy += (py - dy) * f * (delta > 0 ? delta : 1) ]; return DroneModel; })();