import { R as Room } from './room-BikME3W6.cjs'; export { D as DayCycle, a as DayCycleOptions, F as FogPreset, L as LightingPreset, b as LightingRig, P as PathOptions, S as Sky, g as SkyOptions, T as Terrain, h as TerrainOptions, W as Water, i as WaterOptions, j as WorldPath, k as aboveWater, l as applyFog, m as createDayCycle, n as createLightingRig, o as createPath, q as createSky, r as createTerrain, s as createWater } from './room-BikME3W6.cjs'; import { DirectionalLight, Vector3, DataTexture, Scene, Group, Object3D, Points, LineSegments, ShaderMaterial, Mesh, Light, Material, InstancedMesh, HemisphereLight } from 'three'; import { b as Palette } from './types-C_yucwmh.cjs'; import { i as WindField, g as TreeSeason } from './track-W5vkXpFN.cjs'; export { R as RailTrack, S as SwayOptions, b as TrackOptions, c as TrackPoint, W as Wind, j as WindFieldOptions, k as WindOptions, l as applyWind, m as createTrack, o as createWindField } from './track-W5vkXpFN.cjs'; /** * A scene environment — the thing that makes metal look like metal. * * A metal has no diffuse colour. Everything you see on it is a reflection, * so a metal in a scene with nothing to reflect renders BLACK. That is not * a bug in the material: it is the material being right about a world with * no sky in it. `createSky` draws a beautiful gradient dome, but a dome is * geometry — three cannot reflect it, because reflection needs an * environment map and a mesh is not one. * * So this builds one, from the same gradient the sky is drawing: a tiny * equirectangular {@link DataTexture} — sky above, ground bounce below, and * the sun burned in where the lighting rig actually put it. Nothing is * fetched, nothing is loaded, and three PMREM-filters it on first use so * rough surfaces get a blurred version and polished ones a sharp one. * * ```ts * const rig = createLightingRig('day'); * scene.add(createSky({ palette }).mesh, rig.group); * applyEnvironment(scene, { palette, sun: rig.sun }); // now chrome is chrome * ``` * * It affects every PBR material in the scene, not just the metals: it is * ambient light arriving from a direction, which is what the flat * `AmbientLight` in the rig is a stand-in for. */ interface EnvironmentOptions { /** Palette to take the sky colours from. */ palette?: Palette; /** Zenith colour (hex int). Defaults to the palette's `skyTop`. */ top?: number; /** Horizon colour (hex int). Defaults to the palette's `skyBottom`. */ horizon?: number; /** * What the light bounces off below the horizon (hex int). Defaults to the * palette's `grassLow` — the ground half of an environment matters more * than it sounds, because it is what stops the underside of everything * metal from being a black hole. */ ground?: number; /** * The sun to burn into the map, as a light or a direction. A sky with no * sun in it gives polished metal a soft even sheen and no highlight at * all, which reads as plastic. */ sun?: DirectionalLight | Vector3 | null; /** How bright the sun disc is, relative to the sky. Default 6. */ sunIntensity?: number; /** How tight it is: bigger is smaller. Default 380. */ sunFocus?: number; /** Overall strength, written to `scene.environmentIntensity`. Default 1. */ intensity?: number; /** * Equirect width in texels (height is half). Default 128 — this is an * environment, not a photograph, and three blurs it into a PMREM chain * anyway. */ size?: number; } interface SceneEnvironment { /** The generated map, already assigned to `scene.environment`. */ texture: DataTexture; /** Rebuild after changing the palette or moving the sun. */ refresh(options?: EnvironmentOptions): void; /** Take it off the scene and free it. */ dispose(): void; } /** * Paint the equirect: sky above, ground below, sun where the rig put it. * * Use this directly — rather than {@link applyEnvironment} — when only SOME * materials should reflect, because a per-material `envMap` is the only way * to do that. `scene.environment` cannot be opted out of: three overwrites * `material.envMapIntensity` with `scene.environmentIntensity` for every * material that has no `envMap` of its own, so setting it to 0 on one * material does nothing at all. * * ```ts * const map = createEnvironmentMap({ palette, sun: rig.sun }); * chrome.envMap = map; // this one reflects * plaster.envMap = null; // this one does not * ``` */ declare function createEnvironmentMap(options?: EnvironmentOptions): DataTexture; /** * Give the scene something to reflect. Returns a handle so it can be * refreshed when the sun moves or the palette changes, and disposed. */ declare function applyEnvironment(scene: Scene, options?: EnvironmentOptions): SceneEnvironment; /** * The light budget — spending scarcity where the camera is looking. * * Forward-rendered WebGL affords a handful of real dynamic lights before * mobile GPUs weep, but a street wants twenty lamps. The resolution: every * luminous prop keeps its cheap glow (emissive bulb + additive halo) always, * and *claims* a real light it may or may not be granted. The budget owns a * small pool of PointLights and, each update, grants them to the * best-scoring claims — score is priority over distance to the viewpoint — * so light pools appear under the lamps near the camera and quietly leave * the ones behind it. * * Granting is **hysteretic**: an incumbent keeps its light until a * challenger clearly outscores it, so panning the camera doesn't strobe * lights between owners. A kept claim also keeps the *same* PointLight * instance — no rebind flicker. * * ```ts * const budget = createLightBudget({ max: 6 }); * scene.add(budget.group); * for (const lamp of lamps) budget.register(lamp.claim); * // per frame: * budget.update(camera.position); * ``` */ interface LightClaim { /** Where the light lives. An Object3D is tracked live (movers welcome). */ anchor: Object3D | { x: number; y: number; z: number; }; color: number; /** Intensity the granted PointLight burns at. */ intensity: number; /** Falloff distance of the granted light, metres. */ radius: number; /** Bigger outranks distance. Default 1. */ priority?: number; /** * Litness, read live — a doused fixture's claim goes ineligible without * any unregistering. Luminous props close this over their own state. */ isLit?: () => boolean; } interface LightGrant { readonly claim: LightClaim; /** Holding a real light right now? */ readonly granted: boolean; /** Withdraw the claim entirely. */ release(): void; } interface LightBudgetOptions { /** Real lights in the pool. Default 6. */ max?: number; /** * How decisively a challenger must beat an incumbent to take its light * (score ratio). 1 = no stickiness. Default 1.35. */ hysteresis?: number; } interface LightBudget { /** Add this to the scene — the pool lives here. */ group: Group; readonly max: number; /** Claims currently holding a real light. */ readonly active: number; register(claim: LightClaim): LightGrant; /** Re-grant the pool for this viewpoint (camera or hero position). */ update(viewpoint: Object3D | { x: number; y: number; z: number; }): void; } declare function createLightBudget(options?: LightBudgetOptions): LightBudget; /** * Lightning — the sky's percussion. * * A strike is three things at once: the **flash** (a two-pulse spike * driven through whatever targets you hand it — ambient intensity, sky * background, fog color — then decayed back to exactly where they * were), the **bolt** (a seeded forked polyline of additive tubes, * gone in a tenth of a second), and the **thunder** — an `onThunder` * callback delayed by the strike's distance, which is the handshake a * Soundboard answers. Distance IS the delay: close strikes crack * immediately, far ones rumble in late. * * ```ts * const storm = createLightning({ * targets: { ambient: rig.ambient, background: scene.background, fog: scene.fog }, * onThunder: (s) => sounds.crack(Math.min(1.4 - s.distance / 60, 1)), * }); * scene.add(storm.group); * storm.storminess = 0.8; // auto-strikes, seeded * // per frame: storm.update(dt); * ``` */ interface Strike { /** Game-metres from the origin. */ distance: number; /** Bearing of the strike, radians. */ azimuth: number; /** 0..1 — how hard the flash hits. */ energy: number; } interface LightningTargets { ambient?: { intensity: number; }; /** A Color — the scene's background. */ background?: { getHex(): number; setHex(hex: number): unknown; } | null; /** The scene's fog (its color is flashed). */ fog?: { color: { getHex(): number; setHex(hex: number): unknown; }; } | null; } interface LightningOptions { targets?: LightningTargets; seed?: number; /** Mean seconds between auto-strikes at storminess 1. Default 5. */ cadence?: number; /** Game-metres of thunder delay per real second. Default 100. */ soundSpeed?: number; onStrike?: (strike: Strike) => void; onThunder?: (strike: Strike) => void; } interface Lightning { /** Bolts appear here — add it to the scene. */ group: Group; /** 0 = clear skies, 1 = the full show. Drives auto-strikes. */ storminess: number; /** The current flash level, 0..1 — flash your own things by it too. */ readonly flash: number; /** Strikes so far (auto + manual). */ readonly strikes: number; /** Force a strike now; omitted fields are seeded. */ strike(options?: Partial): Strike; update(dt: number): void; } declare function createLightning(options?: LightningOptions): Lightning; /** * Fireworks — celebration tech. * * Seeded rockets rise on a slightly drunken line, burst at the top of * their fuse into a spherical shell of glowing sparks that droop under * gravity and gutter out. One InstancedMesh for everything in flight * (the effects-system idiom: octahedra, scale-is-fade, per-instance * color), so a grand finale is still one draw call. * * `onBurst` fires at every shell break with the position and color — * that's the hook for the boom, the GameFeel thump, and the crowd. * * ```ts * const show = createFireworks({ seed: 7, onBurst: (at) => sounds.impact('soft', 1) }); * scene.add(show.group); * show.launch({ x: -4, y: 0, z: 0 }, { color: 0xff9d5c }); * // per frame: show.update(dt); * ``` */ interface FireworksOptions { seed?: number; /** Spark capacity — the finale budget. Default 600. */ capacity?: number; /** Downward pull on sparks. Default 3.4 (stylized, not Earth). */ gravity?: number; onBurst?: (at: { x: number; y: number; z: number; }, color: number) => void; } interface LaunchOptions { color?: number; /** Sparks in the shell. Default seeded 70–110. */ sparks?: number; } interface Fireworks { group: Group; /** Rockets currently climbing. */ readonly rockets: number; /** Sparks currently burning. */ readonly sparks: number; launch(from?: { x: number; y: number; z: number; }, options?: LaunchOptions): void; update(dt: number): void; } declare function createFireworks(options?: FireworksOptions): Fireworks; /** * Light shafts — god rays for the out-of-doors. * * `createInteriorLight` casts them from windows; this is the same light * let loose: seeded sunbeams slanting through a forest canopy, a ruin's * broken roof, a cavern mouth. Each shaft is a pair of crossed additive * cards that fade along their run (all shafts share ONE merged geometry * and one material — the whole grove is a single draw call), with dust * motes drifting slowly down the beams, twinkling as they go. * * Bind a day cycle (structurally — anything with `sunElevation`) and * the shafts live with the sun: full at midday, gone by dusk. No cycle * means a fixed sun and shafts that never die. * * ```ts * const shafts = createLightShafts({ count: 7, area: 8, seed: 4, cycle }); * scene.add(shafts.group); * // per frame: shafts.update(dt); * ``` */ interface LightShaftsOptions { /** How many beams. Default 7. */ count?: number; /** Radius of the lit patch on the ground, metres. Default 8. */ area?: number; /** Beam run from canopy to ground, metres. Default 13. */ length?: number; /** Beam color. Default a warm 0xfff2c8. */ color?: number; /** Peak card opacity. Default 0.13 — shafts suggest, never shout. */ strength?: number; /** Dust motes per shaft. Default 18; 0 disables. */ dust?: number; /** Tilt from vertical, radians. Default 0.32. */ tilt?: number; /** Which way the beams lean. Default 0.7. */ azimuth?: number; /** A DayCycle (structurally): strength follows the sun. */ cycle?: { readonly sunElevation: number; }; seed?: number; } interface LightShafts { group: Group; /** Current effective strength (after the cycle has its say). */ readonly strength: number; /** Override the base strength (still scaled by the cycle). */ setStrength(value: number): void; /** Drift the motes; follow the bound cycle. */ update(dt: number): void; } declare function createLightShafts(options?: LightShaftsOptions): LightShafts; type PrecipitationType = 'rain' | 'snow' | 'petal'; interface PrecipitationOptions { /** rain (slanted streaks), snow (drifting flakes) or petal (fluttering, spinning blossom/leaf fall). Default 'rain'. */ type?: PrecipitationType; /** Particle count. Default 6000 (rain) / 3500 (snow) / 1400 (petal). */ count?: number; /** Box size around the camera the weather fills, in metres. Default [55, 34, 55]. */ area?: number | [number, number, number]; /** How heavy, 0–1. Default 1. */ intensity?: number; /** A WindField — rain slants and snow/petals drift along it. */ wind?: WindField; /** How strongly the wind pushes the fall (metres/s per unit strength). Default 9 (rain) / 4 (snow) / 5 (petal). */ windInfluence?: number; /** Fall speed, metres/s. Default 14 (rain) / 2.2 (snow) / 1.4 (petal). */ speed?: number; /** Streak length (rain) or particle size in px (snow/petal). Default 0.5 / 9 / 11. */ size?: number; /** Particle colour. Default light blue-grey (rain) / white (snow) / blossom pink (petal). */ color?: number; /** Particle opacity. Default 0.5 (rain) / 0.85 (snow) / 0.9 (petal). */ opacity?: number; seed?: number; } interface Precipitation { /** The renderable — add it to the scene. Follows the camera; never culled. */ object: Points | LineSegments; /** The particle material. */ material: ShaderMaterial; /** Set how heavy it falls, 0–1 (0 stops it). */ setIntensity(value: number): void; /** Snow only: settle a white cap onto the surfaces under `target` as it falls. */ accumulate(target: Object3D, options?: SoakOptions & AccumulateOptions): Precipitation; /** Rain only: wet the surfaces under `target` as it falls, and dry them after. */ soak(target: Object3D, options?: SoakOptions): Precipitation; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } interface SoakOptions { /** How wet it gets at full intensity, 0–1. Default 0.9. */ max?: number; /** Wetting speed (wetness per second). Default 0.22. */ rate?: number; /** * Drying speed once the rain eases (wetness per second). Default 0.045 — * a fifth of the wetting rate, because a wall soaks in a minute and takes * an hour to dry, and a puddle that vanishes the moment the rain stops * reads as a bug rather than as weather. */ dry?: number; /** How well water clings to vertical faces, 0–1. Default 0.55. */ cling?: number; } interface AccumulateOptions { /** Snow colour. Default 0xf4f8fc. */ color?: number; /** How deep it settles, 0–1 (the cap strength). Default 0.85. */ max?: number; /** Settle speed (cap per second). Default 0.08. */ rate?: number; /** How up-facing a face must be to collect snow. Default 0.25. */ capUp?: number; } /** * GPU-driven rain or snow that follows the camera — a finite cloud of particles * wrapped into an infinite fall. Every particle's position is computed in the * vertex shader from a fixed seed plus the clock, so there is **no per-particle * CPU work**: thousands of drops cost one draw call and one uniform update. Rain * falls as slanted streaks, snow as soft drifting flakes, both leaning along a * {@link WindField} if you pass one. It self-animates from the render loop. * * Snow can `accumulate` — settling a white cap onto the surfaces below, reusing * the surface system's up-facing cap, so roofs and ground whiten as it falls. * * ```ts * const snow = createPrecipitation({ type: 'snow', wind }); * scene.add(snow.object); * snow.accumulate(scene); // roofs & ground gather snow * ``` */ declare function createPrecipitation(options?: PrecipitationOptions): Precipitation; /** * The surf zone — what turns a coloured plane into a coast. * * Two effects, on one clock so they agree: **breakers** (bands of * whitewater that form where the swell trips on the bottom and run * shoreward) and the **swash** (the waterline itself running up the beach * and draining back, leaving a mirror-thin sheet behind it). */ interface SurfOptions { /** Water depth at which the swell trips and whitens, metres. Default 1.7. */ breakDepth?: number; /** * How far up the beach the water runs, in metres of DEPTH — the edge's * travel is this divided by the beach slope, so 0.42 m on a 1-in-7 face * is nearly three metres of moving waterline. Default 0.42. */ runUp?: number; /** Seconds per swash cycle, in and back out again. Default 8. */ period?: number; /** Breaker lines per metre of depth — more = tighter surf. Default 2.4. */ bands?: number; } /** Fine surface chop: the detail that makes a sea read as liquid. */ interface RippleOptions { /** Normal perturbation, 0–1. Default 0.34; past ~0.6 it reads as fur. */ strength?: number; /** Ripples per metre. Bigger = finer chop. Default 0.85. */ scale?: number; } interface OceanOptions { /** World-space sea level (the plane's Y). Default 0. */ level?: number; /** Plane extent in metres. Default 240. */ size?: number; /** Grid subdivisions per side (more = smoother crests). Default 180. */ segments?: number; /** Overall wave height in metres. Default 0.5. */ amplitude?: number; /** Crest sharpness, 0–1 (0 = rolling swell, 1 = peaked chop). Default 0.75. */ choppiness?: number; /** Wavelength of the primary swell, metres. Default 26. */ wavelength?: number; /** Wave heading in degrees when no wind is given. Default 30. */ direction?: number; /** Phase-speed multiplier. Default 1. */ speed?: number; /** A WindField — the swell turns downwind and grows with the wind. */ wind?: WindField; /** Storm surge, 0–1, or a live source (`() => weather.storminess`): whips up * bigger, choppier, foamier, darker seas and raises the sea level. Default 0. */ storm?: number | (() => number); /** Sea-level rise at full storm, metres (the surge). Default 1.2. */ surge?: number; /** Terrain height sampler (`terrain.heightAt`): the ocean fades out over land and foams at the shore. */ shore?: (x: number, z: number) => number; /** * The surf zone: breakers that run shoreward, and a waterline that runs * UP the beach and drains back. Needs a `shore` — without one there is * no beach to break on and every term is inert. `false` turns it off. */ surf?: false | SurfOptions; /** * How deep the water goes before it reads as open sea, metres. This is * the width of the TURQUOISE SHELF — on a 1-in-7 beach, a shoal depth of * 12 puts eighty metres of bright water between the sand and the blue, * which is most of what a tropical coast actually is. Default 3. */ shoalDepth?: number; /** * Fine chop riding on the swell. Four Gerstner waves give a sea its * shape, but a swell alone is a rolling sheet — what reads as WATER is * the ripple breaking the light into moving highlights. `false` for * glass (a lagoon, a harbour at dawn). */ ripples?: false | RippleOptions; /** * How see-through the shallows are, 0–1. Clear water IS its bottom: at * 0.8 you read the sand, the reef and the fish through the turquoise, * and the colour deepens to opaque as the floor drops away. Default 0 * — an opaque sheet, which is right for a grey sea and cheaper. */ clarity?: number; /** * A live sea state — `() => seaState.trains`. * * Structurally `SeaState.trains`, duck-typed like `storm`, so the ocean * knows nothing about fetch or wind history. Given one, the four wave * components SPLIT: two of them run with the wind sea and two with the * swell, on their own headings and their own wavelengths. That is a cross * sea, and it is what makes the surface stop looking like one wave train * with some noise on it. * * It overrides `amplitude`, `wavelength`, `direction` and any `wind`. */ sea?: () => { windSea: { height: number; period: number; length: number; from: number; }; swell: { height: number; period: number; length: number; from: number; }; }; /** Deep-water colour. Default 0x184a63. */ deepColor?: number; /** Shallow / shoreward colour. Default 0x3f8fa6. */ shallowColor?: number; /** Sky colour reflected at grazing angles (fresnel). Default 0xbcd4e6. */ skyColor?: number; } interface Ocean { /** The ocean surface — add it to the scene. */ mesh: Mesh; /** Sea level (the plane's Y). */ level: number; /** The wave height at a world point (and time) — sit a boat on this to bob it. */ heightAt(x: number, z: number, time?: number): number; /** * The swash's run-up right now, in metres of extra depth — positive while * the water is running up the beach, negative while it drains. Add it to a * depth reading and gameplay agrees with what the shader is drawing: a * wader gets caught by the wave that visibly arrives. */ readonly runUp: number; /** * How deep the water is over ground of height `groundY`, right now, * including the swash. 0 where the sea has drained away — so a player * walking the edge is in and out of the water as the waves come. */ depthOver(groundY: number): number; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * A Gerstner-wave ocean — the sea that makes a coast feel like a coast. The * surface is a subdivided plane displaced by a sum of Gerstner waves in the * vertex shader, so crests peak and troughs flatten like real swell, with * analytic normals for the light and **whitecap foam** where the waves fold. * It patches a `MeshStandardMaterial`, so PBR lighting, shadows and fog all * survive, and a fresnel term tints the surface with the sky at grazing angles. * * Pass a {@link WindField} and the swell turns downwind and grows with the wind. * Pass a terrain `heightAt` as `shore` and the ocean **fades out over land and * foams along the waterline** — the same handshake the rest of SCENA uses. * * `heightAt(x, z)` gives the wave height on the CPU, so a boat or buoy can ride * the swell — the buoyancy handshake, mirroring `terrain.heightAt`. * * ```ts * const ocean = createOcean({ level: 0, wind, shore: terrain.heightAt }); * scene.add(ocean.mesh); * boat.position.y = ocean.heightAt(boat.position.x, boat.position.z); * ``` */ declare function createOcean(options?: OceanOptions): Ocean; type WeatherPreset = 'clear' | 'overcast' | 'fog' | 'rain' | 'storm' | 'snow' | 'blizzard'; /** The full set of knobs a weather state drives. All are cross-faded on `set`. */ interface WeatherStateParams { /** Wind strength (WindField.strength). */ wind: number; /** Gustiness, 0–1. */ gust: number; /** Rain intensity, 0–1. */ rain: number; /** Snow intensity, 0–1. */ snow: number; /** Fog colour (hex). */ fogColor: number; /** Fog near distance, metres. */ fogNear: number; /** Fog far distance, metres. */ fogFar: number; /** Sky / background colour (hex). */ sky: number; /** Light level multiplier, 0–1 (dims the sun & ambient in storms). */ light: number; /** Sea roughness, 0–1 — wire an ocean's `storm` to `() => weather.storminess` * and it whips up a surge (bigger, choppier, foamier, higher seas). Default 0. */ sea?: number; /** Whether lightning flashes fire in this state. */ lightning?: boolean; } interface WeatherOptions { /** Starting state. Default 'clear'. */ initial?: WeatherPreset | string; /** Reuse an existing WindField (so flora already bound to it responds). Otherwise one is made. */ wind?: WindField; /** The sun / key light to dim in storms (its current intensity is taken as "full sun"). */ sun?: Light; /** The ambient / fill light to dim in storms. */ ambient?: Light; /** Manage `scene.fog` (creating a Fog if absent). Default true. */ fog?: boolean; /** Manage `scene.background` colour. Default true. */ background?: boolean; /** Settle snow onto this object in snowy states (via Precipitation.accumulate). */ accumulateOn?: Object3D; /** Rain particle count. Default 6000. */ rainCount?: number; /** Snow particle count. Default 3500. */ snowCount?: number; /** Override or add states (deep-merged over the built-ins). */ states?: Record>; seed?: number; } interface Weather { /** The shared wind field — bind flora to it so trees lean into the storm. */ wind: WindField; /** The rain system (its intensity is driven by the current state). */ rain: Precipitation; /** The snow system. */ snow: Precipitation; /** The renderables added to the scene (rain + snow). */ objects: Object3D[]; /** The current target state name. */ readonly state: string; /** The live, cross-faded sea roughness, 0–1 — wire an ocean's `storm` to this. */ readonly storminess: number; /** Cross-fade to a state over `fade` seconds (default 4). */ set(name: WeatherPreset | string, options?: { fade?: number; }): Weather; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * A weather controller that cross-fades a whole scene between named states — * `clear`, `overcast`, `fog`, `rain`, `storm`, `snow`, `blizzard` — by driving * the pieces SCENA already has: a {@link WindField}, rain and snow * {@link Precipitation}, the scene's fog and background colour, and (optionally) * the sun and ambient light. Call `set('storm')` and the wind rises, rain * fills in, the fog closes and darkens and lightning cracks — all eased over a * few seconds. It self-animates from the render loop. * * Bind your flora to `weather.wind` so the trees lean into the storm; the one * field drives the bend, the rain's slant and (if you have an ocean) the swell. * * ```ts * const weather = createWeather(scene, { sun: rig.sun, accumulateOn: ground }); * wind = weather.wind; * applyWind(forest.group, { field: weather.wind, height: 4, anchor: 1 }); * weather.set('storm', { fade: 6 }); // roll a storm in over six seconds * ``` */ declare function createWeather(scene: Scene, options?: WeatherOptions): Weather; /** A season is the four names a foliage `Seasons` controller cross-fades between. */ type Season = TreeSeason; /** * How a season re-grades foliage albedo. A pure colour operation — tint the * leaves toward a seasonal hue, push or drop their saturation, and lighten or * darken them — so no geometry is rebuilt and a whole wood turns over a few * seconds by lerping these numbers. */ interface SeasonGrade { /** Hue the foliage is blended toward (hex). */ tint: number; /** How far toward `tint`, 0–1. */ tintAmount: number; /** Saturation multiplier (1 = unchanged, >1 richer, <1 toward grey). */ saturation: number; /** Brightness multiplier (1 = unchanged). */ brightness: number; } interface SeasonsOptions { /** Starting season. Default 'summer' (the as-authored look — no grade). */ initial?: Season; /** Override or extend the built-in grades (merged over the defaults). */ grades?: Partial>>; } interface Seasons { /** The shared shader uniforms (one set, referenced by every bound material). */ readonly uniforms: Record; /** The current target season. */ readonly season: Season; /** Every foliage material re-graded so far. */ materials: Material[]; /** Patch one material to take the seasonal grade. Composes with wind; idempotent. */ bind(material: Material): Seasons; /** Self-animate: drive the cross-fade clock from a rendered object's `onBeforeRender`. */ attach(object: Object3D): Seasons; /** Bind every tagged foliage material under `target`, then attach the driver. */ apply(target: Object3D): Seasons; /** Cross-fade to a season over `fade` seconds (default 6). */ set(season: Season, options?: { fade?: number; }): Seasons; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * A season controller for foliage — the counterpart to {@link createWeather}, * but for the trees themselves. It cross-fades a whole wood between `spring`, * `summer`, `autumn` and `winter` by re-grading each canopy's albedo (tint, * saturation, brightness) in the shader — no geometry rebuilt, so thousands of * scattered trees turn together for the cost of a few uniform writes. Only * foliage is touched; trunks stay planted and unchanged. * * It patches the same foliage materials `createTree` tags, composes cleanly with * a {@link WindField} (a tree can sway *and* turn), and self-animates from the * render loop — so `set('autumn')` and the leaves warm over a few seconds. * * ```ts * const seasons = createSeasons({ initial: 'summer' }); * seasons.apply(forest.group); // re-grade every tagged canopy * seasons.apply(oak.object); // and standalone trees * seasons.set('autumn', { fade: 8 }); // turn the wood over eight seconds * ``` */ declare function createSeasons(options?: SeasonsOptions): Seasons; interface GodRaysOptions { /** How many shafts. Default 18. */ count?: number; /** Shaft length downward, metres. Default 20. */ height?: number; /** Shaft width, metres. Default 1.4. */ width?: number; /** Radius of the disc the shafts scatter across. Default 14. */ spread?: number; /** Sun tilt in degrees — how far the shafts lean from vertical. Default 18. */ tilt?: number; /** Sun azimuth in degrees — which way they lean. Default 0. */ azimuth?: number; /** Shaft colour. Default 0xbfe6f0 (pale cyan). */ color?: number; /** Additive brightness, 0–1. Default 0.14. */ opacity?: number; /** How far the shafts waver, metres. Default 0.5. */ sway?: number; seed?: number; } interface GodRays { /** The additive shaft mesh — add it to the scene, position it at the surface. Self-animates. */ object: Mesh; material: ShaderMaterial; /** Set the additive brightness, 0–1. */ setOpacity(value: number): void; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * Volumetric light shafts falling through water — the "god rays" that make a * submerged scene read as *underwater*. Each shaft is a pair of crossed additive * quads (so it holds up from any camera angle), brightest at the surface and * feathered at the edges, wavering gently in the current. The whole set is one * additive draw call and it self-animates from the render loop. * * Position the object at the water surface; the shafts hang below it, leaning * with the sun (`tilt` / `azimuth`). * * ```ts * const rays = createGodRays({ count: 20, height: 22, tilt: 20 }); * rays.object.position.set(0, waterLevel, 0); * scene.add(rays.object); * ``` */ declare function createGodRays(options?: GodRaysOptions): GodRays; interface CausticsOptions { /** Caustic tint (added to emissive, so it glows regardless of the day cycle). Default 0x9fd8e6. */ color?: number; /** Pattern scale — larger = finer cells. Default 0.5. */ scale?: number; /** How fast the network shifts. Default 0.6. */ speed?: number; /** Brightness of the caustics. Default 0.5. */ intensity?: number; } interface Caustics { /** The shared shader uniforms (one set, referenced by every bound material). */ readonly uniforms: Record; /** Every material patched so far. */ materials: Material[]; /** Set the caustic brightness. */ setIntensity(value: number): Caustics; /** Project caustics onto a material's emissive. Composes with surfaces; idempotent per material. */ bind(material: Material): Caustics; /** Self-animate: drive the shared clock from a rendered object's `onBeforeRender`. */ attach(object: Object3D): Caustics; /** Convenience: `bind` every material under `target`, then `attach` the driver. */ apply(target: Object3D): Caustics; /** Advance the clock manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * Caustics — the rippling net of light that the water surface throws onto the * seabed. It patches a `MeshStandardMaterial` (so PBR, shadows and fog all * survive) and adds a shifting caustic network to the material's *emissive*, so * the light dances regardless of the day/night cycle. Bind it to the sand and * rocks under your ocean; it composes with SCENA surfaces and self-animates. * * ```ts * const caustics = createCaustics({ intensity: 0.5 }); * caustics.apply(seabed); // sand + rocks catch the moving light * ``` */ declare function createCaustics(options?: CausticsOptions): Caustics; interface BubbleOptions { /** Total bubbles across all columns. Default 240. */ count?: number; /** Number of vent points bubbles rise from (ignored if `sources` is given). Default 6. */ columns?: number; /** Explicit vent positions in local XZ; otherwise `columns` are scattered in `area`. */ sources?: Array<[number, number]>; /** Radius (or [x, z]) the vents scatter across when `sources` is omitted. Default 16. */ area?: number | [number, number]; /** Seabed Y the bubbles rise from. Default 0. */ floor?: number; /** How far they rise before popping, metres. Default 8. */ rise?: number; /** Rise speed, metres/s. Default 1.2. */ speed?: number; /** Bubble size in px. Default 8. */ size?: number; /** Lateral wander, metres. Default 0.35. */ wobble?: number; /** Bubble colour. Default 0xcfeaf0. */ color?: number; /** Opacity. Default 0.4. */ opacity?: number; seed?: number; } interface Bubbles { /** The bubble points — add it to the scene, position it at the seabed. Self-animates. */ object: Points; material: ShaderMaterial; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * Streams of bubbles rising from the seabed — from a vent, a wreck, a diver. * Bubbles wander a little and swell as they rise (pressure drops), then pop near * the top; the columns are anchored to fixed world vents, not the camera. Every * bubble's position is computed in the vertex shader from a seed and the clock, * so it's one draw call and no per-particle CPU work. It self-animates. * * ```ts * const bubbles = createBubbles({ columns: 6, area: 14, rise: 9 }); * bubbles.object.position.y = seabedY; * scene.add(bubbles.object); * ``` */ declare function createBubbles(options?: BubbleOptions): Bubbles; interface WaterGradeOptions { /** World Y of the water surface — depth is measured down from here. Default 0. */ surface?: number; /** The colour everything grades toward with distance & depth. Default 0x0e3a49. */ color?: number; /** Extinction per metre of view distance. Default 0.022. */ density?: number; /** Extra extinction per metre of depth below the surface. Default 0.03. */ depthDensity?: number; /** How much faster warm light (red, then green) is absorbed than blue, 0–1. Default 0.6. */ redShift?: number; } interface WaterGrade { /** The shared shader uniforms. */ readonly uniforms: Record; /** Every material patched so far. */ materials: Material[]; /** Set the base extinction density. */ setDensity(value: number): WaterGrade; /** Grade a material by depth & distance. Composes with surfaces; idempotent per material. */ bind(material: Material): WaterGrade; /** Grade every material under `target`. */ apply(target: Object3D): WaterGrade; } /** * The underwater colour grade — the tint that makes a scene read as *deep*. * Real water absorbs light by wavelength (red first, then green, leaving blue), * so distant and deep things go blue-green and dark. This patches a * `MeshStandardMaterial` and applies **per-channel Beer–Lambert extinction** * toward the water colour, driven by view distance and depth below the surface * — physically flavoured, not a flat fog. Bind it to the seabed, rocks, kelp * and fish and the whole world sinks into the blue. Composes with SCENA * surfaces and [caustics](#caustics). * * ```ts * const grade = createWaterGrade({ surface: 0, density: 0.03 }); * grade.apply(reef); // everything fades into the deep with distance & depth * ``` */ declare function createWaterGrade(options?: WaterGradeOptions): WaterGrade; type FlockType = 'birds' | 'fish'; interface FlockOptions { /** birds (fly, wings flap) or fish (swim, tail sways). Default 'birds'. */ type?: FlockType; /** How many. Default 60 (birds) / 80 (fish). */ count?: number; /** Centre of the volume the flock roams, world space. Default [0, 12, 0] birds / [0, 2, 0] fish. */ center?: [number, number, number]; /** Half-extents of that volume (a box), or one number for a cube. Default [26, 6, 26]. */ bounds?: [number, number, number] | number; /** Cruise speed, m/s. Default 7 (birds) / 3 (fish). */ speed?: number; /** Creature length, metres. Default 0.5 (birds) / 0.4 (fish). */ size?: number; /** Body colour. Default 0x2b2b30 birds / 0x6a86a0 fish. */ color?: number; /** Wing-beat / tail-beat rate. Default 9 (birds) / 5 (fish). */ beat?: number; /** Steering weights. */ separation?: number; alignment?: number; cohesion?: number; /** Wheel around the centre at this radius (birds circling a tower). Off by default. */ circle?: number; seed?: number; } interface Flock { /** The instanced flock — add it to the scene. Self-animates. */ object: InstancedMesh; count: number; /** Live boid positions (world space) — for gameplay: scare them, follow one, etc. */ positions: readonly Vector3[]; /** Move the roaming volume's centre (the flock drifts to follow). */ setCenter(x: number, y: number, z: number): void; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * A flock of birds or a school of fish — the thing that makes a sky or a sea * feel *alive*. A lightweight boid simulation (separation, alignment, cohesion, * plus soft bounds and a little wander) steers every creature on the CPU, and * the whole flock draws as **one InstancedMesh** whose wings beat (or tail * sways) in the vertex shader from a per-instance phase — so no two flap in * lockstep. It self-animates from the render loop; pass `circle` and the birds * wheel around the centre like crows over a tower. * * `positions` exposes the live boids, so gameplay can read them — scatter the * flock when an NPC gets close, or have a cat watch one fish. * * ```ts * const crows = createFlock({ type: 'birds', center: [0, 16, 0], circle: 14 }); * scene.add(crows.object); * ``` */ declare function createFlock(options?: FlockOptions): Flock; type HerdType = 'deer' | 'sheep'; interface HerdOptions { /** deer (tall, slender, antlered) or sheep (short, woolly, tight flock). Default 'deer'. */ type?: HerdType; /** How many. Default 12 (deer) / 16 (sheep). */ count?: number; /** Where the herd grazes, world XZ (y is taken from the ground). Default [0, 0]. */ center?: [number, number]; /** Half-extents of the roaming area in XZ, or one number for a square. Default 18. */ radius?: [number, number] | number; /** Ground-height handshake — `terrain.heightAt`. Feet clamp to it every frame. Default flat 0. */ ground?: (x: number, z: number) => number; /** Cruise speed while walking, m/s. Default 2.2 (deer) / 1.4 (sheep). */ speed?: number; /** Body length, metres. Default 1 (deer) / 0.85 (sheep). */ size?: number; /** Body colour. Default 0xa9855a deer / 0xe7e2d6 sheep. */ color?: number; /** Fraction of time spent grazing (head down, still), 0–1. Default 0.6 (deer) / 0.75 (sheep). */ grazing?: number; /** How strongly the body tips to follow the slope, 0–1. Default 0.6. */ slopeAlign?: number; /** Steering weights. */ separation?: number; alignment?: number; cohesion?: number; seed?: number; } interface Herd { /** The instanced herd — add it to the scene. Self-animates. */ object: InstancedMesh; count: number; /** Live animal positions (world space) — for gameplay: spook them, count them, herd them. */ positions: readonly Vector3[]; /** Move the roaming area's centre in XZ (the herd drifts to follow). */ setCenter(x: number, z: number): void; /** Advance manually instead of self-driving (for deterministic loops). */ update(dt: number): void; } /** * A herd of deer or a flock of sheep — ground-dwelling ambient life. A boid * simulation steers every animal across the XZ plane (herds clump, so cohesion * runs high), while their feet clamp to the terrain every frame through the * `ground` handshake (`terrain.heightAt`). Animals graze in place — head down, * legs still — then walk a few steps and graze again. The whole herd draws as * **one InstancedMesh** whose legs stride and head dips in the vertex shader, * scaled by each animal's real speed, so a walking deer strides and a grazing * one nibbles. It self-animates from the render loop. * * `positions` exposes the live animals, so gameplay can read them — spook the * herd, count the flock, or wire the leader to a GAMA agent. * * ```ts * const terrain = createTerrain({ seed: 3 }); * const deer = createHerd({ type: 'deer', center: [0, 0], ground: terrain.heightAt }); * scene.add(terrain.mesh, deer.object); * ``` */ declare function createHerd(options?: HerdOptions): Herd; /** * Impact effects — the visible half of game feel. * * A hit that only changes a number is a spreadsheet event. What sells * contact is debris: dust where a boot lands, sparks where metal meets * stone, droplets where something takes the water. These are the burst * effects for those moments, built the way everything here is built — * low-poly, seeded, and cheap enough to spend freely. * * Two draw calls for every particle in flight, whatever you spawn. All * matte particles share one InstancedMesh, all glowing ones another, and * a particle is a tiny octahedron rather than a billboarded sprite — a * solid needs no camera to face, so `update(dt)` needs no camera, and at * this size the silhouette difference is invisible while the *tumble* a * flat sprite cannot do reads clearly. Fade is done with scale, not * opacity: instanced per-particle opacity would cost a custom shader, and * a mote shrinking to nothing is indistinguishable from one fading out. * * ```ts * const fx = createEffects({ seed: 4 }); * scene.add(fx.group); * fx.burst('dust', boot.position); * fx.burst('sparks', hit.point, { direction: hit.normal }); * fx.ring(splashPoint, { color: 0xbfe3ff }); * // per frame: * fx.update(dt); * ``` */ type BurstKind = 'dust' | 'sparks' | 'debris' | 'splash' | 'confetti'; interface EffectsOptions { /** Maximum simultaneous particles; past it the oldest are recycled. Default 320. */ capacity?: number; /** Ground height particles settle against. Default 0. */ floor?: number; /** Same seed, same debris. Default 1. */ seed?: number; } interface BurstOptions { /** Particle count for this burst. Default depends on kind. */ count?: number; /** Tint override (confetti ignores it — confetti is many colours or it is litter). */ color?: number; /** Particle size multiplier. Default 1. */ size?: number; /** Initial speed multiplier. Default 1. */ speed?: number; /** Launch direction bias (an impact normal). Default straight up. */ direction?: Vector3; /** Cone spread around the direction, 0 tight – 1 hemisphere. Default per kind. */ spread?: number; } interface RingOptions { /** Final radius in metres. Default 1.4. */ radius?: number; /** Seconds to reach it and fade. Default 0.55. */ life?: number; color?: number; /** Starting opacity. Default 0.65. */ opacity?: number; } interface Effects { /** Add this to the scene; both particle meshes and all rings live in it. */ group: Group; /** Spend some particles at a point. */ burst(kind: BurstKind, at: Vector3, options?: BurstOptions): void; /** An expanding, fading ground ring — landings, splashes, shockwaves. */ ring(at: Vector3, options?: RingOptions): void; /** Live particle count, for tests and debug readouts. */ readonly alive: number; update(dt: number): void; } declare function createEffects(options?: EffectsOptions): Effects; /** * A motion trail — the ribbon a fast thing leaves behind. * * Feed it a position every frame; it keeps the recent past as a ribbon * that tapers and fades toward the tail. The fade is real per-vertex * alpha: the colour attribute carries four components, which three treats * as RGBA when the material opts into vertex colours — no custom shader. * * The ribbon is laid perpendicular to the motion within the plane * orthogonal to `up`. With the default Y-up that is exactly right for * vehicles, skids and running characters; for a sword swing, pass the * swing plane's normal as `up`. * * ```ts * const trail = createTrail({ color: 0x8fd0ff, width: 0.3 }); * scene.add(trail.mesh); * // per frame: * trail.push(kart.position); * trail.update(dt); * ``` */ interface TrailOptions { /** Maximum points kept. Default 48. */ length?: number; /** Ribbon width at the head, in metres — it tapers to zero at the tail. Default 0.25. */ width?: number; /** Seconds a point survives. Default 0.7. */ life?: number; color?: number; /** Head opacity. Default 0.7. */ opacity?: number; /** Normal of the ribbon plane. Default +Y (a ground trail). */ up?: Vector3; /** Points closer than this to the last are ignored (metres). Default 0.05. */ minDistance?: number; } interface Trail { mesh: Mesh; /** Record the emitter's position this frame. */ push(point: Vector3): void; /** Age the ribbon; drop what has faded. */ update(dt: number): void; /** Forget everything — teleports should not draw a streak across the map. */ clear(): void; /** Live point count, for tests and debug readouts. */ readonly count: number; } declare function createTrail(options?: TrailOptions): Trail; /** * Marks — the world remembering what happened on it. * * A skid mark is the cheapest persistence-of-action trick in games: the * corner you overcooked is still written on the tarmac three laps later. * This is a pool of ground decals — skids, footprints, scorches — that * fade out over tens of seconds and recycle their oldest slot when full. * * One draw call for every mark on the map. The pool is a single * InstancedMesh of unit quads; which SHAPE a quad shows is decided in the * fragment shader by a per-instance attribute (a soft-ended streak, an * ellipse, a radial scorch), and the slow fade is a second per-instance * attribute — the two things instancing famously cannot vary are exactly * the two things a decal needs, so the shader carries them. * * ```ts * const marks = createMarks({ seed: 2 }); * scene.add(marks.mesh); * marks.stamp('skid', kart.position, kart.heading, { length: 1.4 }); * marks.stamp('footprint', foot.position, walkDir); * // per frame: * marks.update(dt); * ``` */ type GroundMarkKind = 'skid' | 'footprint' | 'scorch'; interface MarksOptions { /** Maximum marks on the ground at once. Default 96. */ capacity?: number; /** Seconds a mark takes to fade away. Default 18. */ fade?: number; /** Mark colour — dark, it multiplies against the ground. Default 0x1c1a17. */ color?: number; /** Height above y=0 the decals float to dodge z-fighting. Default 0.015. */ lift?: number; seed?: number; } interface StampOptions { /** Along-direction size in metres. Defaults: skid 1.2, footprint 0.26, scorch = width. */ length?: number; /** Across-direction size in metres. Defaults: skid 0.16, footprint 0.11, scorch 0.9. */ width?: number; /** Starting opacity 0..1. Default 0.75 (footprint 0.5). */ strength?: number; } interface Marks { mesh: InstancedMesh; /** * Leave a mark at `at`, oriented along `direction` (XZ). Scorches ignore * the direction and take a seeded rotation instead — burn marks have no * heading. */ stamp(kind: GroundMarkKind, at: Vector3, direction?: Vector3, options?: StampOptions): void; /** Live (visible) mark count. */ readonly count: number; update(dt: number): void; /** Wipe the ground clean. */ clear(): void; } declare function createMarks(options?: MarksOptions): Marks; interface InteriorSun { /** Sun elevation in [-1, 1] — same scale as `DayCycle.sunElevation`. */ elevation: number; /** * Compass angle of the sun in radians: 0 = +z, π/2 = +x, -π/2 = -x. * Default 0.35 (a pleasant morning slant). */ azimuth?: number; } interface InteriorLightOptions { /** * A `DayCycle` (or anything with `sunElevation` + `timeOfDay`) to follow: * each `update()` re-aims the shafts and re-grades the fill from it. */ cycle?: { readonly sunElevation: number; timeOfDay: number; }; /** Static sun when no cycle is bound. Default { elevation: 0.75, azimuth: 0.35 }. */ sun?: InteriorSun; /** Peak shaft opacity (0-1). Default 0.16. */ shaftStrength?: number; /** Dust motes drifting in each shaft. Default 26; 0 disables. */ dust?: number; palette?: Palette; } interface InteriorLight { /** Already added to `room.group` (shafts are room-local). */ group: Group; /** The ambient fill — exposed for manual grading. */ hemisphere: HemisphereLight; /** Aim the sun by hand (ignores any bound cycle until the next update). */ setSun(sun: InteriorSun): void; /** Re-read the bound day cycle, if any. Call from the game loop. */ update(dt?: number): void; } /** * Interior daylight for a `createRoom` interior: a palette-tinted hemisphere * fill (warm floor bounce under a cool ceiling), a volumetric-looking light * shaft through every sun-facing window — angled by the sun, landing in a * soft pool on the floor, with dust motes drifting through it — and the * window panes brightening at noon and going dark at night. * * No real lights beyond the single hemisphere: shafts and patches are unlit * additive quads, so the whole effect costs a handful of draw calls. Real * point lights stay a budget the room controls (`hearthLight`, torches). * * ```ts * const light = createInteriorLight(room, { cycle }); // follows the day * game.onUpdate(() => light.update()); // dusk = shafts die * ``` * * The group is added to `room.group` automatically (shafts are room-local), * so `room.setActive(false)` hides the light rig too. */ declare function createInteriorLight(room: Room, options?: InteriorLightOptions): InteriorLight; /** * The sea state — and the one thing in this library with a **memory**. * * The wind can get up in twenty minutes and drop in ten. The sea it raises * cannot: it takes hours to build and **days** to die. So the sea you are in * is almost never the sea that the wind you can feel would make. * * ```ts * const sea = createSeaState({ kind: 'ocean' }); * const ocean = createOcean({ sea: () => sea.trains, size: 900 }); * * sea.setWind(18, 250); // a gale from the west-south-west * sea.state; // 'building' — and it will be for hours * sea.setWind(0, 250); // …and now it drops flat calm * sea.state; // 'dying'. The sea does not care. * sea.height; // still 6 m, and still 4 m tomorrow * ``` * * That asymmetry is the module. A boat can shelter from wind behind a * headland and cannot shelter from the swell that came round it; a harbour * mouth is workable in a gale and unworkable the morning after one; and the * swell running under you right now may have been raised by a storm a * thousand miles away that you will never see. * * ## You cannot make an ocean sea in a lake * * A sea needs **fetch** — a stretch of open water for the wind to work on — * and **duration**. The same forty knots makes ripples across a lake, a nasty * short sea in a channel, and a thirty-foot swell in the Southern Ocean, and * the only difference is how far the wind had to work with: * * ```ts * createSeaState({ kind: 'lake' }).setWind(20, 0); // limit: 0.4 m * createSeaState({ kind: 'ocean' }).setWind(20, 0); // limit: 9.8 m * ``` * * ## Wind sea and swell are two different seas * * They come from different directions, because the swell came from somewhere * else. Where they cross, the sea is **confused** — and a confused sea is the * dangerous one, because there is no pattern in it to steer to. */ type SeaKind = 'lake' | 'coastal' | 'shelf' | 'ocean'; /** How much sea this water can hold, which is a question about its size. */ declare const SEA_KINDS: SeaKind[]; /** * rest / transitioning-toward / at-target / drifting-back — and the axis is * **the sea against the wind that is on it now.** * * `'dying'` is the state this module exists for: a big sea running under a * wind that could not possibly have raised it, because the wind that did has * gone somewhere else. */ type SeaCondition = 'calm' | 'building' | 'full' | 'dying'; /** One train of waves. Two of these make a sea. */ interface Train { /** Significant height, m — the average of the highest third, which is what * a sailor means by "the sea is running two metres". */ height: number; /** Seconds between crests. */ period: number; /** Crest to crest, m. `1.56 × period²` in deep water. */ length: number; /** Degrees it is coming FROM, the way a sailor says it. */ from: number; } interface SeaState { kind: SeaKind; /** `speed` m/s, `from` degrees. */ setWind(speed: number, from?: number): void; readonly wind: number; readonly windFrom: number; /** What this wind has managed to raise HERE, so far. */ readonly windSea: Train; /** What was raised somewhere else, and arrived. */ readonly swell: Train; /** The two of them, for `createOcean({ sea })`. Mutated in place — a live * view, not a snapshot. */ readonly trains: { windSea: Train; swell: Train; }; /** Combined significant height, m. Two trains add in QUADRATURE, not * arithmetically: two 3 m seas crossing make 4.2 m, not 6. */ readonly height: number; /** Of the bigger train. */ readonly period: number; /** Douglas sea state, 0–9. */ readonly douglas: number; /** * How confused it is, 0–1. Peaks when two trains of equal height cross at * right angles, and is zero when they run together — which is why a big * swell with the wind behind it is comfortable and the same swell on the * beam is not. */ readonly confusion: number; /** The most this wind could ever raise here, m. Fetch-limited or fully * developed, whichever is smaller. */ readonly limit: number; /** Metres of open water the wind has to work on. */ readonly fetch: number; /** Seconds until the sea is within a tenth of `limit`. `Infinity` if this * wind will never get there; 0 if it is already there. */ readonly building: number; /** Seconds until the sea is down to `metres`, if the wind stays as it is. * `Infinity` if it never will. */ fallsTo(metres: number): number; /** A swell from a storm you will never see. */ swellIn(from: number, height: number, period: number): void; readonly state: SeaCondition; onState?: (state: SeaCondition) => void; update(dt: number): void; } interface SeaStateOptions { kind?: SeaKind; /** Override the fetch, m. */ fetch?: number; /** Start with a sea already running. */ height?: number; /** …from here. */ from?: number; wind?: number; windFrom?: number; } /** Significant height a wind would raise given all the time and sea room in * the world. `0.0246 U²` — twenty metres a second makes ten metres of sea. */ declare function fullyDeveloped(wind: number): number; /** * What a wind can raise across a limited fetch, m. * * `0.0016 · U · √(F/g)`, the SMB form. It is the reason a lake in a gale is * unpleasant and not dangerous: forty knots across three kilometres of water * cannot make anything bigger than about half a metre, no matter how long it * blows. */ declare function fetchLimited(wind: number, fetch: number): number; /** Seconds between crests, for a sea of this height. Longer seas are older * seas, and that is the whole of it. */ declare function periodFor(height: number): number; /** Deep-water wavelength for a period, m. */ declare function lengthFor(period: number): number; /** Douglas sea state, 0–9, from significant height. */ declare function douglasFor(height: number): number; declare function createSeaState(options?: SeaStateOptions): SeaState; export { type AccumulateOptions, type BubbleOptions, type Bubbles, type BurstKind, type BurstOptions, type Caustics, type CausticsOptions, type Effects, type EffectsOptions, type EnvironmentOptions, type Fireworks, type FireworksOptions, type Flock, type FlockOptions, type FlockType, type GodRays, type GodRaysOptions, type GroundMarkKind, type Herd, type HerdOptions, type HerdType, type InteriorLight, type InteriorLightOptions, type InteriorSun, type LaunchOptions, type LightBudget, type LightBudgetOptions, type LightClaim, type LightGrant, type LightShafts, type LightShaftsOptions, type Lightning, type LightningOptions, type LightningTargets, type Marks, type MarksOptions, type Ocean, type OceanOptions, type Precipitation, type PrecipitationOptions, type PrecipitationType, type RingOptions, type RippleOptions, SEA_KINDS, type SceneEnvironment, type SeaCondition, type SeaKind, type SeaState, type SeaStateOptions, type Season, type SeasonGrade, type Seasons, type SeasonsOptions, type StampOptions, type Strike, type SurfOptions, type Trail, type TrailOptions, type Train, type WaterGrade, type WaterGradeOptions, type Weather, type WeatherOptions, type WeatherPreset, type WeatherStateParams, WindField, applyEnvironment, createBubbles, createCaustics, createEffects, createEnvironmentMap, createFireworks, createFlock, createGodRays, createHerd, createInteriorLight, createLightBudget, createLightShafts, createLightning, createMarks, createOcean, createPrecipitation, createSeaState, createSeasons, createTrail, createWaterGrade, createWeather, douglasFor, fetchLimited, fullyDeveloped, lengthFor, periodFor };