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:
2026-05-29 13:10:08 +07:00
parent 1062b2855a
commit fc1f12bb7e
13 changed files with 503 additions and 289 deletions

View File

@@ -4,7 +4,10 @@ export class Platform extends Physics.Arcade.Sprite {
constructor(scene, x, y, type = 'stable') {
super(scene, x, y, 'platform');
scene.add.existing(this);
scene.physics.add.existing(this, true);
// Moving platforms need a dynamic body; everything else is static.
const isMoving = type === 'moving';
scene.physics.add.existing(this, !isMoving);
this.platformType = type;
this.breakingState = 0;
@@ -13,12 +16,10 @@ export class Platform extends Physics.Arcade.Sprite {
this.startX = x;
this.glowTween = null;
if (type === 'moving') {
this.body.destroy();
this.body = new Phaser.Physics.Arcade.Body(scene.physics.world, this);
this.body.allowGravity = false;
this.body.immovable = true;
this.moveSpeed = Phaser.Math.Between(50, 120) * (Math.random() < 0.5 ? 1 : -1);
if (isMoving) {
this.body.setAllowGravity(false);
this.body.setImmovable(true);
this.moveSpeed = Phaser.Math.Between(50, 120) * (Phaser.Math.RND.frac() < 0.5 ? 1 : -1);
this.moveRange = Phaser.Math.Between(60, 160);
} else if (type === 'breaking') {
this.setTint(0x999999);