Add route-aware wind and moving storm fronts

This commit is contained in:
Admin 2026-09-21 06:33:17 +00:00
parent e5571b9dee
commit 78eec83c91
2 changed files with 198 additions and 7 deletions

View File

@ -18,6 +18,10 @@ export const ENVIRONMENT_ISLANDS = [
const WIND_PATTERN_MS = 16000; const WIND_PATTERN_MS = 16000;
const WIND_MIN_STRENGTH = 7; const WIND_MIN_STRENGTH = 7;
const WIND_MAX_STRENGTH = 24; const WIND_MAX_STRENGTH = 24;
const ROUTE_MIN_CORE_RADIUS = 220;
const ROUTE_EDGE_DEPTH = 320;
const STORM_FRONT_PERIOD_MS = 110000;
const STORM_FRONT_RADIUS = 720;
const clamp01 = (value) => Math.max(0, Math.min(1, value)); const clamp01 = (value) => Math.max(0, Math.min(1, value));
const smootherStep = (value) => { const smootherStep = (value) => {
@ -65,20 +69,142 @@ export const semiRandomWindPattern = (timeMs = 0) => {
}; };
}; };
const pointSegmentDistance = (point, from, to) => {
const abX = (to?.x || 0) - (from?.x || 0);
const abY = (to?.y || 0) - (from?.y || 0);
const apX = (point?.x || 0) - (from?.x || 0);
const apY = (point?.y || 0) - (from?.y || 0);
const lengthSquared = abX * abX + abY * abY;
if (lengthSquared <= 1) return Math.hypot(apX, apY);
const t = Math.max(0, Math.min(1, (apX * abX + apY * abY) / lengthSquared));
const nearestX = (from?.x || 0) + abX * t;
const nearestY = (from?.y || 0) + abY * t;
return Math.hypot((point?.x || 0) - nearestX, (point?.y || 0) - nearestY);
};
export const routeWeatherAt = ({
x = 0,
y = 0,
routes = [],
} = {}) => {
let nearestRoute = null;
let routeDistance = Infinity;
for (const route of routes || []) {
const distance = pointSegmentDistance({ x, y }, route?.from, route?.to);
if (distance < routeDistance) {
nearestRoute = route;
routeDistance = distance;
}
}
if (!nearestRoute) {
return {
routeId: null,
routeDistance: Infinity,
routeInfluence: 0,
routeEdge: 0,
openExposure: 1,
zone: 'open-sky',
};
}
const coreRadius = Math.max(
ROUTE_MIN_CORE_RADIUS,
Math.max(0, Number(nearestRoute.width) || 0) * 0.9,
);
const outerRadius = coreRadius + ROUTE_EDGE_DEPTH;
const calmStart = coreRadius * 0.65;
const routeInfluence = 1 - smootherStep(
(routeDistance - calmStart) / Math.max(1, outerRadius - calmStart),
);
const edgeCenter = coreRadius + ROUTE_EDGE_DEPTH * 0.5;
const edgeHalfWidth = ROUTE_EDGE_DEPTH * 0.62;
const routeEdge = smootherStep(
1 - Math.abs(routeDistance - edgeCenter) / edgeHalfWidth,
);
const openExposure = 1 - routeInfluence;
return {
routeId: nearestRoute.id || null,
routeDistance,
routeInfluence: clamp01(routeInfluence),
routeEdge: clamp01(routeEdge),
openExposure: clamp01(openExposure),
zone: routeInfluence >= 0.72
? 'shipping-lane'
: routeInfluence >= 0.08
? 'route-edge'
: 'open-sky',
};
};
export const stormFrontAt = ({
x = 0,
y = 0,
timeMs = 0,
worldWidth = 5400,
worldHeight = 5400,
} = {}) => {
const period = STORM_FRONT_PERIOD_MS;
const raw = ((Number(timeMs) || 0) % period + period) % period;
const phase = raw / period;
const frontIntensity = (offset, yBase, yWave, scale = 1) => {
const t = (phase + offset) % 1;
const centerX = -900 + t * (worldWidth + 1800);
const centerY = worldHeight * (
yBase + Math.sin(t * Math.PI * 2 + offset * 4.7) * yWave
);
const distance = Math.hypot(
(x - centerX) / 1.55,
y - centerY,
);
return clamp01(1 - distance / STORM_FRONT_RADIUS) * scale;
};
return Math.max(
frontIntensity(0, 0.35, 0.18, 0.95),
frontIntensity(0.53, 0.72, 0.13, 0.74),
);
};
export const environmentWindAt = ({ export const environmentWindAt = ({
x = 0, x = 0,
y = 0, y = 0,
timeMs = 0, timeMs = 0,
islands = ENVIRONMENT_ISLANDS, islands = ENVIRONMENT_ISLANDS,
routes = [],
worldWidth = 5400,
worldHeight = 5400,
} = {}) => { } = {}) => {
const time = timeMs / 1000; const time = timeMs / 1000;
const pattern = semiRandomWindPattern(timeMs); const pattern = semiRandomWindPattern(timeMs);
const routeWeather = routeWeatherAt({ x, y, routes });
const stormIntensity = stormFrontAt({
x,
y,
timeMs,
worldWidth,
worldHeight,
});
// Small spatial variation prevents the whole map feeling like one rigid conveyor belt. // Shipping lanes damp the broad wind pattern. Their edges are more unstable,
// while open sky and moving storm fronts amplify both strength and direction.
const directionalVariance = 0.022
+ routeWeather.openExposure * 0.09
+ routeWeather.routeEdge * 0.055
+ stormIntensity * 0.2;
const localAngle = pattern.angle const localAngle = pattern.angle
+ Math.sin((x + y) / 880 + time / 31) * 0.055; + Math.sin((x + y) / 880 + time / 31) * directionalVariance
const localStrength = pattern.strength + Math.sin(time / 5.2 + x / 1400) * stormIntensity * 0.12;
+ Math.sin((x - y) / 760 + time / 4.7) * 0.9; const strengthScale = 0.72
+ routeWeather.openExposure * 0.5
+ routeWeather.routeEdge * 0.16
+ stormIntensity * 0.68;
const localStrength = pattern.strength * strengthScale
+ Math.sin((x - y) / 760 + time / 4.7)
* (0.45 + routeWeather.openExposure * 0.9 + stormIntensity * 1.2);
let windX = Math.cos(localAngle) * localStrength; let windX = Math.cos(localAngle) * localStrength;
let windY = Math.sin(localAngle) * localStrength; let windY = Math.sin(localAngle) * localStrength;
@ -101,11 +227,23 @@ export const environmentWindAt = ({
windY += normalY * updraft + tangentY * swirl; windY += normalY * updraft + tangentY * swirl;
} }
const strength = Math.hypot(windX, windY);
const zone = stormIntensity >= 0.55
? 'storm'
: routeWeather.zone;
return { return {
x: windX, x: windX,
y: windY, y: windY,
strength: Math.hypot(windX, windY), strength,
heading: ((Math.atan2(windY, windX) * 180 / Math.PI) + 450) % 360, heading: ((Math.atan2(windY, windX) * 180 / Math.PI) + 450) % 360,
routeId: routeWeather.routeId,
routeDistance: routeWeather.routeDistance,
routeInfluence: routeWeather.routeInfluence,
routeEdge: routeWeather.routeEdge,
openExposure: routeWeather.openExposure,
stormIntensity,
zone,
}; };
}; };

