Add dense cloud concealment
This commit is contained in:
parent
aff9f3f276
commit
cc4011ff25
@ -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,
|
||||||
|
|||||||
@ -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');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user