Add dense cloud concealment

This commit is contained in:
Admin 2026-09-20 08:02:23 +00:00
parent aff9f3f276
commit cc4011ff25
2 changed files with 92 additions and 17 deletions

View File

@ -43,6 +43,9 @@ const PLAYER_SMOKE_REAR_OFFSET = 48;
const PLAYER_SMOKE_ENGINE_OFFSET = 17; const PLAYER_SMOKE_ENGINE_OFFSET = 17;
const ROLLING_CLOUD_COUNT = 34; const ROLLING_CLOUD_COUNT = 34;
const ROLLING_CLOUD_WRAP_MARGIN = 180; const ROLLING_CLOUD_WRAP_MARGIN = 180;
const DENSE_CLOUD_MIN_COUNT = 8;
const CLOUD_FIRE_SUPPRESSION_CONCEALMENT = 0.55;
const CLOUD_CLOSE_ENGAGE_RANGE = 180;
const AUTO_RESET_MS = 2200; const AUTO_RESET_MS = 2200;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
@ -204,14 +207,21 @@ class GameScene extends Phaser.Scene {
this.rollingClouds = []; this.rollingClouds = [];
for (let i = 0; i < ROLLING_CLOUD_COUNT; i++) { for (let i = 0; i < ROLLING_CLOUD_COUNT; i++) {
const foreground = Phaser.Math.Between(0, 4) === 0; const denseCloud = i < DENSE_CLOUD_MIN_COUNT || Phaser.Math.Between(0, 5) === 0;
const baseScale = Phaser.Math.FloatBetween(0.85, 2.35); const foreground = denseCloud || Phaser.Math.Between(0, 4) === 0;
const alphaMin = foreground const baseScale = denseCloud
? Phaser.Math.FloatBetween(0.08, 0.13) ? Phaser.Math.FloatBetween(1.55, 2.85)
: Phaser.Math.FloatBetween(0.025, 0.07); : Phaser.Math.FloatBetween(0.85, 2.35);
const alphaMax = foreground const alphaMin = denseCloud
? Phaser.Math.FloatBetween(0.18, 0.3) ? Phaser.Math.FloatBetween(0.34, 0.46)
: Phaser.Math.FloatBetween(0.08, 0.16); : 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)
: foreground
? Phaser.Math.FloatBetween(0.18, 0.3)
: Phaser.Math.FloatBetween(0.08, 0.16);
const cloud = this.add.image( 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),
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),
@ -219,11 +229,12 @@ class GameScene extends Phaser.Scene {
); );
cloud cloud
.setDepth(foreground ? 7 : 2) .setDepth(denseCloud ? 8 : foreground ? 7 : 2)
.setScale(baseScale) .setScale(baseScale)
.setAlpha(alphaMin) .setAlpha(alphaMin)
.setBlendMode(Phaser.BlendModes.SCREEN); .setBlendMode(denseCloud ? Phaser.BlendModes.NORMAL : Phaser.BlendModes.SCREEN);
cloud.setData('denseCloud', denseCloud);
cloud.setData('foreground', foreground); cloud.setData('foreground', foreground);
cloud.setData('baseScale', baseScale); cloud.setData('baseScale', baseScale);
cloud.setData('alphaMin', alphaMin); cloud.setData('alphaMin', alphaMin);
@ -232,6 +243,11 @@ class GameScene extends Phaser.Scene {
cloud.setData('alphaRate', Phaser.Math.FloatBetween(0.00018, 0.00048)); cloud.setData('alphaRate', Phaser.Math.FloatBetween(0.00018, 0.00048));
cloud.setData('rollPhase', Phaser.Math.FloatBetween(0, Math.PI * 2)); cloud.setData('rollPhase', Phaser.Math.FloatBetween(0, Math.PI * 2));
cloud.setData('driftScale', Phaser.Math.FloatBetween(0.55, 1.35)); cloud.setData('driftScale', Phaser.Math.FloatBetween(0.55, 1.35));
cloud.setData('concealmentRadius', denseCloud ? baseScale * 58 : 0);
cloud.setData(
'concealmentStrength',
denseCloud ? Phaser.Math.FloatBetween(0.82, 0.98) : 0,
);
this.rollingClouds.push(cloud); this.rollingClouds.push(cloud);
} }
@ -272,6 +288,43 @@ class GameScene extends Phaser.Scene {
} }
} }
cloudConcealmentAt(x, y) {
if (!this.rollingClouds?.length) return 0;
let concealment = 0;
for (const cloud of this.rollingClouds) {
if (!cloud.getData('denseCloud')) continue;
const radius = cloud.getData('concealmentRadius') || 0;
if (radius <= 0) continue;
const distance = Phaser.Math.Distance.Between(x, y, cloud.x, cloud.y);
if (distance >= radius) continue;
const radialStrength = 1 - (distance / radius);
const strength = cloud.getData('concealmentStrength') || 0;
concealment = Math.max(concealment, radialStrength * strength);
}
return clamp(concealment, 0, 1);
}
updateCloudConcealment() {
if (!this.player?.active) return;
const playerConcealment = this.cloudConcealmentAt(this.player.x, this.player.y);
this.player.setAlpha(Phaser.Math.Linear(1, 0.3, playerConcealment));
this.player.setData('cloudConcealment', playerConcealment);
for (const pirate of this.pirates?.getChildren?.() || []) {
if (!pirate?.active) continue;
const concealment = this.cloudConcealmentAt(pirate.x, pirate.y);
pirate.setAlpha(Phaser.Math.Linear(1, 0.36, concealment));
pirate.setData('cloudConcealment', concealment);
}
}
createEnvironmentPhysics() { createEnvironmentPhysics() {
if (!this.textures.exists('environment-collider')) { if (!this.textures.exists('environment-collider')) {
const colliderTexture = this.make.graphics({ x: 0, y: 0, add: false }); const colliderTexture = this.make.graphics({ x: 0, y: 0, add: false });
@ -619,6 +672,7 @@ class GameScene extends Phaser.Scene {
this.updatePlayer(dt, deltaMs); this.updatePlayer(dt, deltaMs);
this.updatePlayerSmoke(deltaMs); this.updatePlayerSmoke(deltaMs);
this.updateRollingClouds(deltaMs); this.updateRollingClouds(deltaMs);
this.updateCloudConcealment();
this.updatePirates(dt); this.updatePirates(dt);
this.updateProjectiles(); this.updateProjectiles();
this.drawWeaponArcs(); this.drawWeaponArcs();
@ -758,8 +812,11 @@ class GameScene extends Phaser.Scene {
const broadsideTolerance = pirate.getData('broadsideError') || 38; const broadsideTolerance = pirate.getData('broadsideError') || 38;
const reloadMs = pirate.getData('reloadMs') || 1650; const reloadMs = pirate.getData('reloadMs') || 1650;
const projectileDamage = pirate.getData('projectileDamage') || DEFAULT_PIRATE_SHOT_DAMAGE; const projectileDamage = pirate.getData('projectileDamage') || DEFAULT_PIRATE_SHOT_DAMAGE;
const playerConcealment = this.player.getData('cloudConcealment') || 0;
const targetObscured = playerConcealment >= CLOUD_FIRE_SUPPRESSION_CONCEALMENT
&& distance > CLOUD_CLOSE_ENGAGE_RANGE;
if (distance < broadsideRange && broadsideError < broadsideTolerance && this.time.now - lastShot > reloadMs) { if (!targetObscured && distance < broadsideRange && broadsideError < broadsideTolerance && this.time.now - lastShot > reloadMs) {
const side = relative >= 0 ? 1 : -1; const side = relative >= 0 ? 1 : -1;
this.fireBroadside(pirate, side, this.pirateShots, 'cannonball', projectileDamage, 0.9); this.fireBroadside(pirate, side, this.pirateShots, 'cannonball', projectileDamage, 0.9);
pirate.setData('lastShot', this.time.now); pirate.setData('lastShot', this.time.now);
@ -1064,6 +1121,7 @@ class GameScene extends Phaser.Scene {
environment: { environment: {
windSpeed: this.environmentWind?.strength || 0, windSpeed: this.environmentWind?.strength || 0,
windHeading: this.environmentWind?.heading || 0, windHeading: this.environmentWind?.heading || 0,
concealment: this.player?.getData?.('cloudConcealment') || 0,
}, },
radar: activePirates.slice(0, 10).map((pirate) => ({ radar: activePirates.slice(0, 10).map((pirate) => ({
dx: (pirate.x - player.x) / 900, dx: (pirate.x - player.x) / 900,

View File

@ -260,23 +260,40 @@ test('environment physics adds wind, soft island collision, turbulence, and proj
assert.match(source, /windSpeed: this\.environmentWind\?\.strength/); assert.match(source, /windSpeed: this\.environmentWind\?\.strength/);
}); });
test('rolling clouds drift with wind and vary visibility over time', async () => { test('rolling clouds include dense concealment banks that drift with wind', async () => {
const source = await read('src/game-v4.js'); const source = await read('src/game-v4.js');
assert.match(source, /const ROLLING_CLOUD_COUNT = 34/); 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, /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, /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, /const wind = environmentWindAt\(\{/);
assert.match(source, /cloud\.x \+= wind\.x \* driftScale \* dt/); 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\.setAlpha\(Phaser\.Math\.Linear\(alphaMin, alphaMax, visibility\)\)/);
assert.match(source, /cloud\.setScale\(baseScale \* roll, baseScale \/ roll\)/);
assert.doesNotMatch(source, /const clouds = this\.add\.graphics\(\)/); assert.doesNotMatch(source, /const clouds = this\.add\.graphics\(\)/);
}); });
test('dense clouds conceal ships and suppress long-range pirate fire', async () => {
const source = await read('src/game-v4.js');
assert.match(source, /cloudConcealmentAt\(x, y\)/);
assert.match(source, /cloud\.getData\('denseCloud'\)/);
assert.match(source, /Phaser\.Math\.Distance\.Between\(x, y, cloud\.x, cloud\.y\)/);
assert.match(source, /this\.updateCloudConcealment\(\)/);
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 targetObscured = playerConcealment >= CLOUD_FIRE_SUPPRESSION_CONCEALMENT/);
assert.match(source, /distance > CLOUD_CLOSE_ENGAGE_RANGE/);
assert.match(source, /if \(!targetObscured && distance < broadsideRange/);
assert.match(source, /concealment: this\.player\?\.getData\?\.\('cloudConcealment'\) \|\| 0/);
});
test('mobile HUD skips hidden Phaser rendering and throttles DOM state publishing', async () => { test('mobile HUD skips hidden Phaser rendering and throttles DOM state publishing', async () => {
const source = await read('src/game-v4.js'); const source = await read('src/game-v4.js');