tower / files / src / terrain / water.ts
water.ts
| 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 | */ |
| 10 | import { CIRCUMFERENCE_M, G0 } from '../canon/constants.js' |
| 11 | import type { Shape } from './shapes.js' |
| 12 | |
| 13 | export 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 | */ |
| 33 | export 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 | */ |
| 55 | export 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. */ |
| 67 | export 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 | */ |
| 75 | export 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 | } |
| 79 | export 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 | */ |
| 85 | export 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. */ |
| 91 | export const FALL_MIN_M = 12 |