View File

@ -4,7 +4,9 @@ import assert from 'node:assert/strict';
import { import {
ENVIRONMENT_ISLANDS, ENVIRONMENT_ISLANDS,
environmentWindAt, environmentWindAt,
routeWeatherAt,
semiRandomWindPattern, semiRandomWindPattern,
stormFrontAt,
velocityWithEnvironment, velocityWithEnvironment,
} from '../src/environment-physics.js'; } from '../src/environment-physics.js';
@ -47,8 +49,8 @@ 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: 24000 }); const later = environmentWindAt({ x: 1200, y: 1200, timeMs: 24000 });
assert.ok(first.strength > 3 && first.strength < 30); assert.ok(first.strength > 3 && first.strength < 55);
assert.ok(later.strength > 3 && later.strength < 30); assert.ok(later.strength > 3 && later.strength < 55);
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)],
@ -80,3 +82,54 @@ test('environment velocity adds wind according to exposure', () => {
assert.equal(result.x, 106); assert.equal(result.x, 106);
assert.equal(result.y, -16); assert.equal(result.y, -16);
}); });
test('shipping lanes create calm cores with unstable edges and open sky beyond', () => {
const routes = [{
id: 'shipping-1',
from: { x: 1000, y: 1200 },
to: { x: 3600, y: 1200 },
width: 220,
}];
const lane = routeWeatherAt({ x: 2200, y: 1200, routes });
const edge = routeWeatherAt({ x: 2200, y: 1500, routes });
const open = routeWeatherAt({ x: 2200, y: 2200, routes });
assert.equal(lane.zone, 'shipping-lane');
assert.ok(lane.routeInfluence > 0.9);
assert.equal(edge.zone, 'route-edge');
assert.ok(edge.routeEdge > lane.routeEdge);
assert.equal(open.zone, 'open-sky');
assert.ok(open.routeInfluence < 0.05);
});
test('moving storm fronts can cross the world independent of route corridors', () => {
const timeMs = 110000 * 0.25;
const intensity = stormFrontAt({
x: 900,
y: 5400 * 0.53,
timeMs,
worldWidth: 5400,
worldHeight: 5400,
});
assert.ok(intensity > 0.75);
});
test('route-aware wind is calmer in a shipping lane than nearby open sky', () => {
const routes = [{
id: 'shipping-1',
from: { x: 1000, y: 1000 },
to: { x: 3600, y: 1000 },
width: 220,
}];
const lane = environmentWindAt({
x: 2200, y: 1000, timeMs: 0, routes, islands: [],
});
const open = environmentWindAt({
x: 2200, y: 2200, timeMs: 0, routes, islands: [],
});
assert.equal(lane.zone, 'shipping-lane');
assert.equal(open.zone, 'open-sky');
assert.ok(lane.strength < open.strength);
});