From 20088cf144992b85225ede8be039a61ea7a695b5 Mon Sep 17 00:00:00 2001 From: TechEvolveAI Date: Fri, 14 Aug 2026 13:21:54 +0100 Subject: [PATCH] Reduce mobile biome field animation cost --- scripts/smoke-secondary-journeys.js | 28 ++++++++++++++--- .../LevelTraversalQualityContract.test.js | 12 ++++++++ src/systems/ParallaxBiome.js | 30 ++++++++++++++++--- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/scripts/smoke-secondary-journeys.js b/scripts/smoke-secondary-journeys.js index e5c3b30f..76e2556f 100644 --- a/scripts/smoke-secondary-journeys.js +++ b/scripts/smoke-secondary-journeys.js @@ -214,6 +214,23 @@ async function sampleFramePacing(session, sceneName, { const longFrames = intervals.filter(value => value > 33.4).length; const tweenTargets = (scene?.tweens?.getTweens?.() || []) .flatMap(tween => tween?.targets || []); + const biomeManaged = Boolean( + window.ParallaxBiome?.isActive && + window.ParallaxBiome?.scene === scene + ); + const sharedAmbientFieldObjects = biomeManaged + ? new Set( + (window.ParallaxBiome?.layers || []) + .filter(layer => [ + 'nebula', + 'starField', + 'rock', + 'floraField' + ].includes(layer?.type)) + .map(layer => layer?.object) + .filter(Boolean) + ) + : new Set(); const tweenTargetCounts = tweenTargets.reduce((counts, target) => { const key = target?.texture?.key || target?.type || @@ -243,10 +260,6 @@ async function sampleFramePacing(session, sceneName, { counts[key] = (counts[key] || 0) + 1; return counts; }, {}); - const biomeManaged = Boolean( - window.ParallaxBiome?.isActive && - window.ParallaxBiome?.scene === scene - ); resolve({ sceneActive: Boolean(scene?.scene?.isActive?.()), warmupMs: ${warmupMs}, @@ -267,6 +280,9 @@ async function sampleFramePacing(session, sceneName, { ), displayCount: scene?.children?.list?.length || 0, activeTweenCount: scene?.tweens?.getTweens?.().length || 0, + sharedAmbientFieldTweenCount: tweenTargets.filter( + target => sharedAmbientFieldObjects.has(target) + ).length, tweenTargetCounts, graphicsTweenDepths, graphicsTweenProfiles, @@ -2464,6 +2480,10 @@ async function smokeLevel(session, route, sceneName, exceptions, { !renderBudget || framePacing.displayCount > renderBudget.displayCount || framePacing.activeTweenCount > renderBudget.activeTweenCount || + ( + framePacing.performanceTier === 'mobile' && + framePacing.sharedAmbientFieldTweenCount !== 0 + ) || framePacing.postPipelineCount !== 0 || framePacing.performanceTier !== renderBudget.performanceTier || (SMOKE_CASE !== 'all' && framePacing.p95FrameMs > 100) diff --git a/src/__tests__/LevelTraversalQualityContract.test.js b/src/__tests__/LevelTraversalQualityContract.test.js index b390e392..ab78e90d 100644 --- a/src/__tests__/LevelTraversalQualityContract.test.js +++ b/src/__tests__/LevelTraversalQualityContract.test.js @@ -791,8 +791,20 @@ describe('campaign traversal quality contracts', () => { expect(source).toContain("const maxFloraFields = this.performanceTier === 'mobile' ? 1 : 3;"); expect(source).toContain("type: 'starField'"); expect(source).toContain("type: 'floraField'"); + expect(source).toContain('shouldAnimateAmbientFields()'); + expect(source).toContain("return this.performanceTier !== 'mobile'"); + expect(source).toContain('layer.animate && this.shouldAnimateAmbientFields()'); + expect(source).toContain('this.config.effects.enableTwinkling &&'); + expect(source).toContain('this.config.effects.enableGentleFloat &&'); expect(source).toContain("Mobile tier skips post shader"); expect(source).toContain('this.scene?.tweens?.killTweensOf?.(layer.object);'); + + const smokeSource = fs.readFileSync( + path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'), + 'utf8' + ); + expect(smokeSource).toContain('sharedAmbientFieldTweenCount'); + expect(smokeSource).toContain("framePacing.performanceTier === 'mobile'"); }); test('Crystal Caves stages the objective and guardian in forward order', () => { diff --git a/src/systems/ParallaxBiome.js b/src/systems/ParallaxBiome.js index 796a37c2..5dcea083 100644 --- a/src/systems/ParallaxBiome.js +++ b/src/systems/ParallaxBiome.js @@ -525,6 +525,11 @@ class ParallaxBiomeManager { console.log(`biome:info [ParallaxBiome] Biome '${this.config.name}' created with ${this.layers.length} layers + particle systems`); } + shouldAnimateAmbientFields() { + return this.performanceTier !== 'mobile' && + this.config?.performance?.enableAnimations !== false; + } + /** * Layer 1: Create nebula background with animated gradient */ @@ -556,7 +561,11 @@ class ParallaxBiomeManager { wisp.fillEllipse(centerX, centerY, size, size * 0.6); - if (layer.animate) { + if (this.performanceTier === 'mobile') { + wisp.setAlpha(0.18); + } + + if (layer.animate && this.shouldAnimateAmbientFields()) { this.scene.tweens.add({ targets: wisp, x: wisp.x + 30, @@ -626,6 +635,9 @@ class ParallaxBiomeManager { const field = this.scene.add.graphics(); field.setScrollFactor(layer.parallax); field.setDepth(-45 + fieldIndex); + if (this.performanceTier === 'mobile') { + field.setAlpha(0.74 + fieldIndex * 0.06); + } this.layers.push({ type: 'starField', object: field, config: layer }); return field; }); @@ -640,7 +652,10 @@ class ParallaxBiomeManager { ); } - if (this.config.effects.enableTwinkling) { + if ( + this.config.effects.enableTwinkling && + this.shouldAnimateAmbientFields() + ) { fields.forEach((field, fieldIndex) => { this.scene.tweens.add({ targets: field, @@ -735,7 +750,11 @@ class ParallaxBiomeManager { } fields.forEach((field, fieldIndex) => { - if (layer.animate && this.config.effects.enableGentleFloat) { + if ( + layer.animate && + this.config.effects.enableGentleFloat && + this.shouldAnimateAmbientFields() + ) { this.scene.tweens.add({ targets: field, y: field.y + Phaser.Math.Between(-12, 12), @@ -957,7 +976,10 @@ class ParallaxBiomeManager { flora.fillCircle(x, y - baseHeight * 0.2, baseWidth * 2); } - if (layer.animate || this.config.effects.enableBioluminescence) { + if ( + (layer.animate || this.config.effects.enableBioluminescence) && + this.shouldAnimateAmbientFields() + ) { fields.forEach((field, fieldIndex) => { this.scene.tweens.add({ targets: field,