442 lines
16 KiB
JavaScript
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;
|
|
})(); |