tower / files / test / water.test.ts
water.test.ts
| 1 | import { describe, expect, it } from 'vitest' |
| 2 | import { freeFallDriftM } from '../src/canon/motion.js' |
| 3 | import { fallPoint } from '../src/terrain/falls.js' |
| 4 | import { fetchOf, foamOf, manningSpeed, windSea } from '../src/terrain/water.js' |
| 5 | |
| 6 | /** The figures world-generation.md §4.2 and §4.4 print, so the doc and the code agree. */ |
| 7 | describe('wind waves', () => { |
| 8 | it('a 10 m/s wind across 30 km of lake raises 0.9 m waves at a 4.2 s period', () => { |
| 9 | const s = windSea(10, 30_000) |
| 10 | expect(s.heightM).toBeCloseTo(0.9, 1) |
| 11 | expect(s.periodS).toBeCloseTo(4.2, 1) |
| 12 | }) |
| 13 | |
| 14 | it('a mountain tarn 2 km across raises 0.2 m', () => { |
| 15 | expect(windSea(10, 2000).heightM).toBeCloseTo(0.23, 2) |
| 16 | }) |
| 17 | |
| 18 | it('a ring sea, with unlimited fetch, is fully developed: 2.1 m at 10 m/s, 8.6 m at 20', () => { |
| 19 | expect(windSea(10, Infinity).heightM).toBeCloseTo(2.14, 2) |
| 20 | expect(windSea(20, Infinity).heightM).toBeCloseTo(8.57, 2) |
| 21 | }) |
| 22 | |
| 23 | it('never builds a sea past fully developed, however far the wind blows', () => { |
| 24 | expect(windSea(15, 5e6).heightM).toBeLessThanOrEqual(windSea(15, Infinity).heightM + 1e-9) |
| 25 | }) |
| 26 | |
| 27 | it('gives deep-water wavelength for the peak period', () => { |
| 28 | const s = windSea(10, 30_000) |
| 29 | expect(s.lengthM).toBeCloseTo((9.80665 * s.periodS ** 2) / (2 * Math.PI), 6) |
| 30 | }) |
| 31 | |
| 32 | it('makes no waves without wind', () => { |
| 33 | expect(windSea(0, 1e5).heightM).toBe(0) |
| 34 | }) |
| 35 | }) |
| 36 | |
| 37 | describe('fetch', () => { |
| 38 | const band = { type: 'band', zFrom: 0, zTo: 42_000 } as const |
| 39 | it('is unlimited around a ring sea', () => { |
| 40 | expect(fetchOf(band, Math.PI / 2)).toBe(Infinity) |
| 41 | }) |
| 42 | it('is the width of the band for a wind straight along the axis', () => { |
| 43 | expect(fetchOf(band, 0)).toBeCloseTo(42_000, 6) |
| 44 | }) |
| 45 | }) |
| 46 | |
| 47 | describe('rivers', () => { |
| 48 | it('runs a lowland river at a walking pace and a mountain one fast', () => { |
| 49 | expect(manningSpeed(2.7, 0.002)).toBeGreaterThan(1) |
| 50 | expect(manningSpeed(2.7, 0.002)).toBeLessThan(3) |
| 51 | expect(manningSpeed(1.5, 0.08)).toBeGreaterThan(manningSpeed(1.5, 0.002)) |
| 52 | }) |
| 53 | it('turns white as it steepens', () => { |
| 54 | expect(foamOf(0.002)).toBe(0) |
| 55 | expect(foamOf(0.2)).toBe(1) |
| 56 | }) |
| 57 | }) |
| 58 | |
| 59 | describe('waterfalls', () => { |
| 60 | const fall = { river: 'r', z: 100_000, a: 5000, dz: 1, da: 0, topM: 1700, bottomM: 700, widthM: 60, speedMS: 3 } |
| 61 | |
| 62 | it('lands antispinward of the lip by the free-fall drift: 133 m for a kilometre', () => { |
| 63 | const lip = fallPoint(fall, 0, 0) |
| 64 | const foot = fallPoint(fall, 0, 1000) |
| 65 | // A river running deepward: spinward is +a, so the drift is a loss of a. |
| 66 | expect(lip.a - foot.a).toBeCloseTo(freeFallDriftM(1000), 6) |
| 67 | expect(lip.a - foot.a).toBeCloseTo(133, -1) |
| 68 | }) |
| 69 | |
| 70 | it('carries the river forward over the lip at its own speed', () => { |
| 71 | const t = Math.sqrt((2 * 1000) / 9.80665) |
| 72 | expect(fallPoint(fall, 0, 1000).z - fall.z).toBeCloseTo(3 * t, 6) |
| 73 | }) |
| 74 | |
| 75 | it('leans the same way whichever way the river runs', () => { |
| 76 | const spinwardRiver = { ...fall, dz: 0, da: 1 } |
| 77 | const foot = fallPoint(spinwardRiver, 0, 500) |
| 78 | const noSpin = spinwardRiver.a + spinwardRiver.da * spinwardRiver.speedMS * Math.sqrt((2 * 500) / 9.80665) |
| 79 | expect(noSpin - foot.a).toBeCloseTo(freeFallDriftM(500), 6) |
| 80 | }) |
| 81 | }) |