From 96b042a4c233f3f8086b56ef456d24a5c8203fc6 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 20 Sep 2026 08:13:14 +0000 Subject: [PATCH] Cycle dense clouds with changing wind --- src/game-v4.js | 62 ++++++++++++++++++++++++++++++++++++++----- tests/wiring.test.mjs | 26 ++++++++++-------- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/game-v4.js b/src/game-v4.js index 9870ee9..01a9a69 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -44,6 +44,9 @@ const PLAYER_SMOKE_ENGINE_OFFSET = 17; const ROLLING_CLOUD_COUNT = 34; const ROLLING_CLOUD_WRAP_MARGIN = 180; const DENSE_CLOUD_MIN_COUNT = 8; +const DENSE_CLOUD_MIN_SCALE = 2.2; +const DENSE_CLOUD_MAX_SCALE = 3.9; +const DENSE_CLOUD_CONCEALMENT_RADIUS_SCALE = 82; const CLOUD_FIRE_SUPPRESSION_CONCEALMENT = 0.55; const CLOUD_CLOSE_ENGAGE_RANGE = 180; const AUTO_RESET_MS = 2200; @@ -210,15 +213,15 @@ class GameScene extends Phaser.Scene { const denseCloud = i < DENSE_CLOUD_MIN_COUNT || Phaser.Math.Between(0, 5) === 0; const foreground = denseCloud || Phaser.Math.Between(0, 4) === 0; const baseScale = denseCloud - ? Phaser.Math.FloatBetween(1.55, 2.85) + ? Phaser.Math.FloatBetween(DENSE_CLOUD_MIN_SCALE, DENSE_CLOUD_MAX_SCALE) : Phaser.Math.FloatBetween(0.85, 2.35); const alphaMin = denseCloud - ? Phaser.Math.FloatBetween(0.34, 0.46) + ? Phaser.Math.FloatBetween(0.4, 0.52) : foreground ? Phaser.Math.FloatBetween(0.08, 0.13) : Phaser.Math.FloatBetween(0.025, 0.07); const alphaMax = denseCloud - ? Phaser.Math.FloatBetween(0.64, 0.82) + ? Phaser.Math.FloatBetween(0.72, 0.9) : foreground ? Phaser.Math.FloatBetween(0.18, 0.3) : Phaser.Math.FloatBetween(0.08, 0.16); @@ -243,11 +246,18 @@ class GameScene extends Phaser.Scene { 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)); - cloud.setData('concealmentRadius', denseCloud ? baseScale * 58 : 0); + cloud.setData( + 'concealmentRadius', + denseCloud ? baseScale * DENSE_CLOUD_CONCEALMENT_RADIUS_SCALE : 0, + ); cloud.setData( 'concealmentStrength', denseCloud ? Phaser.Math.FloatBetween(0.82, 0.98) : 0, ); + cloud.setData('presence', denseCloud ? Phaser.Math.FloatBetween(0.25, 1) : 1); + cloud.setData('weatherCycleMs', Phaser.Math.Between(18000, 36000)); + cloud.setData('weatherPhase', Phaser.Math.FloatBetween(0, Math.PI * 2)); + cloud.setData('weatherCycleIndex', -1); this.rollingClouds.push(cloud); } @@ -274,7 +284,28 @@ class GameScene extends Phaser.Scene { 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)); + + let presence = 1; + if (cloud.getData('denseCloud')) { + const weatherCycleMs = cloud.getData('weatherCycleMs') || 26000; + const weatherPhase = cloud.getData('weatherPhase') || 0; + const cyclePosition = (now % weatherCycleMs) / weatherCycleMs; + const weatherWave = 0.5 + 0.5 * Math.sin(cyclePosition * Math.PI * 2 + weatherPhase); + const windPresence = clamp((wind.strength - 5) / 18, 0, 1); + const targetPresence = clamp(weatherWave * 0.72 + windPresence * 0.38 - 0.12, 0, 1); + const previousPresence = cloud.getData('presence') ?? targetPresence; + presence = Phaser.Math.Linear(previousPresence, targetPresence, Math.min(1, dt * 0.7)); + cloud.setData('presence', presence); + + const cycleIndex = Math.floor(now / weatherCycleMs); + const previousCycleIndex = cloud.getData('weatherCycleIndex'); + if (presence < 0.06 && cycleIndex !== previousCycleIndex) { + this.reseedDenseCloudUpwind(cloud, wind); + cloud.setData('weatherCycleIndex', cycleIndex); + } + } + + cloud.setAlpha(Phaser.Math.Linear(alphaMin, alphaMax, visibility) * presence); const baseScale = cloud.getData('baseScale') || 1; const rollPhase = cloud.getData('rollPhase') || 0; @@ -289,6 +320,24 @@ class GameScene extends Phaser.Scene { } + + reseedDenseCloudUpwind(cloud, wind) { + const magnitude = Math.max(1, Math.hypot(wind?.x || 0, wind?.y || 0)); + const windX = (wind?.x || 1) / magnitude; + const windY = (wind?.y || 0) / magnitude; + const margin = ROLLING_CLOUD_WRAP_MARGIN; + const crossOffset = Phaser.Math.Between(-Math.round(WORLD_SIZE * 0.45), Math.round(WORLD_SIZE * 0.45)); + const centerX = WORLD_SIZE / 2; + const centerY = WORLD_SIZE / 2; + const sideX = -windY; + const sideY = windX; + + cloud.x = centerX - windX * (WORLD_SIZE / 2 + margin) + sideX * crossOffset; + cloud.y = centerY - windY * (WORLD_SIZE / 2 + margin) + sideY * crossOffset; + cloud.setData('driftScale', Phaser.Math.FloatBetween(0.65, 1.45)); + cloud.setData('weatherPhase', Phaser.Math.FloatBetween(0, Math.PI * 2)); + } + cloudConcealmentAt(x, y) { if (!this.rollingClouds?.length) return 0; @@ -304,7 +353,8 @@ class GameScene extends Phaser.Scene { const radialStrength = 1 - (distance / radius); const strength = cloud.getData('concealmentStrength') || 0; - concealment = Math.max(concealment, radialStrength * strength); + const presence = cloud.getData('presence') ?? 1; + concealment = Math.max(concealment, radialStrength * strength * presence); } return clamp(concealment, 0, 1); diff --git a/tests/wiring.test.mjs b/tests/wiring.test.mjs index 717299c..0d3b3f8 100644 --- a/tests/wiring.test.mjs +++ b/tests/wiring.test.mjs @@ -260,21 +260,23 @@ test('environment physics adds wind, soft island collision, turbulence, and proj assert.match(source, /windSpeed: this\.environmentWind\?\.strength/); }); -test('rolling clouds include dense concealment banks that drift with wind', async () => { +test('rolling clouds include large dense banks that phase in and out with the wind', async () => { const source = await read('src/game-v4.js'); assert.match(source, /const ROLLING_CLOUD_COUNT = 34/); assert.match(source, /const DENSE_CLOUD_MIN_COUNT = 8/); - assert.match(source, /this\.createRollingClouds\(\)/); - assert.match(source, /const denseCloud = i < DENSE_CLOUD_MIN_COUNT/); - assert.match(source, /setDepth\(denseCloud \? 8 : foreground \? 7 : 2\)/); - assert.match(source, /Phaser\.BlendModes\.NORMAL : Phaser\.BlendModes\.SCREEN/); - assert.match(source, /cloud\.setData\('concealmentRadius', denseCloud \? baseScale \* 58 : 0\)/); - assert.match(source, /cloud\.setData\(\s*'concealmentStrength'/); - assert.match(source, /this\.updateRollingClouds\(deltaMs\)/); - assert.match(source, /const wind = environmentWindAt\(\{/); - assert.match(source, /cloud\.x \+= wind\.x \* driftScale \* dt/); - assert.match(source, /cloud\.setAlpha\(Phaser\.Math\.Linear\(alphaMin, alphaMax, visibility\)\)/); + assert.match(source, /const DENSE_CLOUD_MIN_SCALE = 2\.2/); + assert.match(source, /const DENSE_CLOUD_MAX_SCALE = 3\.9/); + assert.match(source, /const DENSE_CLOUD_CONCEALMENT_RADIUS_SCALE = 82/); + assert.match(source, /Phaser\.Math\.FloatBetween\(DENSE_CLOUD_MIN_SCALE, DENSE_CLOUD_MAX_SCALE\)/); + assert.match(source, /cloud\.setData\(\s*'concealmentRadius',[\s\S]*?DENSE_CLOUD_CONCEALMENT_RADIUS_SCALE/); + assert.match(source, /cloud\.setData\('weatherCycleMs', Phaser\.Math\.Between\(18000, 36000\)\)/); + assert.match(source, /const windPresence = clamp\(\(wind\.strength - 5\) \/ 18, 0, 1\)/); + assert.match(source, /const targetPresence = clamp\(weatherWave \* 0\.72 \+ windPresence \* 0\.38 - 0\.12, 0, 1\)/); + assert.match(source, /cloud\.setAlpha\(Phaser\.Math\.Linear\(alphaMin, alphaMax, visibility\) \* presence\)/); + assert.match(source, /this\.reseedDenseCloudUpwind\(cloud, wind\)/); + assert.match(source, /cloud\.x = centerX - windX \* \(WORLD_SIZE \/ 2 \+ margin\)/); + assert.match(source, /cloud\.y = centerY - windY \* \(WORLD_SIZE \/ 2 \+ margin\)/); assert.doesNotMatch(source, /const clouds = this\.add\.graphics\(\)/); }); @@ -288,6 +290,8 @@ test('dense clouds conceal ships and suppress long-range pirate fire', async () assert.match(source, /this\.player\.setAlpha\(Phaser\.Math\.Linear\(1, 0\.3, playerConcealment\)\)/); assert.match(source, /pirate\.setAlpha\(Phaser\.Math\.Linear\(1, 0\.36, concealment\)\)/); assert.match(source, /this\.player\.setData\('cloudConcealment', playerConcealment\)/); + assert.match(source, /const presence = cloud\.getData\('presence'\) \?\? 1/); + assert.match(source, /radialStrength \* strength \* presence/); assert.match(source, /const targetObscured = playerConcealment >= CLOUD_FIRE_SUPPRESSION_CONCEALMENT/); assert.match(source, /distance > CLOUD_CLOSE_ENGAGE_RANGE/); assert.match(source, /if \(!targetObscured && distance < broadsideRange/);