import Phaser from '../vendor/phaser.js'; import { toColor } from '../utils/Color.js'; import { canvasTexture } from '../utils/Textures.js'; const DOT_KEY = '__combat_dot'; const DOT_SIZE = 64; function drawSoftDot(ctx, size) { const c = size / 2; const g = ctx.createRadialGradient(c, c, 0, c, c, c); g.addColorStop(0.0, 'rgba(255,255,255,1.0)'); g.addColorStop(0.22, 'rgba(255,255,255,0.85)'); g.addColorStop(0.55, 'rgba(255,255,255,0.26)'); g.addColorStop(1.0, 'rgba(255,255,255,0.0)'); ctx.fillStyle = g; ctx.fillRect(0, 0, size, size); } /** * Small hand-rolled FX for the combat range: muzzle flashes, hit glows, * spark streams, and the ship-kill burst. Plain additive images stepped * in update() — deliberately NOT the particles emitter (this build's * emitter velocity semantics are fiddly, see the note in * js/visuals/EngineFlare.js), so the look is deterministic, cheap, and * needs no tween stepping. * * All items are world-space (the arena camera is fixed), depths 19–21 * (above ships (10), under the HUD (100)). */ export class Fx { /** @param {object} scene the combat scene */ constructor(scene) { this.scene = scene; this.items = []; // { img, t0, life, kind, ... } Fx.ensureDot(scene); } /** The shared white soft dot (tint reads as its color). @returns {string} the key */ static ensureDot(scene) { if (scene.textures.exists(DOT_KEY)) return DOT_KEY; canvasTexture(scene, DOT_KEY, DOT_SIZE, DOT_SIZE, (ctx, w, h) => drawSoftDot(ctx, Math.min(w, h))); return DOT_KEY; } /** An additive glow: `size` px across at full strength, fading over `life` ms. */ flash(x, y, color, size = 40, life = 220) { const img = this.scene .add.image(x, y, DOT_KEY) .setTint(toColor(color, 0xffffff)) .setBlendMode(Phaser.BlendModes.ADD) .setDepth(20); this.items.push({ img, t0: 0, life, kind: 'flash', grow: size / DOT_SIZE }); return img; } /** A growing, fading disc — the shockwave read (a stand-in for a stroke ring). */ ring(x, y, color, size = 180, life = 450) { const img = this.scene .add.image(x, y, DOT_KEY) .setTint(toColor(color, 0xffffff)) .setBlendMode(Phaser.BlendModes.ADD) .setDepth(19) .setScale(6 / DOT_SIZE) .setAlpha(0.8); this.items.push({ img, t0: 0, life, kind: 'ring', from: 6 / DOT_SIZE, to: size / DOT_SIZE }); return img; } /** A burst of sparks (small dots on random headings, decaying). */ sparks(x, y, color, count = 12, speed = 220, life = 500) { for (let i = 0; i < count; i++) { const a = Math.random() * Math.PI * 2; const sp = speed * (0.4 + Math.random() * 0.8); const s = 4 + Math.random() * 5; const img = this.scene .add.image(x, y, DOT_KEY) .setTint(toColor(color, 0xffffff)) .setBlendMode(Phaser.BlendModes.ADD) .setDepth(21) .setScale(s / DOT_SIZE); this.items.push({ img, t0: 0, life: life * (0.6 + Math.random() * 0.6), kind: 'spark', vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, }); } } /** The big one — a ship's death: core flash + colored bloom + wave + sparks. */ explode(x, y, color) { this.flash(x, y, 0xffffff, 90, 160); this.flash(x, y, color, 150, 420); this.ring(x, y, color, 200, 480); this.sparks(x, y, color, 22, 300, 700); } /** Step every live item (ms). Dead ones are destroyed. */ update(deltaMs) { const dt = deltaMs / 1000; for (let i = this.items.length - 1; i >= 0; i--) { const it = this.items[i]; it.t0 += deltaMs; const t = Math.min(1, it.t0 / it.life); if (t >= 1) { it.img.destroy(); this.items.splice(i, 1); continue; } if (it.kind === 'flash') { it.img.setScale(it.grow * (0.6 + 0.4 * t)); it.img.setAlpha(1 - t); } else if (it.kind === 'ring') { it.img.setScale(it.from + (it.to - it.from) * t); it.img.setAlpha(0.8 * (1 - t)); } else if (it.kind === 'spark') { it.img.x += it.vx * dt; it.img.y += it.vy * dt; const decay = Math.max(0, 1 - 2.2 * dt); it.vx *= decay; it.vy *= decay; it.img.setAlpha(1 - t); } } } /** Drop everything (a range reset). */ clear() { for (const it of this.items) it.img.destroy(); this.items.length = 0; } }