Cycle dense clouds with changing wind

This commit is contained in:
Admin 2026-09-20 08:13:14 +00:00
parent cc4011ff25
commit 96b042a4c2
2 changed files with 71 additions and 17 deletions

View File

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

View File

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