From 58db4f1340b7e804e332beb5b021f32c9dc1a816 Mon Sep 17 00:00:00 2001 From: TechEvolveAI Date: Fri, 14 Aug 2026 17:14:01 +0100 Subject: [PATCH] Bound Void Peaks mobile runtime work --- scripts/smoke-secondary-journeys.js | 46 ++++++++++- .../FourthExpeditionRescueLoop.test.js | 5 +- .../LevelTraversalQualityContract.test.js | 12 +++ src/scenes/levels/VoidPeaksLevel.js | 82 ++++++++++++++----- 4 files changed, 124 insertions(+), 21 deletions(-) diff --git a/scripts/smoke-secondary-journeys.js b/scripts/smoke-secondary-journeys.js index 47867067..e9e3f9d1 100644 --- a/scripts/smoke-secondary-journeys.js +++ b/scripts/smoke-secondary-journeys.js @@ -201,6 +201,12 @@ async function sampleFramePacing(session, sceneName, { const objectiveTextureRevisionAtStart = Number( scene?.campaignObjectiveTextureRevision ) || 0; + const peakEmberDrawCountAtStart = Number( + scene?.peakEmberDrawCount + ) || 0; + const peakEnemyPatrolUpdateCountAtStart = Number( + scene?.peakEnemyPatrolUpdateCount + ) || 0; let previousAt = null; const percentile = (sorted, ratio) => { @@ -292,6 +298,21 @@ async function sampleFramePacing(session, sceneName, { objectiveTextureRevisionAtStart ) }, + peaksRuntime: scene?.scene?.key === 'VoidPeaksLevel' ? { + emberRedrawsDuringSample: Math.max( + 0, + (Number(scene?.peakEmberDrawCount) || 0) - + peakEmberDrawCountAtStart + ), + emberVisibleCount: Number( + scene?.peakEmberVisibleCount + ) || 0, + patrolUpdatesDuringSample: Math.max( + 0, + (Number(scene?.peakEnemyPatrolUpdateCount) || 0) - + peakEnemyPatrolUpdateCountAtStart + ) + } : null, activeTweenCount: scene?.tweens?.getTweens?.().length || 0, sharedAmbientFieldTweenCount: tweenTargets.filter( target => sharedAmbientFieldObjects.has(target) @@ -2702,7 +2723,12 @@ async function smokeLevel(session, route, sceneName, exceptions, { ).length || 0, emberLayerCount: scene.children?.list?.filter( item => item === scene.peakEmberLayer - ).length || 0 + ).length || 0, + emberVisibleCount: Number(scene.peakEmberVisibleCount) || 0, + emberDrawCount: Number(scene.peakEmberDrawCount) || 0, + patrolUpdateCount: Number( + scene.peakEnemyPatrolUpdateCount + ) || 0 } : null, routeGuidance: (() => { const nextSignal = scene?.getNextOrderedRouteSignal?.(); @@ -2834,6 +2860,9 @@ async function smokeLevel(session, route, sceneName, exceptions, { state.peaksAmbientRendering?.starLayerCount !== 1 || state.peaksAmbientRendering?.emberCount !== 18 || state.peaksAmbientRendering?.emberLayerCount !== 1 || + state.peaksAmbientRendering?.emberVisibleCount > 6 || + state.peaksAmbientRendering?.emberDrawCount < 1 || + state.peaksAmbientRendering?.patrolUpdateCount < 1 || state.currentEcologyPlacement?.supportId !== 'peak-ridge-approach' || state.currentEcologyPlacement?.x < @@ -2999,6 +3028,21 @@ async function smokeLevel(session, route, sceneName, exceptions, { JSON.stringify(framePacing.graphicsTweenDepths) ); } + if ( + route === 'voidPeaks' && + ( + framePacing.peaksRuntime?.emberRedrawsDuringSample < 10 || + framePacing.peaksRuntime?.emberRedrawsDuringSample > 22 || + framePacing.peaksRuntime?.emberVisibleCount > 6 || + framePacing.peaksRuntime?.patrolUpdatesDuringSample < 10 || + framePacing.peaksRuntime?.patrolUpdatesDuringSample > 28 + ) + ) { + throw new Error( + `${sceneName} did not keep Peaks runtime work bounded: ` + + JSON.stringify(framePacing.peaksRuntime) + ); + } if ( route === 'voidPeaks' && (state.canvasWidth <= 480 || state.canvasHeight < 620) && diff --git a/src/__tests__/FourthExpeditionRescueLoop.test.js b/src/__tests__/FourthExpeditionRescueLoop.test.js index eee7a5cb..8440b48d 100644 --- a/src/__tests__/FourthExpeditionRescueLoop.test.js +++ b/src/__tests__/FourthExpeditionRescueLoop.test.js @@ -173,7 +173,10 @@ describe('fourth expedition rescue loop', () => { 'const isMobileLayout = this.isMobile || width <= 480 || height < 620' ); expect(source).toContain('const barY = isMobileLayout ? 118 : 60'); - expect(source).toContain('isMobileLayout ? 165 : 90'); + expect(source).toContain('const toastY = isMobileLayout'); + expect(source).toContain('height < 620 ? Math.min(142, height * 0.38)'); + expect(source).toContain('Math.min(225, height * 0.28)'); + expect(source).toContain('y: toastY - 20'); expect(source).toContain('this.createCampaignObjectiveDisplay('); expect(source).toContain('WARNING ${current}/3 // ${nextRelay}'); expect(source).toContain('TITAN PASS OPEN'); diff --git a/src/__tests__/LevelTraversalQualityContract.test.js b/src/__tests__/LevelTraversalQualityContract.test.js index d6e6c53a..5f5ead0e 100644 --- a/src/__tests__/LevelTraversalQualityContract.test.js +++ b/src/__tests__/LevelTraversalQualityContract.test.js @@ -971,7 +971,16 @@ describe('campaign traversal quality contracts', () => { expect(source).toContain('this.peakStarLayer = this.add.graphics()'); expect(source).toContain('this.peakEmbers = Array.from({ length: 18 }'); expect(source).toContain('drawPeakEmbers(time)'); + expect(source).toContain('this.drawPeakEmbers(0, true);'); + expect(source).toContain('const cadence = this.isMobile ? 100 : 50;'); + expect(source).toContain('ember.x < left - 140 || ember.x > right + 140'); + expect(source).toContain('this.peakEmberDrawCount += 1;'); expect(source).toContain('this.peakEmberLayer.fillCircle('); + expect(source).toContain('updatePeakEnemyPatrols(time)'); + expect(source).toContain('this.peakEnemyPatrolNextAt = now + (this.isMobile ? 80 : 40);'); + expect(source).toContain('enemy.patrolSpeed'); + expect(source).toContain('const toastY = isMobileLayout'); + expect(source).toContain('y: toastY - 20'); expect(source).not.toContain('targets: ember,'); }); @@ -2615,6 +2624,9 @@ describe('campaign traversal quality contracts', () => { expect(smoke).toContain('state.reefAmbientRendering?.decorativeTweenCount !== 0'); expect(smoke).toContain('state.peaksAmbientRendering?.starCount !== 35'); expect(smoke).toContain('state.peaksAmbientRendering?.emberLayerCount !== 1'); + expect(smoke).toContain('framePacing.peaksRuntime?.emberRedrawsDuringSample > 22'); + expect(smoke).toContain('framePacing.peaksRuntime?.patrolUpdatesDuringSample > 28'); + expect(smoke).toContain('did not keep Peaks runtime work bounded'); expect(smoke).toContain('smokeForestBatchedCoinPickup(session)'); expect(smoke).toContain('state.coinRendering?.legacyVisualCount !== 0'); expect(smoke).toContain('state.coinRendering?.pickupBodyCount !== 0'); diff --git a/src/scenes/levels/VoidPeaksLevel.js b/src/scenes/levels/VoidPeaksLevel.js index f4ec424f..a8ce5dcb 100644 --- a/src/scenes/levels/VoidPeaksLevel.js +++ b/src/scenes/levels/VoidPeaksLevel.js @@ -137,6 +137,11 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.peakStarLayer = null; this.peakEmbers = []; this.peakEmberLayer = null; + this.peakEmberDrawNextAt = 0; + this.peakEmberDrawCount = 0; + this.peakEmberVisibleCount = 0; + this.peakEnemyPatrolNextAt = 0; + this.peakEnemyPatrolUpdateCount = 0; this.titanGate = null; this.bossGateHintUntil = 0; this.routeHintUntil = 0; @@ -180,6 +185,11 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.peakStarLayer = null; this.peakEmbers = []; this.peakEmberLayer = null; + this.peakEmberDrawNextAt = 0; + this.peakEmberDrawCount = 0; + this.peakEmberVisibleCount = 0; + this.peakEnemyPatrolNextAt = 0; + this.peakEnemyPatrolUpdateCount = 0; this.titanGate = null; this.bossGateHintUntil = 0; this.routeHintUntil = 0; @@ -521,25 +531,42 @@ class VoidPeaksLevel extends PlatformerLevelScene { phaseOffset: Phaser.Math.Between(0, 6200), batched: true })); - this.drawPeakEmbers(0); + this.peakEmberDrawNextAt = 0; + this.peakEmberDrawCount = 0; + this.drawPeakEmbers(0, true); } - drawPeakEmbers(time) { + drawPeakEmbers(time, force = false) { if (!this.peakEmberLayer?.active) return; const now = Number(time) || 0; + const cadence = this.isMobile ? 100 : 50; + if (!force && now < this.peakEmberDrawNextAt) return; + this.peakEmberDrawNextAt = now + cadence; + + const view = this.cameras?.main?.worldView; + const left = Number(view?.left) || 0; + const right = Number(view?.right) || this.levelWidth; + const top = Number(view?.top) || 0; + const bottom = Number(view?.bottom) || this.levelHeight; + let visibleCount = 0; this.peakEmberLayer.clear(); this.peakEmbers.forEach(ember => { + if (ember.x < left - 140 || ember.x > right + 140) return; const phase = ((now + ember.phaseOffset) % ember.duration) / ember.duration; const rise = (Math.sin((phase * Math.PI * 2) - (Math.PI / 2)) + 1) / 2; const y = ember.originY - (ember.travel * rise); + if (y < top - 100 || y > bottom + 100) return; const alpha = 0.05 + ((1 - rise) * 0.5); + visibleCount += 1; this.peakEmberLayer.fillStyle(ember.color, alpha * 0.34); this.peakEmberLayer.fillCircle(ember.x, y, ember.radius * 1.8); this.peakEmberLayer.fillStyle(ember.color, alpha); this.peakEmberLayer.fillCircle(ember.x, y, ember.radius); }); + this.peakEmberVisibleCount = visibleCount; + this.peakEmberDrawCount += 1; } createVoidGeysers() { @@ -840,9 +867,31 @@ class VoidPeaksLevel extends PlatformerLevelScene { const patrols = [...(this.enemies?.getChildren?.() || [])]; const retirement = this.retireRouteEnemies(patrols); this.peakEncounterRhythm = []; + this.peakEnemyPatrolNextAt = 0; return retirement.enemyCount; } + updatePeakEnemyPatrols(time) { + const now = Number(time) || 0; + if (now < this.peakEnemyPatrolNextAt) return 0; + this.peakEnemyPatrolNextAt = now + (this.isMobile ? 80 : 40); + + let updatedCount = 0; + (this.enemies?.getChildren?.() || []).forEach(enemy => { + if (enemy?.enemyType !== 'voidPeakSentinel' || !enemy.body?.enable) return; + if (enemy.x <= enemy.patrolMin) { + enemy.setVelocityX(Math.abs(enemy.body.velocity.x || enemy.patrolSpeed)); + enemy.setFlipX(false); + } else if (enemy.x >= enemy.patrolMax) { + enemy.setVelocityX(-Math.abs(enemy.body.velocity.x || enemy.patrolSpeed)); + enemy.setFlipX(true); + } + updatedCount += 1; + }); + this.peakEnemyPatrolUpdateCount += 1; + return updatedCount; + } + createSentinelTexture(textureKey, color) { if (this.textures.exists(textureKey)) return; @@ -1521,12 +1570,15 @@ class VoidPeaksLevel extends PlatformerLevelScene { showObjectiveToast() { const { width, height } = this.cameras.main; const isMobileLayout = this.isMobile || width <= 480 || height < 620; + const toastY = isMobileLayout + ? (height < 620 ? Math.min(142, height * 0.38) : Math.min(225, height * 0.28)) + : 90; const toast = this.add.text( width / 2, - isMobileLayout ? 165 : 90, + toastY, 'Restore the warning relays and reach Titan Pass', { - fontSize: '18px', + fontSize: isMobileLayout ? '15px' : '18px', color: '#FFD700', backgroundColor: 'rgba(0,0,0,0.72)', padding: { x: 18, y: 8 }, @@ -1537,7 +1589,7 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.tweens.add({ targets: toast, alpha: 0, - y: 60, + y: toastY - 20, delay: 2600, duration: 600, onComplete: () => toast.destroy() @@ -1550,20 +1602,7 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.drawPeakEmbers(time); this.updatePeakReturnCurrentGuidance(); - - if (this.enemies) { - this.enemies.getChildren().forEach(enemy => { - if (enemy.enemyType === 'voidPeakSentinel') { - if (enemy.x <= enemy.patrolMin) { - enemy.setVelocityX(Math.abs(enemy.body.velocity.x || 45)); - enemy.setFlipX(false); - } else if (enemy.x >= enemy.patrolMax) { - enemy.setVelocityX(-Math.abs(enemy.body.velocity.x || 45)); - enemy.setFlipX(true); - } - } - }); - } + this.updatePeakEnemyPatrols(time); this.syncCampaignObjectiveDisplay({ visible: !(this.isCompactObjectiveHUD && this.bossFightActive) @@ -2338,6 +2377,11 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.peakEmberLayer?.destroy?.(); this.peakEmberLayer = null; this.peakEmbers = []; + this.peakEmberDrawNextAt = 0; + this.peakEmberDrawCount = 0; + this.peakEmberVisibleCount = 0; + this.peakEnemyPatrolNextAt = 0; + this.peakEnemyPatrolUpdateCount = 0; super.shutdown(); console.log('[VoidPeaksLevel] Shutting down'); }