Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion scripts/smoke-secondary-journeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/LevelTraversalQualityContract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
97 changes: 62 additions & 35 deletions src/scenes/levels/CrystalCavesLevel.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ class CrystalCavesLevel extends PlatformerLevelScene {
this.caveCoinLayerTween = null;
this.caveCrystalField = [];
this.caveCrystalFieldLayer = null;
this.caveParallaxLayers = [];

// Project Beacon expedition state
this.beaconAnchors = [];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand All @@ -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') {
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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
});
}
}

/**
Expand Down Expand Up @@ -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
});
}
}

/**
Expand Down