fuzzyworld

tower / files / src / terrain / water.ts

water.ts

3.8 KB · history · blame

1/**
2 * The physics of the water: how big the waves are, and how fast a river runs.
3 * See world-generation.md §4.2–§4.4.
4 *
5 * None of this is chosen for looks. Waves come from the wind and the distance it has
6 * blown over water, by the relations oceanographers fit to measured seas; a river's
7 * speed comes from how much it carries and how steeply it falls. Gravity at the floor
8 * is 1 g (design.md §1.2), so Earth's numbers apply unchanged.
9 */
10import { CIRCUMFERENCE_M, G0 } from '../canon/constants.js'
11import type { Shape } from './shapes.js'
12
13export interface WindSea {
14 /** Significant wave height: the mean of the highest third, crest to trough. */
15 heightM: number
16 /** Period at the spectrum's peak. */
17 periodS: number
18 /** Deep-water wavelength at that period. */
19 lengthM: number
20}
21
22/**
23 * The sea a wind raises. Fetch-limited by the JONSWAP relations —
24 *
25 * g·Hs / U² = 0.0016 · (g·F / U²)^½
26 * g·Tp / U = 0.286 · (g·F / U²)^⅓
27 *
28 * — and never past the fully developed sea of Pierson and Moskowitz, Hs = 0.21·U²/g,
29 * which is where a wind that has blown far enough stops building waves. A ring sea's
30 * wind can blow round the whole world and back over the same water, so its sea is
31 * always fully developed (§4.4): 2.1 m at 10 m/s, 8.6 m at 20.
32 */
33export function windSea(windMS: number, fetchM: number): WindSea {
34 const u = Math.max(0, windMS)
35 if (u < 0.1) return { heightM: 0, periodS: 0, lengthM: 0 }
36 const x = (G0 * fetchM) / (u * u)
37 const fetchHeight = (0.0016 * Math.sqrt(x) * u * u) / G0
38 const fetchPeriod = (0.286 * Math.cbrt(x) * u) / G0
39 const fullHeight = (0.21 * u * u) / G0
40 // Pierson–Moskowitz peak frequency, ωp = 0.877 g / U.
41 const fullPeriod = (2 * Math.PI * u) / (0.877 * G0)
42 const heightM = Math.min(fetchHeight, fullHeight)
43 const periodS = Math.min(fetchPeriod, fullPeriod)
44 return { heightM, periodS, lengthM: (G0 * periodS * periodS) / (2 * Math.PI) }
45}
46
47/**
48 * How far the wind blows over a body of water, in the direction it blows: `dirRad` is
49 * measured from deepward toward spinward, the interior view's heading convention.
50 *
51 * Around a band there is no end — the water closes on itself — so any wind with a
52 * circumferential component has, for waves, unlimited fetch; along the axis it has the
53 * band's width. A polygon's fetch is its extent in the wind's direction.
54 */
55export function fetchOf(shape: Shape, dirRad: number): number {
56 const along = Math.abs(Math.cos(dirRad))
57 const around = Math.abs(Math.sin(dirRad))
58 if (shape.type === 'band') {
59 if (around > 0.2) return Infinity
60 return (shape.zTo - shape.zFrom) / Math.max(along, 1e-3)
61 }
62 const b = shape.box
63 return Math.max(10, Math.hypot((b.z1 - b.z0) * along, (b.a1 - b.a0) * around))
64}
65
66/** Longest a wind can blow around Tower before meeting its own waves: the circumference. */
67export const RING_FETCH_M = CIRCUMFERENCE_M
68
69/**
70 * Mean speed of a river, by Manning's equation: v = R^⅔ · S^½ / n. The hydraulic
71 * radius of a wide channel is about its depth, and n = 0.035 is a natural channel
72 * with some stones and weeds. Clamped to what rivers actually do: a pool still moves
73 * a little, and nothing flows faster than about 8 m/s outside a spillway.
74 */
75export function manningSpeed(depthM: number, grade: number): number {
76 const v = (Math.max(0.05, depthM) ** (2 / 3) * Math.sqrt(Math.max(0, grade))) / MANNING_N
77 return Math.min(8, Math.max(0.15, v))
78}
79export const MANNING_N = 0.035
80
81/**
82 * How white a reach is. Rivers break into rapids from a few percent of grade, and are
83 * all foam well before they fall.
84 */
85export function foamOf(grade: number): number {
86 const t = Math.min(1, Math.max(0, (grade - 0.025) / (0.12 - 0.025)))
87 return t * t * (3 - 2 * t)
88}
89
90/** A drop of at least this many metres in one step of a river's course is a waterfall. */
91export const FALL_MIN_M = 12