XGravity/Public/ecma/Models/ExplosionsModel.ecma.js

272 lines
10 KiB
JavaScript

"use strict";
const ExplosionsModel = (function(){
const ExplosionsModel = function(xgravity){
const self = this;
let particles = [];
let shockwaves = [];
let sparks = [];
this.add_sparks = (cell_x, cell_y, normal_x, normal_y, intensity = 1.0) => {
// A mayor intensidad de impacto, más chispas y más velocidad
const base_count = Math.floor(10 + (intensity * 25));
const count = Math.min(60, base_count);
const base_angle = Math.atan2(normal_y, normal_x);
for(let i = 0; i < count; i++){
// Cono de dispersión reflejado con +- 60 grados
const spread = (Math.random() - 0.5) * (Math.PI * 0.7);
const spark_angle = base_angle + spread;
const speed = (Math.random() * 12 + 6) * Math.min(2.5, intensity);
const life = (Math.random() * 0.18) + 0.08;
sparks.push({
x : cell_x,
y : cell_y,
vx : Math.cos(spark_angle) * speed,
vy : Math.sin(spark_angle) * speed,
len : (Math.random() * 0.3) + 0.15,
life : life,
max_life : life,
color : Math.random() > 0.3 ? "#fef08a" : "#f97316" // Chispas blanco-amarillo y naranja
});
}
};
const get_cell_size = (width, height) => {
const cells = xgravity.settings.get("cells", null, 40);
return (width < height ? width : height) / cells;
};
this.add = (cell_x, cell_y, type = "shield") => {
// 1. Delegar el audio al servicio central
if(xgravity.audio){
xgravity.audio.play(type === "shield" ? "shield_explosion" : "hit_explosion");
}
const is_shield = type === "shield";
// 2. Extraer parámetros visuales de Settings
const particles_range = xgravity.settings.get("explosion_particles_range", null, [45, 75]);
const spread_reach = xgravity.settings.get("explosion_particles_spread", null, 11.0);
const base_shockwave_radius = xgravity.settings.get("explosion_shockwave_radius", null, 4.5);
const min_p = Math.min(particles_range[0], particles_range[1]);
const max_p = Math.max(particles_range[0], particles_range[1]);
const count = Math.floor(min_p + Math.random() * (max_p - min_p + 1));
const max_radius = is_shield ? base_shockwave_radius : (base_shockwave_radius * 1.15);
// 3. Onda expansiva elíptica con inclinación aleatoria
shockwaves.push({
x : cell_x,
y : cell_y,
radius : 0.4,
max_radius : max_radius,
speed : is_shield ? 9.5 : 8.0,
angle : Math.random() * Math.PI * 2,
aspect_ratio : 0.42 + (Math.random() * 0.18),
alpha : 1.0,
color_rgb : is_shield ? "56, 189, 248" : "239, 68, 68"
});
// 4. Micropartículas esféricas difuminadas
for(let i = 0; i < count; i++){
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * spread_reach) + 1.2;
const life = (Math.random() * 0.38) + 0.28;
let core_rgb = "255, 255, 255";
let rim_rgb = "56, 189, 248";
if(is_shield){
rim_rgb = Math.random() > 0.4 ? "56, 189, 248" : (Math.random() > 0.5 ? "186, 230, 253" : "2, 132, 199");
} else {
rim_rgb = Math.random() > 0.4 ? "239, 68, 68" : (Math.random() > 0.5 ? "249, 115, 22" : "251, 191, 36");
}
particles.push({
x : cell_x,
y : cell_y,
vx : Math.cos(angle) * speed,
vy : Math.sin(angle) * speed,
size : (Math.random() * 0.16) + 0.08,
life : life,
max_life : life,
core_rgb : core_rgb,
rim_rgb : rim_rgb
});
}
};
this.add_mothership_impact = (cx, cy) => {
// Flash de impacto en la cabina
particles.push({
type : "screen_flash",
alpha : 0.85,
color : "#ffffff",
fade_speed : 2.5
});
// 60-80 Fragmentos de roca y metralla de diversos tamaños
const count = 75;
for(let i = 0; i < count; i++){
const angle = Math.random() * Math.PI * 2;
const speed = 120 + (Math.random() * 650); // Píxeles/segundo
const size = 3 + (Math.random() * 18); // Desde gravilla hasta peñascos
const is_rock = Math.random() > 0.3;
particles.push({
type : is_rock ? "rock_shard" : "plasma_spark",
x : cx + ((Math.random() - 0.5) * 40),
y : cy + ((Math.random() - 0.5) * 40),
vx : Math.cos(angle) * speed,
vy : Math.sin(angle) * speed,
angle : Math.random() * Math.PI * 2,
angular_vel : (Math.random() - 0.5) * 12.0,
size : size,
life : 1.0,
decay : 0.45 + (Math.random() * 0.75),
color : is_rock ? (Math.random() > 0.5 ? "#78716c" : "#a8a29e") : (Math.random() > 0.5 ? "#f97316" : "#facc15"),
vertices : is_rock ? [
{ x: -size * 0.5, y: -size * 0.4 },
{ x: size * 0.6, y: -size * 0.3 },
{ x: size * 0.4, y: size * 0.5 },
{ x: -size * 0.3, y: size * 0.6 }
] : null
});
}
};
this.update = (delta_time) => {
for(let i = particles.length - 1; i >= 0; i--){
const p = particles[i];
p.life -= delta_time;
if(p.life <= 0){
particles.splice(i, 1);
continue;
}
p.x += p.vx * delta_time;
p.y += p.vy * delta_time;
p.vx *= 0.93;
p.vy *= 0.93;
}
for(let i = shockwaves.length - 1; i >= 0; i--){
const s = shockwaves[i];
s.radius += s.speed * delta_time;
s.alpha = Math.max(0, 1.0 - (s.radius / s.max_radius));
if(s.radius >= s.max_radius){
shockwaves.splice(i, 1);
}
}
for(let i = sparks.length - 1; i >= 0; i--){
const s = sparks[i];
s.life -= delta_time;
if(s.life <= 0){
sparks.splice(i, 1);
continue;
}
s.x += s.vx * delta_time;
s.y += s.vy * delta_time;
s.vx *= 0.90;
s.vy *= 0.90;
}
};
this.render = (ctx, width, height) => {
const cell_px = get_cell_size(width, height);
const cx = width / 2;
const cy = height / 2;
ctx.save();
ctx.globalCompositeOperation = "lighter";
// Ondas de choque elípticas
for(let i = 0; i < shockwaves.length; i++){
const s = shockwaves[i];
const px = cx + (s.x * cell_px);
const py = cy + (s.y * cell_px);
const r_px = s.radius * cell_px;
ctx.save();
ctx.translate(px, py);
ctx.rotate(s.angle);
ctx.scale(1.0, s.aspect_ratio);
ctx.lineWidth = cell_px * 0.14 * s.alpha;
ctx.strokeStyle = `rgba(${s.color_rgb}, ${s.alpha * 0.85})`;
ctx.beginPath();
ctx.arc(0, 0, r_px, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = cell_px * 0.05 * s.alpha;
ctx.strokeStyle = `rgba(255, 255, 255, ${s.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(0, 0, r_px * 0.82, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
// Micropartículas
for(let i = 0; i < particles.length; i++){
const p = particles[i];
const px = cx + (p.x * cell_px);
const py = cy + (p.y * cell_px);
const r_px = p.size * cell_px;
const progress = Math.max(0, p.life / p.max_life);
const grad = ctx.createRadialGradient(px, py, 0, px, py, r_px);
grad.addColorStop(0.0, `rgba(${p.core_rgb}, ${progress})`);
grad.addColorStop(0.45, `rgba(${p.rim_rgb}, ${progress * 0.75})`);
grad.addColorStop(1.0, `rgba(${p.rim_rgb}, 0.0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(px, py, r_px, 0, Math.PI * 2);
ctx.fill();
}
// Renderizado de Chispas Cinéticas (sin delta_time)
for(let i = 0; i < sparks.length; i++){
const s = sparks[i];
const px = cx + (s.x * cell_px);
const py = cy + (s.y * cell_px);
const progress = Math.max(0, s.life / s.max_life);
// Calculamos la longitud y dirección de la estela en base a su velocidad
const spd = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
const dir_x = spd > 0.001 ? s.vx / spd : 0;
const dir_y = spd > 0.001 ? s.vy / spd : 0;
// Longitud visible de la estela en píxeles (usa s.len escalado por la velocidad y vida)
const tail_length_px = (s.len || 0.25) * cell_px * Math.min(2.5, spd * 0.15);
ctx.lineWidth = cell_px * 0.06;
ctx.strokeStyle = s.color;
ctx.globalAlpha = progress;
ctx.beginPath();
ctx.moveTo(px, py);
// La estela se proyecta hacia atrás respecto a la trayectoria
ctx.lineTo(px - (dir_x * tail_length_px), py - (dir_y * tail_length_px));
ctx.stroke();
}
ctx.restore();
};
this.reset = () => {
particles = [];
shockwaves = [];
};
};
return ExplosionsModel;
})();