From a7b99860fcffba29942481b40ca2d603ae014cff Mon Sep 17 00:00:00 2001 From: TechEvolveAI Date: Fri, 14 Aug 2026 14:55:58 +0100 Subject: [PATCH] Improve Final Void opening flow --- scripts/smoke-secondary-journeys.js | 107 ++++++++++++++++++ .../LevelTraversalQualityContract.test.js | 36 ++++++ src/scenes/PlatformerLevelScene.js | 28 +++-- src/scenes/levels/FinalVoidLevel.js | 46 +++++--- 4 files changed, 191 insertions(+), 26 deletions(-) diff --git a/scripts/smoke-secondary-journeys.js b/scripts/smoke-secondary-journeys.js index 0eddaddd..0a5e8ff3 100644 --- a/scripts/smoke-secondary-journeys.js +++ b/scripts/smoke-secondary-journeys.js @@ -2557,6 +2557,27 @@ async function smokeLevel(session, route, sceneName, exceptions, { `${sceneName} did not keep Peaks ambience batched: ${JSON.stringify(state)}` ); } + if ( + route === 'finalVoid' && + ( + state.currentEcologyPlacement?.supportId !== + 'final-return-approach' || + state.currentEcologyPlacement?.x < + state.currentEcologyPlacement?.supportLeft || + state.currentEcologyPlacement?.x > + state.currentEcologyPlacement?.supportRight || + Math.abs( + state.currentEcologyPlacement?.y - + state.currentEcologyPlacement?.supportTop + ) > 8 || + state.currentEcologyPlacement?.spawnDistance < 850 + ) + ) { + throw new Error( + `${sceneName} placed the Current Heart inside its opening viewport: ` + + JSON.stringify(state.currentEcologyPlacement) + ); + } const renderBudget = CAMPAIGN_MOBILE_RENDER_BUDGETS[route]; if (!renderBudget) { throw new Error(`${sceneName} has no authored mobile render budget`); @@ -2654,6 +2675,20 @@ async function smokeLevel(session, route, sceneName, exceptions, { ); } } + if ( + route === 'finalVoid' && + (state.canvasWidth <= 480 || state.canvasHeight < 620) && + [ + 'depth:106:visible', + 'depth:115:visible', + 'depth:179:visible' + ].some(depth => framePacing.graphicsTweenDepths?.[depth]) + ) { + throw new Error( + `${sceneName} kept offscreen route decorations animating on mobile: ` + + JSON.stringify(framePacing.graphicsTweenDepths) + ); + } const forestEnemyScheduler = route === 'mythicalForest' ? await smokeForestSharedEnemyScheduler(session) : null; @@ -3588,6 +3623,78 @@ async function smokeLevel(session, route, sceneName, exceptions, { await startCampaignScene(session, { route, sceneName }); await delay(400); } + if (route === 'finalVoid') { + const mainRouteStaged = await evaluate(session, `(() => { + const scene = window.mythicalGame.scene.getScene( + 'FinalVoidLevel' + ); + const routeState = scene?.optionalRouteRewards?.get?.( + 'final_trust_bridge' + ); + const support = scene?.getTraversalSupport?.( + routeState?.choice?.mainSupportIds?.[0] + ); + if (!scene?.player?.body || !routeState?.choice || !support?.body) { + return null; + } + scene.player.body.reset( + support.x, + support.body.top - scene.player.body.height - 4 + ); + scene.player.setVelocity?.(0, 0); + return { supportId: support.traversalId }; + })()`); + if (!mainRouteStaged) { + throw new Error(`${sceneName} could not stage its low crossing`); + } + mainRouteEffect = await waitFor( + () => evaluate(session, `(() => { + const scene = window.mythicalGame.scene.getScene( + 'FinalVoidLevel' + ); + const routeState = scene?.optionalRouteRewards?.get?.( + 'final_trust_bridge' + ); + if (routeState?.choice?.selectedPath !== 'main') return null; + const optionalAccepted = scene.recordOptionalRouteProgress( + 'final_trust_bridge', + { x: scene.player.x, y: scene.player.y } + ); + return { + selectedPath: routeState.choice.selectedPath, + finalRouteChoice: scene.finalRouteChoice, + optionalAccepted, + progress: routeState.progress, + completed: routeState.completed, + bondReserveReady: scene.bondReserveReady === true, + pickupActive: scene.optionalRoutePickup?.active === true + }; + })()`), + { timeoutMs: 2500, message: `${sceneName} low crossing selection` } + ); + if ( + mainRouteEffect.selectedPath !== 'main' || + mainRouteEffect.finalRouteChoice !== 'main' || + mainRouteEffect.optionalAccepted !== false || + mainRouteEffect.progress !== 0 || + mainRouteEffect.completed !== false || + mainRouteEffect.bondReserveReady !== false || + mainRouteEffect.pickupActive !== false || + !choicePresentation.mainTradeoff.includes( + 'RIFT DAMAGE + 2 GUARDS' + ) || + !choicePresentation.challengeLabel.includes('EARN RESCUE') + ) { + throw new Error( + `${sceneName} low crossing failed to lock out its optional rescue: ` + + JSON.stringify({ choicePresentation, mainRouteEffect }) + ); + } + // Trust Bridge must be proven from an independent clean scene; + // selecting the low crossing intentionally locks out its reward. + await startCampaignScene(session, { route, sceneName }); + await delay(400); + } let rejectedOptionalPickup = null; if (route === 'mythicalForest') { diff --git a/src/__tests__/LevelTraversalQualityContract.test.js b/src/__tests__/LevelTraversalQualityContract.test.js index 4e606075..fc7e2906 100644 --- a/src/__tests__/LevelTraversalQualityContract.test.js +++ b/src/__tests__/LevelTraversalQualityContract.test.js @@ -1490,6 +1490,27 @@ describe('campaign traversal quality contracts', () => { expect(source).toContain('this.refreshPersistedExpeditionRouteState();'); expect(source).toContain('incomingDamage = Math.max(0, this.health - 1);'); expect(source).toContain('this.bondReserveEcho?.destroy?.();'); + expect(source).toContain('shouldAnimateFinalRouteDecorations()'); + expect(source).toContain('{ animate: animateRouteDecorations }'); + + const smoke = fs.readFileSync( + path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'), + 'utf8' + ); + expect(smoke).toContain( + "mainRouteEffect.finalRouteChoice !== 'main'" + ); + expect(smoke).toContain( + 'low crossing failed to lock out its optional rescue' + ); + expect(smoke).toContain( + 'Trust Bridge must be proven from an independent clean scene' + ); + expect(smoke).toContain( + 'kept offscreen route decorations animating on mobile' + ); + expect(smoke).toContain("'depth:115:visible'"); + expect(smoke).toContain("'depth:179:visible'"); }); test.each([ @@ -2384,6 +2405,21 @@ describe('campaign traversal quality contracts', () => { expect(smoke).toContain('departureCueX > 360'); }); + test('Final Void keeps the optional Current Heart beyond its first bond signal', () => { + const shared = read('PlatformerLevelScene.js'); + const smoke = fs.readFileSync( + path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'), + 'utf8' + ); + + expect(shared).toContain("supportId: 'final-return-approach'"); + expect(shared).toContain('x: 1230'); + expect(smoke).toContain('spawnDistance < 850'); + expect(smoke).toContain( + 'placed the Current Heart inside its opening viewport' + ); + }); + test('Stellar Reef binds every required waypoint to a distinct relay support', () => { const source = read('levels/ReefLevel.js'); diff --git a/src/scenes/PlatformerLevelScene.js b/src/scenes/PlatformerLevelScene.js index c4e2c3c0..2c49bf09 100644 --- a/src/scenes/PlatformerLevelScene.js +++ b/src/scenes/PlatformerLevelScene.js @@ -112,8 +112,9 @@ const CURRENT_NODE_LEVEL_CONFIG = Object.freeze({ label: 'AURORA CURRENT' }), final_void_1: Object.freeze({ - x: 420, - groundOffset: 130, + x: 1230, + groundOffset: 190, + supportId: 'final-return-approach', label: 'CURRENT HEART' }) }); @@ -1877,7 +1878,10 @@ class PlatformerLevelScene extends Phaser.Scene { }; } - createTraversalLandingGuide(id, color = 0x7FFFD4, { depth = 179 } = {}) { + createTraversalLandingGuide(id, color = 0x7FFFD4, { + depth = 179, + animate = true + } = {}) { const support = this.getTraversalSupport(id); if (!support?.body) return null; @@ -1896,14 +1900,16 @@ class PlatformerLevelScene extends Phaser.Scene { support.x, top - 2 ); - const tween = this.tweens.add({ - targets: visual, - alpha: { from: 0.55, to: 1 }, - duration: 720, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + const tween = animate + ? this.tweens.add({ + targets: visual, + alpha: { from: 0.55, to: 1 }, + duration: 720, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }) + : null; const guide = { id, visual, tween }; this.traversalLandingGuides.push(guide); return guide; diff --git a/src/scenes/levels/FinalVoidLevel.js b/src/scenes/levels/FinalVoidLevel.js index 0d97ecc6..af097d75 100644 --- a/src/scenes/levels/FinalVoidLevel.js +++ b/src/scenes/levels/FinalVoidLevel.js @@ -650,13 +650,15 @@ class FinalVoidLevel extends PlatformerLevelScene { route.strokeCircle(1900, groundY - 430, 17); route.strokeCircle(2270, groundY - 460, 12); - this.tweens.add({ - targets: route, - alpha: { from: 0.5, to: 1 }, - duration: 950, - yoyo: true, - repeat: -1 - }); + if (this.shouldAnimateFinalRouteDecorations()) { + this.tweens.add({ + targets: route, + alpha: { from: 0.5, to: 1 }, + duration: 950, + yoyo: true, + repeat: -1 + }); + } const fractureRouteMarker = this.add.text(1690, groundY - 82, '', { fontSize: '11px', @@ -883,6 +885,15 @@ class FinalVoidLevel extends PlatformerLevelScene { }); } + shouldAnimateFinalRouteDecorations() { + const { width, height } = this.cameras.main; + return !( + this.isMobile || + width <= 480 || + height < 620 + ); + } + createVoidFractures() { const fractures = [ { x: 930, width: 120, label: 'JUMP THE RIFT →' }, @@ -917,13 +928,15 @@ class FinalVoidLevel extends PlatformerLevelScene { } ).setOrigin(0.5).setDepth(182); - this.tweens.add({ - targets: visual, - alpha: { from: 0.5, to: 1 }, - duration: 850, - yoyo: true, - repeat: -1 - }); + if (this.shouldAnimateFinalRouteDecorations()) { + this.tweens.add({ + targets: visual, + alpha: { from: 0.5, to: 1 }, + duration: 850, + yoyo: true, + repeat: -1 + }); + } this.physics.add.overlap(this.player, zone, () => { if (!this.isInvincible && !this.bossDefeated) { @@ -935,6 +948,8 @@ class FinalVoidLevel extends PlatformerLevelScene { } createBondAnchors() { + const animateRouteDecorations = + this.shouldAnimateFinalRouteDecorations(); const anchors = [ { id: 'final_bond_1', @@ -987,7 +1002,8 @@ class FinalVoidLevel extends PlatformerLevelScene { zone, landingGuide: this.createTraversalLandingGuide( anchor.activationSupportIds[0], - 0xA9F3E4 + 0xA9F3E4, + { animate: animateRouteDecorations } ), activated: false };