Phase A: stability and performance
A1 Pooled particle system - New ParticleManager owns 9 reusable Phaser emitters created once per scene (jump, explosion, powerup, puff, sparks, flame, wind, spring, speedline) - BootScene generates reusable white textures (px_square/soft/streak/ring) - GameScene burst helpers + EffectsManager flow effects now delegate to the pool instead of allocating rectangles + tweens every frame - Quality auto-detect (low on mobile/small screens) cuts particle counts A2 Fix high-speed landing tunneling - Cap downward velocity at PHYSICS.maxFallSpeed (boosts unaffected) - platformCollisionFilter now uses a swept deltaY one-way check so a fast fall can never be wrongly rejected or passed through A3 Safe storage - New utils/storage.js wraps localStorage with in-memory fallback so private mode / quota errors cannot crash the game; all access routed through it A4-A6 Lifecycle and robustness - Global error / unhandledrejection guard recovers to the menu - GameScene auto-pauses on tab hidden and cleans up the cross-scene listener on shutdown; fps pacing + disableContextMenu in game config - Moving platforms use a proper dynamic body instead of destroy()+new Body Verified in-browser: menu + game load with zero console errors, all emitters active, normal bouncing works, fall speed capped.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { GAME_WIDTH, GAME_HEIGHT } from '../config/game.config.js';
|
||||
|
||||
const FLAME_COLORS = [0xffd700, 0xffaa00, 0xff6600, 0xff3300, 0xaa0000];
|
||||
const WIND_COLORS = [0xffffff, 0xbbddff, 0x66aaff, 0x4477cc];
|
||||
|
||||
/**
|
||||
* Drives WHEN boost visual effects play (state machine over the player's
|
||||
* power-up state). All actual particle allocation is delegated to the pooled
|
||||
* ParticleManager (scene.particles), so this class never creates GameObjects.
|
||||
*/
|
||||
export class EffectsManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
@@ -11,228 +11,57 @@ export class EffectsManager {
|
||||
this.springTrailUntil = 0;
|
||||
}
|
||||
|
||||
get pm() {
|
||||
return this.scene.particles;
|
||||
}
|
||||
|
||||
update(player, delta) {
|
||||
if (!player || !player.active) return;
|
||||
const pm = this.pm;
|
||||
if (!pm) return;
|
||||
|
||||
if (!player || !player.active || player.state === 'dead') {
|
||||
pm.stopFlow();
|
||||
return;
|
||||
}
|
||||
|
||||
const state = player.state;
|
||||
const now = this.scene.time.now;
|
||||
const feetY = player.y + player.displayHeight / 2 - 6;
|
||||
|
||||
if ((state === 'rocket' || state === 'propeller') && Math.abs(player.body.velocity.y) > 150) {
|
||||
if (state === 'rocket') this._spawnFlame(player);
|
||||
else this._spawnWind(player);
|
||||
|
||||
this.speedLineTimer += delta;
|
||||
if (this.speedLineTimer > 65) {
|
||||
this.speedLineTimer = 0;
|
||||
this._spawnSpeedLine();
|
||||
}
|
||||
if (state === 'rocket' && Math.abs(player.body.velocity.y) > 150) {
|
||||
pm.flameAt(player.x, feetY);
|
||||
this._speedLines(delta);
|
||||
} else if (state === 'propeller' && Math.abs(player.body.velocity.y) > 150) {
|
||||
pm.windAt(player.x, player.y);
|
||||
this._speedLines(delta);
|
||||
} else if (state === 'normal' && now < this.springTrailUntil && player.body.velocity.y < -500) {
|
||||
this._spawnSpringSpark(player);
|
||||
pm.springAt(player.x, feetY);
|
||||
} else {
|
||||
pm.stopFlow();
|
||||
}
|
||||
|
||||
if (this.lastState !== state) {
|
||||
if ((this.lastState === 'rocket' || this.lastState === 'propeller') && state === 'normal') {
|
||||
this._spawnEndPuff(player);
|
||||
pm.puff(player.x, player.y);
|
||||
}
|
||||
this.lastState = state;
|
||||
}
|
||||
}
|
||||
|
||||
_speedLines(delta) {
|
||||
this.speedLineTimer += delta;
|
||||
if (this.speedLineTimer > 65) {
|
||||
this.speedLineTimer = 0;
|
||||
this.pm.speedLine();
|
||||
}
|
||||
}
|
||||
|
||||
startBoost(player, type) {
|
||||
this._spawnBurst(player, type);
|
||||
if (this.pm) this.pm.boostBurst(player.x, player.y, type);
|
||||
if (type === 'spring') {
|
||||
this.springTrailUntil = this.scene.time.now + 700;
|
||||
} else {
|
||||
this.lastState = type;
|
||||
}
|
||||
}
|
||||
|
||||
_spawnSpringSpark(player) {
|
||||
const baseY = player.y + player.displayHeight / 2 - 4;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const jitter = Phaser.Math.Between(-12, 12);
|
||||
const color = Phaser.Utils.Array.GetRandom([0xffd700, 0xfff066, 0x22c55e, 0xa3e635]);
|
||||
const size = Phaser.Math.Between(4, 9);
|
||||
const p = this.scene.add.rectangle(player.x + jitter, baseY, size, size, color, 0.9).setDepth(-2);
|
||||
this.scene.tweens.add({
|
||||
targets: p,
|
||||
x: p.x + Phaser.Math.Between(-25, 25),
|
||||
y: p.y + Phaser.Math.Between(40, 90),
|
||||
scaleX: 0,
|
||||
scaleY: 0,
|
||||
alpha: 0,
|
||||
angle: Phaser.Math.Between(-180, 180),
|
||||
duration: Phaser.Math.Between(400, 600),
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_spawnFlame(player) {
|
||||
const baseY = player.y + player.displayHeight / 2 - 6;
|
||||
const count = 2;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const jitter = Phaser.Math.Between(-9, 9);
|
||||
const color = Phaser.Utils.Array.GetRandom(FLAME_COLORS);
|
||||
const size = Phaser.Math.Between(8, 16);
|
||||
const p = this.scene.add.rectangle(player.x + jitter, baseY, size, size, color, 0.95)
|
||||
.setDepth(-2);
|
||||
this.scene.tweens.add({
|
||||
targets: p,
|
||||
x: p.x + Phaser.Math.Between(-12, 12),
|
||||
y: p.y + Phaser.Math.Between(35, 80),
|
||||
scaleX: 0,
|
||||
scaleY: 0,
|
||||
alpha: 0,
|
||||
angle: Phaser.Math.Between(-180, 180),
|
||||
duration: Phaser.Math.Between(380, 650),
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// Spark
|
||||
if (Math.random() < 0.4) {
|
||||
const spark = this.scene.add.circle(player.x + Phaser.Math.Between(-5, 5), baseY, 2, 0xffffaa, 1)
|
||||
.setDepth(-1);
|
||||
this.scene.tweens.add({
|
||||
targets: spark,
|
||||
x: spark.x + Phaser.Math.Between(-25, 25),
|
||||
y: spark.y + Phaser.Math.Between(50, 110),
|
||||
alpha: 0,
|
||||
duration: 500,
|
||||
onComplete: () => spark.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_spawnWind(player) {
|
||||
const count = 2;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Phaser.Math.Between(0, 360);
|
||||
const radius = Phaser.Math.Between(20, 40);
|
||||
const startX = player.x + Math.cos(angle * Math.PI / 180) * radius;
|
||||
const startY = player.y + Math.sin(angle * Math.PI / 180) * radius * 0.5;
|
||||
const color = Phaser.Utils.Array.GetRandom(WIND_COLORS);
|
||||
const len = Phaser.Math.Between(6, 14);
|
||||
const p = this.scene.add.rectangle(startX, startY, len, 2, color, 0.75)
|
||||
.setDepth(-2);
|
||||
this.scene.tweens.add({
|
||||
targets: p,
|
||||
x: p.x + Phaser.Math.Between(-15, 15),
|
||||
y: p.y + Phaser.Math.Between(30, 80),
|
||||
alpha: 0,
|
||||
scaleX: 0.3,
|
||||
duration: Phaser.Math.Between(300, 550),
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_spawnSpeedLine() {
|
||||
const cam = this.scene.cameras.main;
|
||||
const side = Math.random() < 0.5 ? 'left' : 'right';
|
||||
const x = side === 'left'
|
||||
? Phaser.Math.Between(0, 40)
|
||||
: Phaser.Math.Between(GAME_WIDTH - 40, GAME_WIDTH);
|
||||
const y = cam.scrollY + Phaser.Math.Between(0, GAME_HEIGHT);
|
||||
const len = Phaser.Math.Between(30, 70);
|
||||
const line = this.scene.add.rectangle(x, y, 2, len, 0xffffff, 0.45).setDepth(-1);
|
||||
this.scene.tweens.add({
|
||||
targets: line,
|
||||
y: line.y + Phaser.Math.Between(150, 280),
|
||||
alpha: 0,
|
||||
scaleY: 0.5,
|
||||
duration: Phaser.Math.Between(280, 450),
|
||||
ease: 'Quad.easeIn',
|
||||
onComplete: () => line.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
_spawnBurst(player, type) {
|
||||
const cx = player.x;
|
||||
const cy = player.y;
|
||||
let colorRing, colorSpark;
|
||||
if (type === 'rocket') { colorRing = 0xff6600; colorSpark = 0xffd700; }
|
||||
else if (type === 'spring') { colorRing = 0x22c55e; colorSpark = 0xffd700; }
|
||||
else { colorRing = 0x88ccff; colorSpark = 0xffffff; }
|
||||
|
||||
// Expanding ring
|
||||
const ring = this.scene.add.circle(cx, cy, 14, undefined, 0)
|
||||
.setStrokeStyle(4, colorRing, 0.9)
|
||||
.setDepth(-1);
|
||||
this.scene.tweens.add({
|
||||
targets: ring,
|
||||
scale: 6,
|
||||
alpha: 0,
|
||||
duration: 380,
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => ring.destroy(),
|
||||
});
|
||||
|
||||
// Second slower ring
|
||||
const ring2 = this.scene.add.circle(cx, cy, 10, undefined, 0)
|
||||
.setStrokeStyle(3, colorSpark, 0.7)
|
||||
.setDepth(-1);
|
||||
this.scene.tweens.add({
|
||||
targets: ring2,
|
||||
scale: 4,
|
||||
alpha: 0,
|
||||
duration: 520,
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => ring2.destroy(),
|
||||
});
|
||||
|
||||
// Radial sparks
|
||||
const sparkCount = type === 'rocket' ? 16 : 10;
|
||||
for (let i = 0; i < sparkCount; i++) {
|
||||
const angle = (i / sparkCount) * Math.PI * 2 + Phaser.Math.FloatBetween(-0.1, 0.1);
|
||||
const dist = Phaser.Math.Between(50, 110);
|
||||
const p = this.scene.add.rectangle(cx, cy, 5, 5, colorSpark, 1).setDepth(0);
|
||||
this.scene.tweens.add({
|
||||
targets: p,
|
||||
x: cx + Math.cos(angle) * dist,
|
||||
y: cy + Math.sin(angle) * dist,
|
||||
scaleX: 0,
|
||||
scaleY: 0,
|
||||
alpha: 0,
|
||||
duration: 500,
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// Brief camera punch
|
||||
if (type === 'rocket') {
|
||||
this.scene.cameras.main.shake(120, 0.006);
|
||||
} else if (type === 'spring') {
|
||||
this.scene.cameras.main.shake(60, 0.002);
|
||||
} else {
|
||||
this.scene.cameras.main.shake(80, 0.003);
|
||||
}
|
||||
}
|
||||
|
||||
_spawnEndPuff(player) {
|
||||
const cx = player.x;
|
||||
const cy = player.y;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const angle = Phaser.Math.FloatBetween(0, Math.PI * 2);
|
||||
const dist = Phaser.Math.Between(30, 60);
|
||||
const size = Phaser.Math.Between(8, 16);
|
||||
const gray = Phaser.Utils.Array.GetRandom([0x888888, 0xaaaaaa, 0x666666]);
|
||||
const p = this.scene.add.rectangle(cx, cy, size, size, gray, 0.6).setDepth(-2);
|
||||
this.scene.tweens.add({
|
||||
targets: p,
|
||||
x: cx + Math.cos(angle) * dist,
|
||||
y: cy + Math.sin(angle) * dist - 15,
|
||||
scaleX: 1.5,
|
||||
scaleY: 1.5,
|
||||
alpha: 0,
|
||||
duration: Phaser.Math.Between(500, 800),
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => p.destroy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user