Files
slop-game-opus5.5/app/static/js/systems/Effects.js
T
2026-09-24 17:57:54 +02:00

238 lines
7.4 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.
/**
* Visuelle Rückmeldung: Explosionen, Funken, Rauch, Brandspuren, Mündungsfeuer,
* schwebende Texte und Bildschirmwackeln.
*
* Rückmeldung ist kein Luxus: Ohne Treffer-Aufblitzen und Funken fühlt sich Schießen
* „hohl“ an und man sieht nicht, ob man überhaupt trifft. Sind die Kenney-Partikel
* nicht geladen, fällt alles auf einfache Kreise zurück.
* @module systems/Effects
*/
import { COLORS, FX, GAME } from '../config.js';
import { UI } from '../ui/theme.js';
import { TEXTURE_KEYS } from './Placeholders.js';
/** Kenney-Partikel sind 128 px groß; auf diese Größen wird skaliert. */
const PARTICLE_SOURCE_SIZE = 128;
/** Höchstzahl gleichzeitig sichtbarer Brandspuren (älteste verschwinden zuerst). */
const MAX_SCORCH_MARKS = 40;
const SCORCH_LIFETIME_MS = 20000;
const MUZZLE_FLASH_MS = 70;
export class Effects {
/** @param {Phaser.Scene} scene */
constructor(scene) {
this.scene = scene;
const has = (key) => scene.textures.exists(key);
this.hasKenney = has('fx-spark') && has('fx-smoke') && has('fx-fire');
/** @type {Phaser.GameObjects.Image[]} */
this.scorchMarks = [];
// Gemeinsame Emitter: deutlich günstiger als ein Emitter pro Treffer.
const sparkKey = has('fx-spark') ? 'fx-spark' : TEXTURE_KEYS.particle;
const sparkScale = has('fx-spark') ? 14 / PARTICLE_SOURCE_SIZE : 0.9;
this.sparks = scene.add.particles(0, 0, sparkKey, {
lifespan: 380,
speed: { min: 80, max: 280 },
scale: { start: sparkScale, end: 0 },
alpha: { start: 1, end: 0 },
rotate: { min: 0, max: 360 },
blendMode: 'ADD',
emitting: false,
});
this.sparks.setDepth(GAME.depth.effects);
if (this.hasKenney) {
this.fire = scene.add.particles(0, 0, 'fx-fire', {
lifespan: 450,
speed: { min: 20, max: 140 },
scale: { start: 0.35, end: 0 },
alpha: { start: 0.9, end: 0 },
rotate: { min: 0, max: 360 },
blendMode: 'ADD',
emitting: false,
});
this.smoke = scene.add.particles(0, 0, 'fx-smoke', {
lifespan: 1100,
speed: { min: 10, max: 60 },
scale: { start: 0.3, end: 0.9 },
alpha: { start: 0.45, end: 0 },
rotate: { min: 0, max: 360 },
tint: 0x555555,
emitting: false,
});
this.fire.setDepth(GAME.depth.effects);
this.smoke.setDepth(GAME.depth.effects - 1);
}
}
/**
* Funkenregen an einer Stelle.
* @param {number} x
* @param {number} y
* @param {number} color
* @param {number} [count]
*/
burst(x, y, color, count = 8) {
this.sparks.setParticleTint(color);
this.sparks.explode(count, x, y);
}
/**
* Explosion mit Feuer, Rauch, Lichtblitz und Brandspur. Der Ring zeigt den
* tatsächlichen Wirkungsradius, damit Spieler lernen, wie weit Schaden reicht.
* @param {number} x
* @param {number} y
* @param {number} radius
* @param {number} color
*/
explosion(x, y, radius, color) {
const ring = this.scene.add.circle(x, y, radius, color, 0.22).setDepth(GAME.depth.effects);
ring.setStrokeStyle(4, color, 0.9);
ring.setScale(0.15);
this.scene.tweens.add({
targets: ring,
scale: 1,
alpha: 0,
duration: 380,
ease: 'Cubic.easeOut',
onComplete: () => ring.destroy(),
});
if (this.hasKenney) {
const amount = Math.min(28, Math.round(radius / 6));
this.fire.explode(amount, x, y);
this.smoke.explode(Math.ceil(amount / 2), x, y);
this.glow(x, y, radius * 1.6, color);
this.scorch(x, y, radius);
}
this.burst(x, y, color, Math.min(40, Math.round(radius / 5)));
this.burst(x, y, COLORS.cream, 8);
this.shake('explosion');
}
/** Kurzer, additiver Lichtblitz. */
glow(x, y, size, color) {
if (!this.scene.textures.exists('fx-glow')) return;
const flash = this.scene.add
.image(x, y, 'fx-glow')
.setDisplaySize(size, size)
.setTint(color)
.setBlendMode(Phaser.BlendModes.ADD)
.setDepth(GAME.depth.effects);
this.scene.tweens.add({ targets: flash, alpha: 0, duration: 260, onComplete: () => flash.destroy() });
}
/** Brandspur auf dem Boden, die langsam verblasst. */
scorch(x, y, radius) {
if (!this.scene.textures.exists('fx-scorch')) return;
const mark = this.scene.add
.image(x, y, 'fx-scorch')
.setDisplaySize(radius * 2.2, radius * 2.2)
.setAngle(Math.random() * 360)
.setTint(0x1a1410)
.setAlpha(0.55)
.setDepth(GAME.depth.ground + 2);
this.scorchMarks.push(mark);
if (this.scorchMarks.length > MAX_SCORCH_MARKS) this.scorchMarks.shift()?.destroy();
this.scene.tweens.add({
targets: mark,
alpha: 0,
delay: SCORCH_LIFETIME_MS,
duration: 2000,
onComplete: () => {
this.scorchMarks = this.scorchMarks.filter((m) => m !== mark);
mark.destroy();
},
});
}
/**
* Aufsteigender, verblassender Text (Punkte, Meldungen am Ort).
* @param {number} x
* @param {number} y
* @param {string} text
* @param {string} [color]
* @param {number} [fontSize]
*/
floatingText(x, y, text, color = '#f2c14e', fontSize = 18) {
const label = this.scene.add
.text(x, y, text, {
fontFamily: UI.uiFont,
fontStyle: 'bold',
fontSize: `${fontSize}px`,
color,
stroke: '#0d1321',
strokeThickness: 4,
})
.setOrigin(0.5)
.setDepth(GAME.depth.effects);
this.scene.tweens.add({
targets: label,
y: y - 36,
alpha: 0,
duration: FX.damageNumberMs,
ease: 'Quad.easeOut',
onComplete: () => label.destroy(),
});
}
/**
* Kamera wackeln lassen.
* @param {'hit'|'explosion'} kind
*/
shake(kind) {
const cfg = FX.screenShake[kind];
this.scene.cameras.main.shake(cfg.durationMs, cfg.intensity);
}
/**
* Lässt ein Sprite kurz weiß aufblitzen (Treffer-Feedback).
* @param {Phaser.GameObjects.Sprite} sprite
*/
flash(sprite) {
sprite.setTintFill(0xffffff);
this.scene.time.delayedCall(FX.hitFlashMs, () => {
if (sprite.active) sprite.clearTint();
});
}
/**
* Nachbild einer Figur (Ausweichrolle): verdeutlicht Richtung und Tempo der Bewegung.
* @param {Phaser.GameObjects.Sprite} sprite
*/
afterimage(sprite) {
const ghost = this.scene.add
.image(sprite.x, sprite.y, sprite.texture.key)
.setRotation(sprite.rotation)
.setAlpha(0.45)
.setTint(COLORS.gold)
.setDepth(sprite.depth - 1);
this.scene.tweens.add({ targets: ghost, alpha: 0, scale: 1.15, duration: 260, onComplete: () => ghost.destroy() });
}
/**
* Mündungsfeuer an der Waffenspitze.
* @param {number} x
* @param {number} y
* @param {number} angle Schussrichtung (Bogenmaß).
* @param {number} [color]
*/
muzzle(x, y, angle, color = COLORS.playerBullet) {
if (this.scene.textures.exists('fx-muzzle')) {
// Die Kenney-Grafik zeigt nach oben – um 90° drehen, damit sie in Schussrichtung zeigt.
const flash = this.scene.add
.image(x, y, 'fx-muzzle')
.setOrigin(0.5, 0.9)
.setRotation(angle + Math.PI / 2)
.setScale(0.26)
.setTint(color)
.setBlendMode(Phaser.BlendModes.ADD)
.setDepth(GAME.depth.effects);
this.scene.tweens.add({ targets: flash, alpha: 0, duration: MUZZLE_FLASH_MS, onComplete: () => flash.destroy() });
} else {
this.burst(x, y, color, 3);
}
}
}