From 82eef3f26e25bad3adbc2a660e01fa8f0792c3cb Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 20 Sep 2026 07:47:40 +0000 Subject: [PATCH] Add rolling wind-driven clouds --- src/game-v4.js | 99 ++++++++++++++++++++++++++++++++++++++----- tests/wiring.test.mjs | 17 ++++++++ 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/game-v4.js b/src/game-v4.js index 8f25a6e..31ab38e 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -41,6 +41,8 @@ const MAX_PIRATES = 6; const HUD_PUBLISH_INTERVAL_MS = 50; const PLAYER_SMOKE_REAR_OFFSET = 48; const PLAYER_SMOKE_ENGINE_OFFSET = 17; +const ROLLING_CLOUD_COUNT = 34; +const ROLLING_CLOUD_WRAP_MARGIN = 180; const AUTO_RESET_MS = 2200; const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); @@ -101,6 +103,7 @@ class GameScene extends Phaser.Scene { create() { this.physics.world.setBounds(0, 0, WORLD_SIZE, WORLD_SIZE); this.drawWorld(); + this.createRollingClouds(); this.createAnimations(); this.player = this.physics.add.sprite(1200, 1550, 'player-airship', 0); @@ -184,6 +187,90 @@ class GameScene extends Phaser.Scene { + + createRollingClouds() { + if (!this.textures.exists('rolling-cloud')) { + const cloudTexture = this.make.graphics({ x: 0, y: 0, add: false }); + cloudTexture.fillStyle(0xe7f0f4, 0.34); + cloudTexture.fillEllipse(64, 42, 92, 46); + cloudTexture.fillStyle(0xf5f8fa, 0.26); + cloudTexture.fillCircle(42, 38, 27); + cloudTexture.fillCircle(67, 29, 31); + cloudTexture.fillCircle(88, 40, 25); + cloudTexture.generateTexture('rolling-cloud', 128, 84); + cloudTexture.destroy(); + } + + this.rollingClouds = []; + for (let i = 0; i < ROLLING_CLOUD_COUNT; i++) { + const foreground = Phaser.Math.Between(0, 4) === 0; + const baseScale = Phaser.Math.FloatBetween(0.85, 2.35); + const alphaMin = foreground + ? Phaser.Math.FloatBetween(0.08, 0.13) + : Phaser.Math.FloatBetween(0.025, 0.07); + const alphaMax = foreground + ? Phaser.Math.FloatBetween(0.18, 0.3) + : Phaser.Math.FloatBetween(0.08, 0.16); + const cloud = this.add.image( + Phaser.Math.Between(-ROLLING_CLOUD_WRAP_MARGIN, WORLD_SIZE + ROLLING_CLOUD_WRAP_MARGIN), + Phaser.Math.Between(-ROLLING_CLOUD_WRAP_MARGIN, WORLD_SIZE + ROLLING_CLOUD_WRAP_MARGIN), + 'rolling-cloud', + ); + + cloud + .setDepth(foreground ? 7 : 2) + .setScale(baseScale) + .setAlpha(alphaMin) + .setBlendMode(Phaser.BlendModes.SCREEN); + + cloud.setData('foreground', foreground); + cloud.setData('baseScale', baseScale); + cloud.setData('alphaMin', alphaMin); + cloud.setData('alphaMax', alphaMax); + cloud.setData('alphaPhase', Phaser.Math.FloatBetween(0, Math.PI * 2)); + cloud.setData('alphaRate', Phaser.Math.FloatBetween(0.00018, 0.00048)); + cloud.setData('rollPhase', Phaser.Math.FloatBetween(0, Math.PI * 2)); + cloud.setData('driftScale', Phaser.Math.FloatBetween(0.55, 1.35)); + + this.rollingClouds.push(cloud); + } + } + + updateRollingClouds(deltaMs) { + if (!this.rollingClouds?.length) return; + + const dt = Math.min(deltaMs / 1000, 0.05); + const now = this.time?.now ?? 0; + + for (const cloud of this.rollingClouds) { + const wind = environmentWindAt({ + x: cloud.x, + y: cloud.y, + timeMs: now, + }); + const driftScale = cloud.getData('driftScale') || 1; + cloud.x += wind.x * driftScale * dt; + cloud.y += wind.y * driftScale * dt; + + const phase = cloud.getData('alphaPhase') || 0; + const rate = cloud.getData('alphaRate') || 0.0003; + const alphaMin = cloud.getData('alphaMin') || 0.04; + const alphaMax = cloud.getData('alphaMax') || 0.14; + const visibility = 0.5 + 0.5 * Math.sin(now * rate + phase); + cloud.setAlpha(Phaser.Math.Linear(alphaMin, alphaMax, visibility)); + + const baseScale = cloud.getData('baseScale') || 1; + const rollPhase = cloud.getData('rollPhase') || 0; + const roll = 1 + Math.sin(now * 0.00022 + rollPhase) * 0.045; + cloud.setScale(baseScale * roll, baseScale / roll); + + if (cloud.x > WORLD_SIZE + ROLLING_CLOUD_WRAP_MARGIN) cloud.x = -ROLLING_CLOUD_WRAP_MARGIN; + if (cloud.y > WORLD_SIZE + ROLLING_CLOUD_WRAP_MARGIN) cloud.y = -ROLLING_CLOUD_WRAP_MARGIN; + if (cloud.x < -ROLLING_CLOUD_WRAP_MARGIN) cloud.x = WORLD_SIZE + ROLLING_CLOUD_WRAP_MARGIN; + if (cloud.y < -ROLLING_CLOUD_WRAP_MARGIN) cloud.y = WORLD_SIZE + ROLLING_CLOUD_WRAP_MARGIN; + } + } + createEnvironmentPhysics() { if (!this.textures.exists('environment-collider')) { const colliderTexture = this.make.graphics({ x: 0, y: 0, add: false }); @@ -476,17 +563,6 @@ class GameScene extends Phaser.Scene { for (let x = 0; x <= WORLD_SIZE; x += 100) bg.lineBetween(x, 0, x, WORLD_SIZE); for (let y = 0; y <= WORLD_SIZE; y += 100) bg.lineBetween(0, y, WORLD_SIZE, y); - const clouds = this.add.graphics(); - for (let i = 0; i < 46; i++) { - const x = Phaser.Math.Between(80, WORLD_SIZE - 80); - const y = Phaser.Math.Between(80, WORLD_SIZE - 80); - const r = Phaser.Math.Between(24, 62); - clouds.fillStyle(0xbcd2df, Phaser.Math.FloatBetween(0.025, 0.08)); - clouds.fillCircle(x, y, r); - clouds.fillCircle(x + r * 0.55, y + 8, r * 0.7); - clouds.fillCircle(x - r * 0.45, y + 5, r * 0.6); - } - const island = this.add.graphics(); island.fillStyle(0x5d694e, 1); island.fillEllipse(430, 480, 230, 150); @@ -541,6 +617,7 @@ class GameScene extends Phaser.Scene { this.updatePlayer(dt, deltaMs); this.updatePlayerSmoke(deltaMs); + this.updateRollingClouds(deltaMs); this.updatePirates(dt); this.updateProjectiles(); this.drawWeaponArcs(); diff --git a/tests/wiring.test.mjs b/tests/wiring.test.mjs index aa8765e..5434acd 100644 --- a/tests/wiring.test.mjs +++ b/tests/wiring.test.mjs @@ -249,6 +249,23 @@ test('environment physics adds wind, soft island collision, turbulence, and proj assert.match(source, /windSpeed: this\.environmentWind\?\.strength/); }); +test('rolling clouds drift with wind and vary visibility over time', async () => { + const source = await read('src/game-v4.js'); + + assert.match(source, /const ROLLING_CLOUD_COUNT = 34/); + assert.match(source, /this\.createRollingClouds\(\)/); + assert.match(source, /this\.updateRollingClouds\(deltaMs\)/); + assert.match(source, /this\.add\.image\([\s\S]*?'rolling-cloud'/); + assert.match(source, /setDepth\(foreground \? 7 : 2\)/); + assert.match(source, /setBlendMode\(Phaser\.BlendModes\.SCREEN\)/); + assert.match(source, /const wind = environmentWindAt\(\{/); + assert.match(source, /cloud\.x \+= wind\.x \* driftScale \* dt/); + assert.match(source, /const visibility = 0\.5 \+ 0\.5 \* Math\.sin/); + assert.match(source, /cloud\.setAlpha\(Phaser\.Math\.Linear\(alphaMin, alphaMax, visibility\)\)/); + assert.match(source, /cloud\.setScale\(baseScale \* roll, baseScale \/ roll\)/); + assert.doesNotMatch(source, /const clouds = this\.add\.graphics\(\)/); +}); + test('mobile HUD skips hidden Phaser rendering and throttles DOM state publishing', async () => { const source = await read('src/game-v4.js');