XGravity/Public/ecma/Models/DroneModel.ecma.js

442 lines
16 KiB
JavaScript

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