From 6f607e6c536f5b255398a7504f362215c9d42696 Mon Sep 17 00:00:00 2001 From: TechEvolveAI Date: Fri, 14 Aug 2026 15:17:44 +0100 Subject: [PATCH] Improve Void Peaks route value --- scripts/smoke-secondary-journeys.js | 126 +++++++++++++++++- .../FourthExpeditionRescueLoop.test.js | 31 +++++ .../LevelTraversalQualityContract.test.js | 27 +++- src/scenes/PlatformerLevelScene.js | 5 +- src/scenes/levels/VoidPeaksLevel.js | 98 ++++++++++---- 5 files changed, 258 insertions(+), 29 deletions(-) diff --git a/scripts/smoke-secondary-journeys.js b/scripts/smoke-secondary-journeys.js index 0a5e8ff3..f9eb00da 100644 --- a/scripts/smoke-secondary-journeys.js +++ b/scripts/smoke-secondary-journeys.js @@ -2550,11 +2550,23 @@ async function smokeLevel(session, route, sceneName, exceptions, { state.peaksAmbientRendering?.starCount !== 35 || state.peaksAmbientRendering?.starLayerCount !== 1 || state.peaksAmbientRendering?.emberCount !== 18 || - state.peaksAmbientRendering?.emberLayerCount !== 1 + state.peaksAmbientRendering?.emberLayerCount !== 1 || + state.currentEcologyPlacement?.supportId !== + 'peak-ridge-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 < 1200 ) ) { throw new Error( - `${sceneName} did not keep Peaks ambience batched: ${JSON.stringify(state)}` + `${sceneName} did not keep Peaks ambience and opening bounded: ` + + JSON.stringify(state) ); } if ( @@ -2689,6 +2701,20 @@ async function smokeLevel(session, route, sceneName, exceptions, { JSON.stringify(framePacing.graphicsTweenDepths) ); } + if ( + route === 'voidPeaks' && + (state.canvasWidth <= 480 || state.canvasHeight < 620) && + [ + 'depth:120:visible', + 'depth:130:visible', + 'depth:179:visible' + ].some(depth => framePacing.graphicsTweenDepths?.[depth]) + ) { + throw new Error( + `${sceneName} kept whole-route scenery animating on mobile: ` + + JSON.stringify(framePacing.graphicsTweenDepths) + ); + } const forestEnemyScheduler = route === 'mythicalForest' ? await smokeForestSharedEnemyScheduler(session) : null; @@ -3623,6 +3649,102 @@ async function smokeLevel(session, route, sceneName, exceptions, { await startCampaignScene(session, { route, sceneName }); await delay(400); } + if (route === 'voidPeaks') { + const mainRouteStaged = await evaluate(session, `(() => { + const scene = window.mythicalGame.scene.getScene( + 'VoidPeaksLevel' + ); + const routeState = scene?.optionalRouteRewards?.get?.( + 'peaks_relic_ridge' + ); + 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 warning line`); + } + mainRouteEffect = await waitFor( + () => evaluate(session, `(() => { + const scene = window.mythicalGame.scene.getScene( + 'VoidPeaksLevel' + ); + const routeState = scene?.optionalRouteRewards?.get?.( + 'peaks_relic_ridge' + ); + if (routeState?.choice?.selectedPath !== 'main') return null; + const optionalAccepted = scene.recordOptionalRouteProgress( + 'peaks_relic_ridge', + { x: scene.player.x, y: scene.player.y } + ); + const energyBefore = scene.crystalEnergy; + const chargesBefore = scene.freeSpecialAttackCharges; + const objectiveBefore = scene.getPeakObjectiveText?.() || ''; + const persistedBefore = + scene.getExpeditionRouteState?.().titanSurgeCharges; + scene.performSpecialAttack(); + return { + selectedPath: routeState.choice.selectedPath, + peakRouteChoice: scene.peakRouteChoice, + optionalAccepted, + progress: routeState.progress, + completed: routeState.completed, + optionalFragmentsRemaining: ( + scene.collectibles?.getChildren?.() || [] + ).filter(item => ( + item?.optionalRouteId === 'peaks_relic_ridge' && + item.active !== false + )).length, + objective: scene.getPeakObjectiveText?.() || '', + objectiveBefore, + energyBefore, + energyAfter: scene.crystalEnergy, + chargesBefore, + chargesAfter: scene.freeSpecialAttackCharges, + persistedBefore, + persistedCharges: + scene.getExpeditionRouteState?.().titanSurgeCharges + }; + })()`), + { timeoutMs: 2500, message: `${sceneName} low warning line selection` } + ); + if ( + mainRouteEffect.selectedPath !== 'main' || + mainRouteEffect.peakRouteChoice !== 'main' || + mainRouteEffect.optionalAccepted !== false || + mainRouteEffect.progress !== 0 || + mainRouteEffect.completed !== false || + mainRouteEffect.optionalFragmentsRemaining !== 0 || + mainRouteEffect.chargesBefore !== 1 || + mainRouteEffect.chargesAfter !== 0 || + mainRouteEffect.energyAfter !== mainRouteEffect.energyBefore || + mainRouteEffect.persistedBefore !== 1 || + mainRouteEffect.persistedCharges !== 0 || + !mainRouteEffect.objectiveBefore.includes('FREE BLAST READY') || + !mainRouteEffect.objective.includes('FREE BLAST SPENT') || + !choicePresentation.mainTradeoff.includes('1 FREE BLAST') || + !choicePresentation.challengeLabel.includes('2 RELICS') || + !choicePresentation.rewardLabel.includes('1 HIT') + ) { + throw new Error( + `${sceneName} route rewards contradicted their promise: ` + + JSON.stringify({ choicePresentation, mainRouteEffect }) + ); + } + // The high ridge must be proven in an independent clean scene; + // choosing the low line intentionally retires its relics. + await startCampaignScene(session, { route, sceneName }); + await delay(400); + } if (route === 'finalVoid') { const mainRouteStaged = await evaluate(session, `(() => { const scene = window.mythicalGame.scene.getScene( diff --git a/src/__tests__/FourthExpeditionRescueLoop.test.js b/src/__tests__/FourthExpeditionRescueLoop.test.js index be7c3bf7..eee7a5cb 100644 --- a/src/__tests__/FourthExpeditionRescueLoop.test.js +++ b/src/__tests__/FourthExpeditionRescueLoop.test.js @@ -106,6 +106,37 @@ describe('fourth expedition rescue loop', () => { expect(source).toContain('window.InventoryManager?.addItem?.({'); }); + test('makes both Peaks routes consequential and persists their combat rewards', () => { + const source = readLevel(); + + expect(source).toContain( + "mainTradeoff: 'SHORT + RISKY\\nEARNS: TITAN SURGE // 1 FREE BLAST'" + ); + expect(source).toContain( + "challengeLabel: 'HIGH RIDGE // 2 RELICS, FEWER GUARDS'" + ); + expect(source).toContain('titanSurgeCharges: this.peakRouteChoice'); + expect(source).toContain('this.freeSpecialAttackCharges += 1'); + expect(source).toContain('this.retireUnavailablePeakRouteFragments()'); + expect(source).toContain('TITAN SURGE // 1 FREE BLAST READY'); + expect(source).toContain('TITAN SURGE // FREE BLAST SPENT'); + expect(source).toContain('onFreeSpecialAttackConsumed()'); + expect(source).toContain('this.refreshPersistedExpeditionRouteState()'); + }); + + test('keeps whole-route decoration static on compact mobile screens', () => { + const source = readLevel(); + + expect(source).toContain('shouldAnimatePeakRouteDecorations()'); + expect(source).toContain( + 'return !(this.isMobile || width <= 480 || height < 620)' + ); + expect(source).toContain('{ animate: animateRouteDecorations }'); + expect(source).toContain( + '{ animate: this.shouldAnimatePeakRouteDecorations() }' + ); + }); + test('protects the player during the intro and after restoration begins', () => { const source = readLevel(); const startBoss = source.match( diff --git a/src/__tests__/LevelTraversalQualityContract.test.js b/src/__tests__/LevelTraversalQualityContract.test.js index fc7e2906..5fae5516 100644 --- a/src/__tests__/LevelTraversalQualityContract.test.js +++ b/src/__tests__/LevelTraversalQualityContract.test.js @@ -1090,7 +1090,7 @@ describe('campaign traversal quality contracts', () => { expect(source).toContain('this.retirePeakPatrolsForTitan();'); expect(source).toContain("mainLabel: 'LOW WARNING LINE →'"); expect(source).toContain( - "mainTradeoff: 'SHORTER // GEYSERS + HEAVY GUARDS'" + "mainTradeoff: 'SHORT + RISKY\\nEARNS: TITAN SURGE // 1 FREE BLAST'" ); expect(source).toContain( "challengeLabel: 'HIGH RIDGE // 2 RELICS, FEWER GUARDS'" @@ -1539,7 +1539,7 @@ describe('campaign traversal quality contracts', () => { 'levels/VoidPeaksLevel.js', "id: 'peaks_relic_ridge'", "mainLabel: 'LOW WARNING LINE →'", - "mainTradeoff: 'SHORTER // GEYSERS + HEAVY GUARDS'", + "mainTradeoff: 'SHORT + RISKY\\nEARNS: TITAN SURGE // 1 FREE BLAST'", "challengeLabel: 'HIGH RIDGE // 2 RELICS, FEWER GUARDS'" ], [ @@ -2625,6 +2625,29 @@ describe('campaign traversal quality contracts', () => { expect(smoke).toContain('Aurora Quiet Light returned after reload'); }); + test('keeps the Peaks opening clear and proves both route rewards independently', () => { + const base = read('PlatformerLevelScene.js'); + const smoke = read('../../scripts/smoke-secondary-journeys.js'); + + expect(base).toContain("supportId: 'peak-ridge-approach'"); + expect(smoke).toContain( + "state.currentEcologyPlacement?.supportId !==\n 'peak-ridge-approach'" + ); + expect(smoke).toContain( + "route === 'voidPeaks' &&\n (state.canvasWidth <= 480" + ); + expect(smoke).toContain("'depth:120:visible'"); + expect(smoke).toContain("'depth:130:visible'"); + expect(smoke).toContain( + "message: `${sceneName} low warning line selection`" + ); + expect(smoke).toContain('optionalFragmentsRemaining !== 0'); + expect(smoke).toContain('persistedCharges !== 0'); + expect(smoke).toContain( + 'choosing the low line intentionally retires its relics' + ); + }); + test('runtime checkpoints retain the same authored identity persisted for reload recovery', () => { const source = read('PlatformerLevelScene.js'); diff --git a/src/scenes/PlatformerLevelScene.js b/src/scenes/PlatformerLevelScene.js index 2c49bf09..a36c29a1 100644 --- a/src/scenes/PlatformerLevelScene.js +++ b/src/scenes/PlatformerLevelScene.js @@ -102,8 +102,9 @@ const CURRENT_NODE_LEVEL_CONFIG = Object.freeze({ label: 'REEF CURRENT' }), void_peaks_1: Object.freeze({ - x: 520, - groundOffset: 130, + x: 2095, + groundOffset: 235, + supportId: 'peak-ridge-approach', label: 'RIDGE CURRENT' }), aurora_depths_1: Object.freeze({ diff --git a/src/scenes/levels/VoidPeaksLevel.js b/src/scenes/levels/VoidPeaksLevel.js index c36de1d7..f4ec424f 100644 --- a/src/scenes/levels/VoidPeaksLevel.js +++ b/src/scenes/levels/VoidPeaksLevel.js @@ -502,6 +502,12 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.physics.add.overlap(this.player, this.collectibles, this.collectItem, null, this); } + shouldAnimatePeakRouteDecorations() { + const width = Number(this.cameras?.main?.width) || 0; + const height = Number(this.cameras?.main?.height) || 0; + return !(this.isMobile || width <= 480 || height < 620); + } + createPeakAtmosphere() { this.peakEmberLayer?.destroy?.(); this.peakEmberLayer = this.add.graphics().setDepth(40); @@ -537,6 +543,7 @@ class VoidPeaksLevel extends PlatformerLevelScene { } createVoidGeysers() { + const animateRouteDecorations = this.shouldAnimatePeakRouteDecorations(); const geysers = [ { x: 620, width: 360 }, { x: 1500, width: 380 }, { x: 2420, width: 500 }, { x: 3380, width: 520 } @@ -556,13 +563,15 @@ class VoidPeaksLevel extends PlatformerLevelScene { visual.strokeRoundedRect(x, y - 86, width, 86, 10); visual.setDepth(120); - this.tweens.add({ - targets: visual, - alpha: { from: 0.35, to: 0.85 }, - duration: 900, - yoyo: true, - repeat: -1 - }); + if (animateRouteDecorations) { + this.tweens.add({ + targets: visual, + alpha: { from: 0.35, to: 0.85 }, + duration: 900, + yoyo: true, + repeat: -1 + }); + } this.physics.add.overlap(this.player, hazard, () => { if (!this.isInvincible) { @@ -573,6 +582,7 @@ class VoidPeaksLevel extends PlatformerLevelScene { } createPeakReturnCurrents() { + const animateRouteDecorations = this.shouldAnimatePeakRouteDecorations(); const currents = [ { id: 'peak-return-lower', @@ -655,14 +665,16 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.physics.add.overlap(this.player, zone, () => { this.activatePeakReturnCurrent(current); }); - this.tweens.add({ - targets: visual, - alpha: { from: 0.55, to: 1 }, - duration: 720, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (animateRouteDecorations) { + this.tweens.add({ + targets: visual, + alpha: { from: 0.55, to: 1 }, + duration: 720, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } this.peakReturnCurrents.push(current); }); } @@ -889,10 +901,14 @@ class VoidPeaksLevel extends PlatformerLevelScene { } getPeakObjectiveText() { - const optional = this.getOptionalRouteStatusText( - 'peaks_relic_ridge', - `OPTIONAL // STAR FRAGMENTS ${this.starFragmentsCollected}/${this.totalStarFragments}` - ); + const optional = this.peakRouteChoice === 'main' + ? this.freeSpecialAttackCharges > 0 + ? 'TITAN SURGE // 1 FREE BLAST READY' + : 'TITAN SURGE // FREE BLAST SPENT' + : this.getOptionalRouteStatusText( + 'peaks_relic_ridge', + `OPTIONAL // STAR FRAGMENTS ${this.starFragmentsCollected}/${this.totalStarFragments}` + ); if (this.bossDefeated) { return `WARNING NETWORK RESTORED\nTHE TITAN IS SAFE\n${optional}`; @@ -918,6 +934,7 @@ class VoidPeaksLevel extends PlatformerLevelScene { } createSignalRelays() { + const animateRouteDecorations = this.shouldAnimatePeakRouteDecorations(); const relays = [ { id: 'peaks_relay_1', @@ -970,7 +987,8 @@ class VoidPeaksLevel extends PlatformerLevelScene { zone, landingGuide: this.createTraversalLandingGuide( relay.activationSupportIds[0], - 0xFF8A4C + 0xFF8A4C, + { animate: animateRouteDecorations } ), activated: false }; @@ -1136,7 +1154,7 @@ class VoidPeaksLevel extends PlatformerLevelScene { returnLabel: 'WARNING LINE →', choice: { mainLabel: 'LOW WARNING LINE →', - mainTradeoff: 'SHORTER // GEYSERS + HEAVY GUARDS', + mainTradeoff: 'SHORT + RISKY\nEARNS: TITAN SURGE // 1 FREE BLAST', challengeLabel: 'HIGH RIDGE // 2 RELICS, FEWER GUARDS', mainMarker: spine, mainZone: { @@ -1208,6 +1226,9 @@ class VoidPeaksLevel extends PlatformerLevelScene { peakFragmentMask: this.peakCollectedFragmentMask, relicRidgeProgress: Number(route?.progress) || 0, relicRidgeCompleted: route?.completed === true, + titanSurgeCharges: this.peakRouteChoice === 'main' + ? this.freeSpecialAttackCharges + : 0, ridgeGuardCharges: this.peakRouteChoice === 'optional' ? this.optionalRouteGuardCharges : 0, @@ -1219,6 +1240,7 @@ class VoidPeaksLevel extends PlatformerLevelScene { if (!['main', 'optional'].includes(path)) return false; if (this.peakRouteChoice && this.peakRouteChoice !== path) return false; + const firstSelection = !this.peakRouteChoice; this.peakRouteChoice = path; const choice = this.optionalRouteRewards?.get?.('peaks_relic_ridge')?.choice; if (choice) { @@ -1228,6 +1250,12 @@ class VoidPeaksLevel extends PlatformerLevelScene { choice.rejoined = rejoined && path === 'optional'; choice.sequence ||= 1; } + if (path === 'main') { + this.retireUnavailablePeakRouteFragments(); + if (!restoring && firstSelection) { + this.freeSpecialAttackCharges += 1; + } + } if (!restoring) this.refreshPersistedExpeditionRouteState(); return true; } @@ -1251,7 +1279,16 @@ class VoidPeaksLevel extends PlatformerLevelScene { this.retireCollectedPeakFragments(); const route = this.optionalRouteRewards?.get?.('peaks_relic_ridge'); - if (route && path === 'optional') { + if (path === 'main') { + const persistedSurgeCharges = Number(routeState.titanSurgeCharges); + this.freeSpecialAttackCharges = Phaser.Math.Clamp( + Number.isFinite(persistedSurgeCharges) + ? persistedSurgeCharges + : 1, + 0, + 10 + ); + } else if (route && path === 'optional') { route.progress = Phaser.Math.Clamp( Number(routeState.relicRidgeProgress) || 0, 0, @@ -1278,6 +1315,14 @@ class VoidPeaksLevel extends PlatformerLevelScene { return true; } + retireUnavailablePeakRouteFragments() { + [...(this.collectibles?.getChildren?.() || [])].forEach(item => { + if (item?.optionalRouteId === 'peaks_relic_ridge') { + item.destroy?.(); + } + }); + } + countCollectedPeakFragments() { let count = 0; for (let index = 0; index < this.totalStarFragments; index += 1) { @@ -1327,6 +1372,12 @@ class VoidPeaksLevel extends PlatformerLevelScene { } } + onFreeSpecialAttackConsumed() { + if (this.peakRouteChoice === 'main') { + this.refreshPersistedExpeditionRouteState(); + } + } + showDistantReplyNetwork(relay) { const lineLayer = this.add.graphics(); lineLayer.setDepth(175); @@ -1389,7 +1440,8 @@ class VoidPeaksLevel extends PlatformerLevelScene { }); const titanLandingGuide = this.createTraversalLandingGuide( 'peak-titan-gate', - 0xF2C94C + 0xF2C94C, + { animate: this.shouldAnimatePeakRouteDecorations() } ); this.physics.add.overlap(this.player, gate, () => {