135 lines
4.3 KiB
JavaScript
135 lines
4.3 KiB
JavaScript
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;
|
||
}
|
||
}
|