Add rolling wind-driven clouds

This commit is contained in:
Admin 2026-09-20 07:47:40 +00:00
parent f3eb0dfaaf
commit 82eef3f26e
2 changed files with 105 additions and 11 deletions

View File

@ -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();

View File

@ -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');