From 9638e198d95143a65ab90a60257809da297c8884 Mon Sep 17 00:00:00 2001 From: TechEvolveAI Date: Fri, 14 Aug 2026 13:28:21 +0100 Subject: [PATCH] Bound Crystal Caves mobile ambience --- scripts/smoke-secondary-journeys.js | 23 ++++- .../LevelTraversalQualityContract.test.js | 13 +++ src/scenes/levels/CrystalCavesLevel.js | 97 ++++++++++++------- 3 files changed, 97 insertions(+), 36 deletions(-) diff --git a/scripts/smoke-secondary-journeys.js b/scripts/smoke-secondary-journeys.js index 76e2556f..106f6a6f 100644 --- a/scripts/smoke-secondary-journeys.js +++ b/scripts/smoke-secondary-journeys.js @@ -2333,6 +2333,24 @@ async function smokeLevel(session, route, sceneName, exceptions, { item => item === scene.caveCrystalFieldLayer ).length || 0 } : null, + caveAmbientRendering: Array.isArray(scene?.caveParallaxLayers) ? { + parallaxLayerCount: scene.caveParallaxLayers.filter( + layer => layer?.active !== false + ).length, + storyDecorationTweenCount: ( + scene?.tweens?.getTweens?.() || [] + ).filter(tween => (tween?.targets || []).some( + target => [ + 'brokenLantern', + 'minerSkeleton' + ].includes(target?.caveAmbientRole) + )).length, + coinLayerTweenCount: ( + scene?.tweens?.getTweens?.() || [] + ).filter(tween => (tween?.targets || []).includes( + scene.caveCoinLayer + )).length + } : null, reefAmbientRendering: Array.isArray(scene?.cosmicDustParticles) ? { nebulaLayerCount: scene.nebulaParticles?.filter( layer => layer?.active !== false @@ -2421,7 +2439,10 @@ async function smokeLevel(session, route, sceneName, exceptions, { state.caveCoinRendering?.layerCount !== 1 || state.caveCoinRendering?.physicsCoinCount !== 0 || state.caveCrystalRendering?.batchedCount !== 11 || - state.caveCrystalRendering?.layerCount !== 1 + state.caveCrystalRendering?.layerCount !== 1 || + state.caveAmbientRendering?.parallaxLayerCount !== 2 || + state.caveAmbientRendering?.storyDecorationTweenCount !== 0 || + state.caveAmbientRendering?.coinLayerTweenCount !== 0 ) ) { throw new Error( diff --git a/src/__tests__/LevelTraversalQualityContract.test.js b/src/__tests__/LevelTraversalQualityContract.test.js index ab78e90d..b52734b3 100644 --- a/src/__tests__/LevelTraversalQualityContract.test.js +++ b/src/__tests__/LevelTraversalQualityContract.test.js @@ -836,8 +836,21 @@ describe('campaign traversal quality contracts', () => { expect(source).toContain('redrawCaveCrystalField()'); expect(source).toContain('this.caveCrystalField.push(crystal);'); expect(source).toContain('!Number.isFinite(crystal.lastDrawnAlpha)'); + expect(source).toContain('this.caveParallaxLayers.push(graphics);'); + expect(source).toContain("graphics.caveAmbientRole = 'brokenLantern';"); + expect(source).toContain("graphics.caveAmbientRole = 'minerSkeleton';"); + expect(source).toContain('if (!this.isMobile) {'); expect(source).not.toContain("const coin = this.collectibles.create(x, y, textureKey);"); expect(source).not.toContain('targets: coin,'); + + const smokeSource = fs.readFileSync( + path.join(__dirname, '../../scripts/smoke-secondary-journeys.js'), + 'utf8' + ); + expect(smokeSource).toContain('caveAmbientRendering'); + expect(smokeSource).toContain('parallaxLayerCount !== 2'); + expect(smokeSource).toContain('storyDecorationTweenCount !== 0'); + expect(smokeSource).toContain('coinLayerTweenCount !== 0'); }); test('Stellar Reef batches ambient fields and bounds reusable cosmic dust', () => { diff --git a/src/scenes/levels/CrystalCavesLevel.js b/src/scenes/levels/CrystalCavesLevel.js index 55ea4734..7d8c9bc6 100644 --- a/src/scenes/levels/CrystalCavesLevel.js +++ b/src/scenes/levels/CrystalCavesLevel.js @@ -202,6 +202,7 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.caveCoinLayerTween = null; this.caveCrystalField = []; this.caveCrystalFieldLayer = null; + this.caveParallaxLayers = []; // Project Beacon expedition state this.beaconAnchors = []; @@ -281,6 +282,7 @@ class CrystalCavesLevel extends PlatformerLevelScene { this.caveCoinLayerTween = null; this.caveCrystalField = []; this.caveCrystalFieldLayer = null; + this.caveParallaxLayers = []; this.beaconAnchors = []; this.beaconAnchorsActivated = 0; this.caveRouteAligned = false; @@ -3098,14 +3100,16 @@ class CrystalCavesLevel extends PlatformerLevelScene { if (this.caveCoinLayer?.active) return this.caveCoinLayer; this.caveCoinLayer = this.add.graphics().setDepth(125); - this.caveCoinLayerTween = this.tweens.add({ - targets: this.caveCoinLayer, - y: { from: 0, to: -6 }, - duration: 1000, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut' - }); + if (!this.isMobile) { + this.caveCoinLayerTween = this.tweens.add({ + targets: this.caveCoinLayer, + y: { from: 0, to: -6 }, + duration: 1000, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut' + }); + } return this.caveCoinLayer; } @@ -3398,8 +3402,10 @@ class CrystalCavesLevel extends PlatformerLevelScene { * Create a parallax background layer */ createParallaxLayer(scrollFactor, elements, depth) { + const graphics = this.add.graphics(); + elements.forEach(element => { - const graphics = this.add.graphics(); + const originX = element.x; if (element.type === 'crystalCluster') { // Distant crystal cluster @@ -3411,9 +3417,9 @@ class CrystalCavesLevel extends PlatformerLevelScene { const offsetX = Phaser.Math.Between(-element.size / 2, element.size / 2); const height = Phaser.Math.Between(element.size * 0.5, element.size); graphics.fillTriangle( - offsetX - 15, 0, - offsetX + 15, 0, - offsetX, -height + originX + offsetX - 15, 0, + originX + offsetX + 15, 0, + originX + offsetX, -height ); } } else if (element.type === 'stalactites') { @@ -3424,29 +3430,40 @@ class CrystalCavesLevel extends PlatformerLevelScene { const offsetX = (i - 3) * 20; const height = Phaser.Math.Between(40, element.size); graphics.fillTriangle( - offsetX - 8, 0, - offsetX + 8, 0, - offsetX, height + originX + offsetX - 8, 0, + originX + offsetX + 8, 0, + originX + offsetX, height ); } } else if (element.type === 'caveWall') { // Cave wall formation graphics.fillStyle(0x1A1025, 0.4); - graphics.fillRect(-element.size / 2, -100, element.size, 300); + graphics.fillRect( + originX - element.size / 2, + -100, + element.size, + 300 + ); // Add some texture graphics.fillStyle(0x2D1B4E, 0.3); for (let i = 0; i < 8; i++) { - const x = Phaser.Math.Between(-element.size / 2, element.size / 2); + const x = originX + Phaser.Math.Between( + -element.size / 2, + element.size / 2 + ); const y = Phaser.Math.Between(-80, 180); graphics.fillCircle(x, y, Phaser.Math.Between(5, 20)); } } - - graphics.setPosition(element.x, this.levelHeight - 300); - graphics.setScrollFactor(scrollFactor, 1); - graphics.setDepth(depth); }); + + graphics.setPosition(0, this.levelHeight - 300); + graphics.setScrollFactor(scrollFactor, 1); + graphics.setDepth(depth); + graphics.caveAmbientRole = 'parallaxLayer'; + this.caveParallaxLayers.push(graphics); + return graphics; } /** @@ -3525,15 +3542,20 @@ class CrystalCavesLevel extends PlatformerLevelScene { graphics.setPosition(x, y); graphics.setDepth(20); + graphics.caveAmbientRole = 'brokenLantern'; // Subtle flicker - this.tweens.add({ - targets: graphics, - alpha: { from: 0.6, to: 0.9 }, - duration: Phaser.Math.Between(200, 400), - yoyo: true, - repeat: -1 - }); + if (this.isMobile) { + graphics.setAlpha(0.78); + } else { + this.tweens.add({ + targets: graphics, + alpha: { from: 0.6, to: 0.9 }, + duration: Phaser.Math.Between(200, 400), + yoyo: true, + repeat: -1 + }); + } } /** @@ -3610,15 +3632,20 @@ class CrystalCavesLevel extends PlatformerLevelScene { graphics.setPosition(x, y); graphics.setDepth(10); + graphics.caveAmbientRole = 'minerSkeleton'; // Subtle ambient movement (settling dust) - this.tweens.add({ - targets: graphics, - alpha: { from: 0.8, to: 0.9 }, - duration: 3000, - yoyo: true, - repeat: -1 - }); + if (this.isMobile) { + graphics.setAlpha(0.85); + } else { + this.tweens.add({ + targets: graphics, + alpha: { from: 0.8, to: 0.9 }, + duration: 3000, + yoyo: true, + repeat: -1 + }); + } } /**