Add semi-random wind weather pattern
This commit is contained in:
parent
82eef3f26e
commit
b0b88f432c
@ -15,21 +15,72 @@ export const ENVIRONMENT_ISLANDS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const WIND_PATTERN_MS = 16000;
|
||||||
|
const WIND_MIN_STRENGTH = 7;
|
||||||
|
const WIND_MAX_STRENGTH = 24;
|
||||||
|
|
||||||
|
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
||||||
|
const smootherStep = (value) => {
|
||||||
|
const t = clamp01(value);
|
||||||
|
return t * t * t * (t * (t * 6 - 15) + 10);
|
||||||
|
};
|
||||||
|
const hash01 = (index, salt = 0) => {
|
||||||
|
const value = Math.sin(index * 12.9898 + salt * 78.233) * 43758.5453123;
|
||||||
|
return value - Math.floor(value);
|
||||||
|
};
|
||||||
|
const shortestAngleDelta = (from, to) => {
|
||||||
|
let delta = (to - from) % (Math.PI * 2);
|
||||||
|
if (delta > Math.PI) delta -= Math.PI * 2;
|
||||||
|
if (delta < -Math.PI) delta += Math.PI * 2;
|
||||||
|
return delta;
|
||||||
|
};
|
||||||
|
|
||||||
|
const windTargetForSlot = (slot) => ({
|
||||||
|
angle: hash01(slot, 1) * Math.PI * 2,
|
||||||
|
strength: WIND_MIN_STRENGTH
|
||||||
|
+ hash01(slot, 2) * (WIND_MAX_STRENGTH - WIND_MIN_STRENGTH),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const semiRandomWindPattern = (timeMs = 0) => {
|
||||||
|
const safeTime = Math.max(0, Number(timeMs) || 0);
|
||||||
|
const slot = Math.floor(safeTime / WIND_PATTERN_MS);
|
||||||
|
const phase = (safeTime % WIND_PATTERN_MS) / WIND_PATTERN_MS;
|
||||||
|
const current = windTargetForSlot(slot);
|
||||||
|
const next = windTargetForSlot(slot + 1);
|
||||||
|
|
||||||
|
// Hold each weather state briefly, then ease toward the next random target.
|
||||||
|
const blend = smootherStep((phase - 0.18) / 0.82);
|
||||||
|
const angle = current.angle
|
||||||
|
+ shortestAngleDelta(current.angle, next.angle) * blend;
|
||||||
|
const strength = current.strength
|
||||||
|
+ (next.strength - current.strength) * blend;
|
||||||
|
|
||||||
|
// A smaller deterministic gust keeps the air alive without frame-to-frame jitter.
|
||||||
|
const gust = Math.sin(safeTime / 2300 + slot * 0.73)
|
||||||
|
* (1.1 + hash01(slot, 3) * 1.8);
|
||||||
|
|
||||||
|
return {
|
||||||
|
angle,
|
||||||
|
strength: Math.max(4, strength + gust),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const environmentWindAt = ({
|
export const environmentWindAt = ({
|
||||||
x = 0,
|
x = 0,
|
||||||
y = 0,
|
y = 0,
|
||||||
timeMs = 0,
|
timeMs = 0,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const time = timeMs / 1000;
|
const time = timeMs / 1000;
|
||||||
const angle = 0.62
|
const pattern = semiRandomWindPattern(timeMs);
|
||||||
+ Math.sin(time / 29) * 0.2
|
|
||||||
+ Math.sin((x + y) / 880) * 0.06;
|
|
||||||
const strength = 14
|
|
||||||
+ Math.sin(time / 8.5) * 3.2
|
|
||||||
+ Math.sin(time / 2.9) * 1.4;
|
|
||||||
|
|
||||||
let windX = Math.cos(angle) * strength;
|
// Small spatial variation prevents the whole map feeling like one rigid conveyor belt.
|
||||||
let windY = Math.sin(angle) * strength;
|
const localAngle = pattern.angle
|
||||||
|
+ Math.sin((x + y) / 880 + time / 31) * 0.055;
|
||||||
|
const localStrength = pattern.strength
|
||||||
|
+ Math.sin((x - y) / 760 + time / 4.7) * 0.9;
|
||||||
|
|
||||||
|
let windX = Math.cos(localAngle) * localStrength;
|
||||||
|
let windY = Math.sin(localAngle) * localStrength;
|
||||||
|
|
||||||
for (const island of ENVIRONMENT_ISLANDS) {
|
for (const island of ENVIRONMENT_ISLANDS) {
|
||||||
const dx = x - island.x;
|
const dx = x - island.x;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import assert from 'node:assert/strict';
|
|||||||
import {
|
import {
|
||||||
ENVIRONMENT_ISLANDS,
|
ENVIRONMENT_ISLANDS,
|
||||||
environmentWindAt,
|
environmentWindAt,
|
||||||
|
semiRandomWindPattern,
|
||||||
velocityWithEnvironment,
|
velocityWithEnvironment,
|
||||||
} from '../src/environment-physics.js';
|
} from '../src/environment-physics.js';
|
||||||
|
|
||||||
@ -15,12 +16,39 @@ test('environment exposes collidable island geometry', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('semi-random wind varies strength and direction across weather windows', () => {
|
||||||
|
const samples = [0, 16000, 32000, 48000, 64000]
|
||||||
|
.map((timeMs) => semiRandomWindPattern(timeMs));
|
||||||
|
|
||||||
|
const roundedStrengths = new Set(samples.map(({ strength }) => Math.round(strength * 10)));
|
||||||
|
const roundedHeadings = new Set(samples.map(({ angle }) => Math.round(angle * 10)));
|
||||||
|
|
||||||
|
assert.ok(roundedStrengths.size >= 3);
|
||||||
|
assert.ok(roundedHeadings.size >= 3);
|
||||||
|
for (const sample of samples) {
|
||||||
|
assert.ok(sample.strength >= 4 && sample.strength < 28);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('semi-random wind changes smoothly instead of jumping each frame', () => {
|
||||||
|
const before = semiRandomWindPattern(15950);
|
||||||
|
const after = semiRandomWindPattern(16050);
|
||||||
|
const vector = ({ angle, strength }) => ({
|
||||||
|
x: Math.cos(angle) * strength,
|
||||||
|
y: Math.sin(angle) * strength,
|
||||||
|
});
|
||||||
|
const a = vector(before);
|
||||||
|
const b = vector(after);
|
||||||
|
|
||||||
|
assert.ok(Math.hypot(b.x - a.x, b.y - a.y) < 2.5);
|
||||||
|
});
|
||||||
|
|
||||||
test('ambient wind changes over time and remains bounded', () => {
|
test('ambient wind changes over time and remains bounded', () => {
|
||||||
const first = environmentWindAt({ x: 1200, y: 1200, timeMs: 0 });
|
const first = environmentWindAt({ x: 1200, y: 1200, timeMs: 0 });
|
||||||
const later = environmentWindAt({ x: 1200, y: 1200, timeMs: 18000 });
|
const later = environmentWindAt({ x: 1200, y: 1200, timeMs: 24000 });
|
||||||
|
|
||||||
assert.ok(first.strength > 5 && first.strength < 30);
|
assert.ok(first.strength > 3 && first.strength < 30);
|
||||||
assert.ok(later.strength > 5 && later.strength < 30);
|
assert.ok(later.strength > 3 && later.strength < 30);
|
||||||
assert.notDeepEqual(
|
assert.notDeepEqual(
|
||||||
[Math.round(first.x * 100), Math.round(first.y * 100)],
|
[Math.round(first.x * 100), Math.round(first.y * 100)],
|
||||||
[Math.round(later.x * 100), Math.round(later.y * 100)],
|
[Math.round(later.x * 100), Math.round(later.y * 100)],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user