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