diff --git a/scripts/smoke-secondary-journeys.js b/scripts/smoke-secondary-journeys.js index f9eb00da..385065a8 100644 --- a/scripts/smoke-secondary-journeys.js +++ b/scripts/smoke-secondary-journeys.js @@ -30,7 +30,7 @@ const CAMPAIGN_MOBILE_RENDER_BUDGETS = Object.freeze({ }), crystalCaves: Object.freeze({ displayCount: 225, - activeTweenCount: 60, + activeTweenCount: 30, performanceTier: 'mobile' }), reef: Object.freeze({ @@ -2511,7 +2511,18 @@ async function smokeLevel(session, route, sceneName, exceptions, { state.caveCrystalRendering?.layerCount !== 1 || state.caveAmbientRendering?.parallaxLayerCount !== 2 || state.caveAmbientRendering?.storyDecorationTweenCount !== 0 || - state.caveAmbientRendering?.coinLayerTweenCount !== 0 + state.caveAmbientRendering?.coinLayerTweenCount !== 0 || + state.currentEcologyPlacement?.supportId !== + 'caves-chamber-bridge' || + state.currentEcologyPlacement?.x < + state.currentEcologyPlacement?.supportLeft || + state.currentEcologyPlacement?.x > + state.currentEcologyPlacement?.supportRight || + Math.abs( + state.currentEcologyPlacement?.y - + state.currentEcologyPlacement?.supportTop + ) > 8 || + state.currentEcologyPlacement?.spawnDistance < 1000 ) ) { throw new Error( @@ -2650,6 +2661,21 @@ async function smokeLevel(session, route, sceneName, exceptions, { JSON.stringify({ renderBudget, framePacing }) ); } + if ( + route === 'crystalCaves' && + (state.canvasWidth <= 480 || state.canvasHeight < 620) && + [ + 'depth:-5:visible', + 'depth:44:visible', + 'depth:45:visible', + 'depth:84:visible' + ].some(depth => framePacing.graphicsTweenDepths?.[depth]) + ) { + throw new Error( + `${sceneName} kept offscreen cave guidance animating on mobile: ` + + JSON.stringify(framePacing.graphicsTweenDepths) + ); + } if (route === 'mythicalForest') { if ( !framePacing.forestEnemyOverlapActive || @@ -3649,6 +3675,106 @@ async function smokeLevel(session, route, sceneName, exceptions, { await startCampaignScene(session, { route, sceneName }); await delay(400); } + if (route === 'crystalCaves') { + const mainRouteStaged = await evaluate(session, `(() => { + const scene = window.mythicalGame.scene.getScene( + 'CrystalCavesLevel' + ); + const routeState = scene?.optionalRouteRewards?.get?.( + 'caves_secret_slide' + ); + 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 lower passage`); + } + mainRouteEffect = await waitFor( + () => evaluate(session, `(() => { + const scene = window.mythicalGame.scene.getScene( + 'CrystalCavesLevel' + ); + const routeState = scene?.optionalRouteRewards?.get?.( + 'caves_secret_slide' + ); + if (routeState?.choice?.selectedPath !== 'main') return null; + const optionalAccepted = scene.recordOptionalRouteProgress( + 'caves_secret_slide', + { x: scene.player.x, y: scene.player.y } + ); + const energyBefore = scene.crystalEnergy; + const multiplierBefore = scene.nextRangedDamageMultiplier; + const objectiveBefore = scene.getCrystalObjectiveText?.() || ''; + const persistedBefore = scene.getExpeditionRouteState?.() + .crystalFocusReady; + scene.performRangedAttack(); + return { + selectedPath: routeState.choice.selectedPath, + crystalChamberRoute: scene.crystalChamberRoute, + optionalAccepted, + progress: routeState.progress, + completed: routeState.completed, + wardPickupActive: scene.crystalWardPickup?.active === true, + optionalPickupsRemaining: ( + scene.collectibles?.getChildren?.() || [] + ).filter(item => ( + item?.optionalRouteId === 'caves_secret_slide' && + item.active !== false + )).length, + objective: scene.getCrystalObjectiveText?.() || '', + objectiveBefore, + energyBefore, + energyAfter: scene.crystalEnergy, + multiplierBefore, + multiplierAfter: scene.nextRangedDamageMultiplier, + persistedBefore, + persistedAfter: scene.getExpeditionRouteState?.() + .crystalFocusReady + }; + })()`), + { timeoutMs: 2500, message: `${sceneName} lower passage selection` } + ); + if ( + mainRouteEffect.selectedPath !== 'main' || + mainRouteEffect.crystalChamberRoute !== 'main' || + mainRouteEffect.optionalAccepted !== false || + mainRouteEffect.progress !== 0 || + mainRouteEffect.completed !== false || + mainRouteEffect.wardPickupActive !== false || + mainRouteEffect.optionalPickupsRemaining !== 0 || + mainRouteEffect.multiplierBefore !== 2 || + mainRouteEffect.multiplierAfter !== 1 || + mainRouteEffect.energyAfter !== mainRouteEffect.energyBefore || + mainRouteEffect.persistedBefore !== true || + mainRouteEffect.persistedAfter !== false || + !mainRouteEffect.objectiveBefore.includes( + 'CRYSTAL FOCUS x2 READY' + ) || + !mainRouteEffect.objective.includes('CRYSTAL FOCUS SPENT') || + !choicePresentation.mainTradeoff.includes('NEXT SHOT x2') || + !choicePresentation.challengeLabel.includes('1-HIT WARD') || + !choicePresentation.rewardLabel.includes('1 HIT') + ) { + throw new Error( + `${sceneName} route rewards contradicted their promise: ` + + JSON.stringify({ choicePresentation, mainRouteEffect }) + ); + } + // The Spider Walk must be proven from an independent clean scene; + // choosing the lower passage intentionally retires its Ward. + await startCampaignScene(session, { route, sceneName }); + await delay(400); + } if (route === 'voidPeaks') { const mainRouteStaged = await evaluate(session, `(() => { const scene = window.mythicalGame.scene.getScene( diff --git a/src/__tests__/CurrentEcologyGameplayContract.test.js b/src/__tests__/CurrentEcologyGameplayContract.test.js index f5bfcf22..91fd0d25 100644 --- a/src/__tests__/CurrentEcologyGameplayContract.test.js +++ b/src/__tests__/CurrentEcologyGameplayContract.test.js @@ -28,6 +28,7 @@ describe('Current ecology gameplay contract', () => { expect(source).toContain('aurora_depths_1: Object.freeze({'); expect(source).toContain('final_void_1: Object.freeze({'); expect(source).toContain('this.createCurrentEcologyNode();'); + expect(source).toContain("supportId: 'caves-chamber-bridge'"); }); test('applies and explains one bounded upstream consequence', () => { diff --git a/src/__tests__/LevelTraversalQualityContract.test.js b/src/__tests__/LevelTraversalQualityContract.test.js index 5fae5516..a6f474a4 100644 --- a/src/__tests__/LevelTraversalQualityContract.test.js +++ b/src/__tests__/LevelTraversalQualityContract.test.js @@ -1403,6 +1403,11 @@ describe('campaign traversal quality contracts', () => { test('Crystal Spider Walk is a persistent, mutually exclusive chamber route', () => { const source = read('levels/CrystalCavesLevel.js'); + const platformerSource = read('PlatformerLevelScene.js'); + const smoke = fs.readFileSync( + path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'), + 'utf8' + ); expect(source).toContain('const CAVE_ENCOUNTER_PLAN = Object.freeze(['); expect(source).toContain("beat: 'opening-stomp-lesson'"); @@ -1426,11 +1431,34 @@ describe('campaign traversal quality contracts', () => { expect(source).toContain("shield.optionalRouteId = 'caves_secret_slide'"); expect(source).toContain("onMainSelected: () => this.selectCrystalChamberRoute('main')"); expect(source).toContain("onOptionalSelected: () => this.selectCrystalChamberRoute('optional')"); + expect(source).toContain( + "mainTradeoff: 'SHORT // ARMORED CRAWLER\\nEARNS: CRYSTAL FOCUS // NEXT SHOT x2'" + ); + expect(source).toContain( + "challengeLabel: 'SPIDER + SLIDE // EARN 1-HIT WARD'" + ); expect(source).toContain("'CALM THE CRYSTAL SPIDER FIRST'"); expect(source).toContain('restoreCrystalChamberRoute(resume.routeState'); expect(source).toContain('crystalWardGuardCharges'); + expect(source).toContain('crystalFocusReady'); + expect(source).toContain('onNextRangedDamageConsumed()'); + expect(source).toContain('shouldAnimateCrystalRouteDecorations()'); + expect(source).toContain('{ depth: 84, animate: animateRouteDecorations }'); + expect(source).toContain('!this.bossFightActive'); expect(source).toContain("this.grantOptionalRouteGuard('CRYSTAL WARD', 1)"); expect(source).not.toContain('this.activateShield();'); + expect(platformerSource).toContain( + 'this.onNextRangedDamageConsumed?.(rangedDamage);' + ); + expect(smoke).toContain('lower passage selection'); + expect(smoke).toContain('CRYSTAL FOCUS x2 READY'); + expect(smoke).toContain( + 'The Spider Walk must be proven from an independent clean scene' + ); + expect(smoke).toContain( + 'kept offscreen cave guidance animating on mobile' + ); + expect(smoke).toContain("'caves-chamber-bridge'"); }); test('Final Void rewards the Trust Bridge with one reliable rescue', () => { @@ -1525,8 +1553,8 @@ describe('campaign traversal quality contracts', () => { 'levels/CrystalCavesLevel.js', "id: 'caves_secret_slide'", "mainLabel: 'LOWER PASSAGE →'", - "mainTradeoff: 'SHORT // ARMORED CRAWLER'", - "challengeLabel: 'SPIDER + CRYSTAL SLIDE'" + "mainTradeoff: 'SHORT // ARMORED CRAWLER\\nEARNS: CRYSTAL FOCUS // NEXT SHOT x2'", + "challengeLabel: 'SPIDER + SLIDE // EARN 1-HIT WARD'" ], [ 'levels/ReefLevel.js', diff --git a/src/__tests__/SecondExpeditionRescueLoop.test.js b/src/__tests__/SecondExpeditionRescueLoop.test.js index 458ef456..ab432a12 100644 --- a/src/__tests__/SecondExpeditionRescueLoop.test.js +++ b/src/__tests__/SecondExpeditionRescueLoop.test.js @@ -251,6 +251,62 @@ describe('second expedition rescue loop', () => { ); }); + test('gives the armored lower passage a persisted Crystal Focus shot', () => { + const CrystalCavesLevel = loadLevelClass(); + const scene = new CrystalCavesLevel(); + const choice = {}; + scene.optionalRouteRewards = new Map([ + ['caves_secret_slide', { + completed: false, + choice + }] + ]); + scene.nextRangedDamageMultiplier = 1; + scene.clearCrystalWardPickup = jest.fn(); + scene.refreshPersistedExpeditionRouteState = jest.fn(); + + expect(scene.selectCrystalChamberRoute('main')).toBe(true); + expect(scene.crystalChamberRoute).toBe('main'); + expect(scene.crystalFocusReady).toBe(true); + expect(scene.nextRangedDamageMultiplier).toBe(2); + expect(scene.getCrystalChamberStatusText('fallback')).toContain( + 'CRYSTAL FOCUS x2 READY' + ); + expect(scene.getExpeditionRouteState().crystalFocusReady).toBe(true); + expect(scene.selectCrystalChamberRoute('optional')).toBe(false); + + scene.onNextRangedDamageConsumed(2); + expect(scene.crystalFocusReady).toBe(false); + expect(scene.getCrystalChamberStatusText('fallback')).toContain( + 'CRYSTAL FOCUS SPENT' + ); + expect(scene.getExpeditionRouteState().crystalFocusReady).toBe(false); + }); + + test('restores Crystal Focus and keeps compact route decoration static', () => { + const CrystalCavesLevel = loadLevelClass(); + const scene = new CrystalCavesLevel(); + scene.optionalRouteRewards = new Map([ + ['caves_secret_slide', { completed: false, choice: {} }] + ]); + scene.nextRangedDamageMultiplier = 1; + scene.clearCrystalWardPickup = jest.fn(); + + expect(scene.restoreCrystalChamberRoute({ + crystalChamberRoute: 'main', + crystalFocusReady: true + })).toBe(true); + expect(scene.crystalFocusReady).toBe(true); + expect(scene.nextRangedDamageMultiplier).toBe(2); + + scene.isMobile = true; + scene.cameras = { main: { width: 390, height: 844 } }; + expect(scene.shouldAnimateCrystalRouteDecorations()).toBe(false); + scene.isMobile = false; + scene.cameras.main = { width: 1280, height: 720 }; + expect(scene.shouldAnimateCrystalRouteDecorations()).toBe(true); + }); + test('changes the live objective from route-finding to companion rescue and guardian contact', () => { const CrystalCavesLevel = loadLevelClass(); const scene = new CrystalCavesLevel(); diff --git a/src/scenes/PlatformerLevelScene.js b/src/scenes/PlatformerLevelScene.js index a36c29a1..d523a607 100644 --- a/src/scenes/PlatformerLevelScene.js +++ b/src/scenes/PlatformerLevelScene.js @@ -91,8 +91,9 @@ const CURRENT_NODE_LEVEL_CONFIG = Object.freeze({ label: 'ROOT CURRENT' }), crystal_caves_1: Object.freeze({ - x: 760, + x: 1500, groundOffset: 125, + supportId: 'caves-chamber-bridge', label: 'CRYSTAL CURRENT' }), reef_1: Object.freeze({ @@ -5470,6 +5471,7 @@ class PlatformerLevelScene extends Phaser.Scene { ); this.nextRangedDamageMultiplier = 1; if (rangedDamage > 1) { + this.onNextRangedDamageConsumed?.(rangedDamage); this.showFloatingText( `POWER SHOT x${rangedDamage}`, this.player.x, diff --git a/src/scenes/levels/CrystalCavesLevel.js b/src/scenes/levels/CrystalCavesLevel.js index 19e2d863..11880e98 100644 --- a/src/scenes/levels/CrystalCavesLevel.js +++ b/src/scenes/levels/CrystalCavesLevel.js @@ -212,6 +212,7 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.woundedCrystalGrove = null; this.crystalChamberRoute = null; this.crystalSpiderCalmed = false; + this.crystalFocusReady = false; this.crystalWardPickup = null; this.crystalCoreLift = null; this.wardGateHintUntil = 0; @@ -290,6 +291,7 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.woundedCrystalGrove = null; this.crystalChamberRoute = null; this.crystalSpiderCalmed = false; + this.crystalFocusReady = false; this.crystalWardPickup = null; this.crystalCoreLift = null; this.wardGateHintUntil = 0; @@ -783,29 +785,43 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.powerWell.wellY = y; this.powerWell.wellRadius = 35; - // Pulsing animation - this.tweens.add({ - targets: this.powerWell, - alpha: { from: 0.7, to: 1 }, - scaleX: { from: 0.95, to: 1.05 }, - scaleY: { from: 0.95, to: 1.05 }, - duration: 1500, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (this.shouldAnimateCrystalRouteDecorations()) { + this.tweens.add({ + targets: this.powerWell, + alpha: { from: 0.7, to: 1 }, + scaleX: { from: 0.95, to: 1.05 }, + scaleY: { from: 0.95, to: 1.05 }, + duration: 1500, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } else { + this.powerWell.setAlpha(0.9); + } // Particle effect this.time.addEvent({ delay: 500, callback: () => { - if (this.powerWell && window.FXLibrary) { - window.FXLibrary.stardustBurst(this, this.powerWell.wellX, this.powerWell.wellY - 10, { + const well = this.powerWell; + const worldView = this.cameras?.main?.worldView; + if ( + !well?.active || + !this.bossFightActive || + !window.FXLibrary || + (worldView?.contains && !worldView.contains(well.wellX, well.wellY)) + ) return; + window.FXLibrary.stardustBurst( + this, + well.wellX, + well.wellY - 10, + { count: 3, color: [0xE040FB, 0x7B68EE], duration: 800 - }); - } + } + ); }, loop: true }); @@ -1143,6 +1159,12 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.createCrystalCoreEngine(); } + shouldAnimateCrystalRouteDecorations() { + const width = Number(this.cameras?.main?.width) || 0; + const height = Number(this.cameras?.main?.height) || 0; + return !(this.isMobile || width <= 480 || height < 620); + } + createCrystalCoreLift() { const x = 4820; const bottom = this.levelHeight - 50; @@ -1185,14 +1207,18 @@ class CrystalCavesLevel extends PlatformerLevelScene { }); this.refreshCrystalCoreLift(); - this.tweens.add({ - targets: visual, - alpha: { from: 0.68, to: 1 }, - duration: 720, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (this.shouldAnimateCrystalRouteDecorations()) { + this.tweens.add({ + targets: visual, + alpha: { from: 0.68, to: 1 }, + duration: 720, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } else { + visual.setAlpha(0.86); + } } refreshCrystalCoreLift() { @@ -1274,6 +1300,7 @@ class CrystalCavesLevel extends PlatformerLevelScene { } createBeaconCheckpoints() { + const animateRouteDecorations = this.shouldAnimateCrystalRouteDecorations(); const anchors = [ { id: 'caves_anchor_1', @@ -1335,7 +1362,7 @@ class CrystalCavesLevel extends PlatformerLevelScene { landingGuide: this.createTraversalLandingGuide( supportId, 0x00FFFF, - { depth: 84 } + { depth: 84, animate: animateRouteDecorations } ), activated: false, respawnY: supportCheckpoint.y @@ -1546,6 +1573,9 @@ class CrystalCavesLevel extends PlatformerLevelScene { crystalWardGuardCharges: this.crystalChamberRoute === 'optional' ? this.optionalRouteGuardCharges : 0, + crystalFocusReady: this.crystalChamberRoute === 'main' + ? this.crystalFocusReady === true + : false, crystalWoundTended: this.crystalWoundTended === true }; } @@ -1556,6 +1586,7 @@ class CrystalCavesLevel extends PlatformerLevelScene { return false; } + const firstSelection = !this.crystalChamberRoute; this.crystalChamberRoute = path; const route = this.optionalRouteRewards?.get?.('caves_secret_slide'); const choice = route?.choice; @@ -1569,6 +1600,23 @@ class CrystalCavesLevel extends PlatformerLevelScene { if (path === 'main') { this.clearCrystalWardPickup(); + if (!restoring && firstSelection) { + this.crystalFocusReady = true; + this.nextRangedDamageMultiplier = Math.max( + 2, + Number(this.nextRangedDamageMultiplier) || 1 + ); + window.FXLibrary?.stardustBurst?.( + this, + this.player?.x || 2040, + (this.player?.y || this.levelHeight - 120) - 35, + { + count: 18, + color: [0x00FFFF, 0x7B68EE, 0xFFFFFF], + duration: 800 + } + ); + } } if (!restoring) { this.refreshPersistedExpeditionRouteState(); @@ -1586,7 +1634,15 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.retireCrystalSpiderFromResume(); } - if (path === 'optional' && routeState?.crystalWardClaimed === true) { + if (path === 'main') { + this.crystalFocusReady = routeState?.crystalFocusReady !== false; + if (this.crystalFocusReady) { + this.nextRangedDamageMultiplier = Math.max( + 2, + Number(this.nextRangedDamageMultiplier) || 1 + ); + } + } else if (routeState?.crystalWardClaimed === true) { const route = this.optionalRouteRewards?.get?.('caves_secret_slide'); if (route) { route.progress = route.required; @@ -1623,6 +1679,12 @@ class CrystalCavesLevel extends PlatformerLevelScene { } } + onNextRangedDamageConsumed() { + if (this.crystalChamberRoute !== 'main' || !this.crystalFocusReady) return; + this.crystalFocusReady = false; + this.refreshPersistedExpeditionRouteState(); + } + createWoundedCrystalGrove() { const x = 2730; const groundY = this.levelHeight - 50; @@ -2872,8 +2934,8 @@ class CrystalCavesLevel extends PlatformerLevelScene { returnLabel: 'SLIDE BACK TO THE CAVE ROUTE →', choice: { mainLabel: 'LOWER PASSAGE →', - mainTradeoff: 'SHORT // ARMORED CRAWLER', - challengeLabel: 'SPIDER + CRYSTAL SLIDE', + mainTradeoff: 'SHORT // ARMORED CRAWLER\nEARNS: CRYSTAL FOCUS // NEXT SHOT x2', + challengeLabel: 'SPIDER + SLIDE // EARN 1-HIT WARD', mainMarker: chamberRouteMarker, mainZone: { left: 1900, right: 2440, @@ -2977,27 +3039,19 @@ class CrystalCavesLevel extends PlatformerLevelScene { shield.collectibleType = 'crystalShield'; shield.optionalRouteId = 'caves_secret_slide'; - // Pulsing glow animation - this.tweens.add({ - targets: shield, - alpha: { from: 0.8, to: 1 }, - scaleX: { from: 1, to: 1.15 }, - scaleY: { from: 1, to: 1.15 }, - duration: 800, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); - - // Rotating shine effect - this.tweens.add({ - targets: shield, - angle: { from: -5, to: 5 }, - duration: 2000, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (this.shouldAnimateCrystalRouteDecorations()) { + this.tweens.add({ + targets: shield, + alpha: { from: 0.8, to: 1 }, + scaleX: { from: 1, to: 1.15 }, + scaleY: { from: 1, to: 1.15 }, + angle: { from: -5, to: 5 }, + duration: 900, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } return shield; } @@ -3055,17 +3109,18 @@ class CrystalCavesLevel extends PlatformerLevelScene { crystal.setPosition(x, y); crystal.setDepth(-5); - // Prominent pulse animation - this.tweens.add({ - targets: crystal, - alpha: { from: 0.7, to: 1 }, - scaleX: { from: 1, to: 1.2 }, - scaleY: { from: 1, to: 1.2 }, - duration: 1000, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (this.shouldAnimateCrystalRouteDecorations()) { + this.tweens.add({ + targets: crystal, + alpha: { from: 0.7, to: 1 }, + scaleX: { from: 1, to: 1.2 }, + scaleY: { from: 1, to: 1.2 }, + duration: 1000, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } // Store as background crystal for proximity lighting too crystal.crystalColor = 0x00FFFF; @@ -3195,17 +3250,18 @@ class CrystalCavesLevel extends PlatformerLevelScene { relic.body.setAllowGravity(false); relic.collectibleType = 'starFragment'; - // Pulsing glow animation - this.tweens.add({ - targets: relic, - alpha: { from: 0.7, to: 1 }, - scaleX: { from: 0.95, to: 1.05 }, - scaleY: { from: 0.95, to: 1.05 }, - duration: 1500, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (this.shouldAnimateCrystalRouteDecorations()) { + this.tweens.add({ + targets: relic, + alpha: { from: 0.7, to: 1 }, + scaleX: { from: 0.95, to: 1.05 }, + scaleY: { from: 0.95, to: 1.05 }, + duration: 1500, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } return relic; } @@ -3936,27 +3992,27 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.crystalCore = this.physics.add.staticSprite(coreX, coreY, textureKey); this.crystalCore.setDepth(50); + const animateRouteDecorations = this.shouldAnimateCrystalRouteDecorations(); - // Pulsing animation - this.tweens.add({ - targets: this.crystalCore, - scaleX: { from: 1, to: 1.15 }, - scaleY: { from: 1, to: 1.15 }, - alpha: { from: 0.8, to: 1 }, - duration: 1200, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); - - // Slow rotation - this.tweens.add({ - targets: this.crystalCore, - angle: 360, - duration: 8000, - repeat: -1, - ease: 'Linear' - }); + if (animateRouteDecorations) { + this.tweens.add({ + targets: this.crystalCore, + scaleX: { from: 1, to: 1.15 }, + scaleY: { from: 1, to: 1.15 }, + alpha: { from: 0.8, to: 1 }, + duration: 1200, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + this.tweens.add({ + targets: this.crystalCore, + angle: 360, + duration: 8000, + repeat: -1, + ease: 'Linear' + }); + } this.createGuardianGateState({ x: coreX, @@ -3979,15 +4035,18 @@ class CrystalCavesLevel extends PlatformerLevelScene { beacon.fillStyle(0x00FFFF, 0.25); beacon.fillRect(coreX - 8, 0, 16, coreY); - // Pulsing beacon - this.tweens.add({ - targets: beacon, - alpha: { from: 0.3, to: 0.8 }, - duration: 1500, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (animateRouteDecorations) { + this.tweens.add({ + targets: beacon, + alpha: { from: 0.3, to: 0.8 }, + duration: 1500, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } else { + beacon.setAlpha(0.65); + } // Overlap for collection - triggers boss fight this.physics.add.overlap(this.player, this.crystalCore, () => { @@ -5449,7 +5508,11 @@ class CrystalCavesLevel extends PlatformerLevelScene { ? 'SPIDER WALK // CLAIM THE WARD' : 'SPIDER WALK // CALM THE SPIDER'; } - if (path === 'main') return 'LOWER PASSAGE // REJOIN AHEAD'; + if (path === 'main') { + return this.crystalFocusReady + ? 'LOWER PASSAGE // CRYSTAL FOCUS x2 READY' + : 'LOWER PASSAGE // CRYSTAL FOCUS SPENT'; + } if (this.beaconAnchorsActivated === 1) { return 'CHAMBER FORK // CHOOSE A ROUTE'; }