slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
/**
* 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);
}
}
}