orbit/js/combat/Fx.js

135 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}