import { h as TreeSpecies, f as TreeOptions, i as WindField, R as RailTrack } from './track-BEKMppGw.js'; export { T as TREE_BIOMES, a as TREE_SPECIES, d as TreeBiome, e as TreeBiomeOptions, g as TreeSeason, n as createTree, t as treeBiome } from './track-BEKMppGw.js'; import { b as Palette, c as Prop, a as Carryable, G as Gathering, O as Obstacle, d as PropSlot, W as WaterBody, e as PropSurface } from './types-C_yucwmh.js'; import { S as ScatterItem } from './scatter-DqtkmBLg.js'; import { Object3D, Group, InstancedMesh, Vector3, MeshStandardMaterial, SpotLight, Vector2, PointLight } from 'three'; import { d as ScreenMode, f as ScreenPanel, c as PictureStyle, a as Picture, h as SurfaceKind } from './picture-xZNAPu7r.js'; /** * A silhouette family. The billboard carves this shape per-fragment from its * UVs — so an impostor is one camera-facing quad, not a texture. * - `conifer` — a tall triangle over a trunk (pine, **sequoia**). * - `round` — a broad dome (oak, maple, **banyan**). * - `column` — a slender flame (cypress). * - `umbrella` — a wide flat crown high on a thin trunk (**acacia**). * - `bottle` — a sparse crown on a fat tapering trunk (**baobab**). */ type ImpostorProfile = 'conifer' | 'round' | 'column' | 'umbrella' | 'bottle'; interface ImpostorOptions { /** Species to imitate (picks the silhouette, size and colours). */ species?: TreeSpecies; /** Silhouette family, if not deriving it from `species`. */ profile?: ImpostorProfile; /** Height in world units. Default: the species' typical height. */ height?: number; /** Crown diameter in world units. Default: derived from height. */ width?: number; /** Foliage colour (hex). Default: derived from the palette + species. */ foliage?: number; /** Trunk colour (hex). Default: the palette trunk. */ trunk?: number; seed?: number; palette?: Palette; } /** * A billboard **impostor** — a single camera-facing quad that stands in for a * full tree at distance. The species silhouette is carved procedurally from the * quad's UVs (no texture), lit by a baked gradient and three's fog, and the * billboard is expanded in the vertex shader so one shared quad faces the camera * for every instance. Built for {@link scatter}'s far-LOD slot, so a *dense * giant forest* — thousands of sequoias — can cull to a handful of draw calls * of billboards beyond the swap distance while the near tiles keep full geometry. * * ```ts * const far = createImpostor({ species: 'sequoia', palette }); * // usually via treeLOD(), which pairs the full tree with this automatically: * scatter({ items: [treeLOD('sequoia', { palette })], lod: { distance: 90 } }); * ``` */ declare function createImpostor(options?: ImpostorOptions): Prop; interface TreeLODOptions extends TreeOptions { /** Relative frequency among scatter items. Default 1. */ weight?: number; /** Full-detail visual variants. Default 4. */ variants?: number; /** Per-instance uniform scale range. Default [0.85, 1.2]. */ scale?: [number, number]; } /** * A {@link ScatterItem} that pairs a full {@link createTree} with its billboard * {@link createImpostor} — the near/far LOD couple. Drop it straight into * `scatter({ items, lod })` and tiles past the swap distance trade full trees * for camera-facing impostors, so a forest of giants stays a few draw calls at * range while the trees you can walk up to keep every branch. * * ```ts * const forest = scatter({ * area, surface: terrain.heightAt, density: 0.01, minSpacing: 7, * items: [treeLOD('sequoia', { palette }), treeLOD('pine', { palette, weight: 3 })], * lod: { distance: 90, tileSize: 24 }, * }); * scene.add(forest.group); * // each frame: forest.update(camera) * ``` */ declare function treeLOD(species: TreeSpecies, options?: TreeLODOptions): ScatterItem; interface RockOptions { seed?: number; /** Approximate radius. Default ~0.4–1.1 by seed. */ size?: number; palette?: Palette; } /** * A seeded low-poly boulder: an icosahedron with jittered vertices and a * flattened underside so it sits on the ground. */ declare function createRock(options?: RockOptions): Prop; interface CrateOptions { seed?: number; size?: number; /** 0–1 color wear. Default 0.3. */ weathering?: number; palette?: Palette; } /** * A wooden crate: panel box with darker edge framing, seeded wear tint. It's * a **carryable** — hand it to ANIMA's `Carry` and a character hoists it to * the chest (`carry: 'crate'`), the hold point offset to the box's centre. */ declare function createCrate(options?: CrateOptions): Carryable; /** * Carryable props — things a character picks up and carries. Each returns a * `Carryable` (a `Prop` plus a `carry` style and hold-point `grip`), so it * drops into ANIMA's `Carry`: `new Carry(rig, loco).pickUp(createBarrel())`. * Origins sit at the base (place them on the ground); `grip` puts the hold * point where the hands go, so no runtime IK is needed. */ interface CarryableOptions { seed?: number; color?: number; palette?: Palette; } /** A hooped wooden barrel — hugged to the chest (`crate` style). */ declare function createBarrel(options?: CarryableOptions): Carryable; /** A woven basket with an arched handle — carried at the side, by the handle. */ declare function createBasket(options?: CarryableOptions): Carryable; /** A cinched sack — hoisted onto the shoulder (`shoulder` style). */ declare function createSack(options?: CarryableOptions): Carryable; interface LanternOptions extends CarryableOptions { /** Emissive glow colour. Default warm. */ glow?: number; } /** A hand lantern — carried at the side, hanging from its bail. Glass glows. */ declare function createLantern(options?: LanternOptions): Carryable; /** * Rhythmic work stations — props built to be WORKED, not just used. A worker * stands at the `work` slot playing the station's `action` loop (ANIMA: chop, * mine, saw, stir), and each frame you drive `update(dt, working)`: the * station advances `progress` (0→1), throws a burst of effects on the impact * beat (chips, sparks, sawdust, steam) and fires `onYield` once per cycle — * the "do work over time and produce something" verb. Pair the yield with * GAMA's `Stockpile`: * * Walk the worker to the `work` slot, hold the tool, and layer the action * loop over the idle stance (the loop owns the arms — don't fight it with a * held pose): * * ```ts * const block = createChoppingBlock(); * attach(rig, 'handRight', block.tool); // ANIMA holds the axe * const swing = loco.overlay(createLoopClip(rig, block.action)); // the chop, over idle * block.onYield = () => stock.add('wood'); // GAMA counts the logs * game.onUpdate((t) => block.update(t.delta, working)); // effects + progress + yield * ``` */ interface WorkStation extends Prop { /** The ANIMA loop the worker plays ('chop' | 'mine' | 'saw' | 'stir'). */ readonly action: string; /** Progress toward the next yield, 0→1 (resets each cycle). */ readonly progress: number; /** A tool the worker holds — `attach(rig, 'handRight', station.tool)`. */ readonly tool: Object3D; /** Fired once per work cycle, with the running total produced. */ onYield?: (total: number) => void; /** Advance the station. `working` (default true) gates progress + effects. */ update(dt: number, working?: boolean): void; } interface WorkStationOptions { seed?: number; palette?: Palette; } /** A chopping block: a stump with a log to split, an axe, flying wood chips. */ declare function createChoppingBlock(options?: WorkStationOptions): WorkStation; /** An ore vein: a boulder streaked with glowing ore, a pickaxe, dust + sparks. */ declare function createOreVein(options?: WorkStationOptions): WorkStation; /** A cook-pot: a cauldron on a tripod over embers, a ladle, rising steam. */ declare function createCookpot(options?: WorkStationOptions): WorkStation; /** A sawhorse with a plank being cut, a hand-saw, and falling sawdust. */ declare function createSawhorse(options?: WorkStationOptions): WorkStation; interface FenceOptions { seed?: number; /** Run length along local +x. Default 6. */ length?: number; postSpacing?: number; height?: number; palette?: Palette; } /** * A rustic fence run along local +x, centered at the origin: posts with * two slightly-crooked rails. Chain several and rotate to enclose areas. * The obstacle radius covers the whole run (rough but steering-safe); * for tight navigation, bake a navmesh — the geometry is authoritative. */ declare function createFence(options?: FenceOptions): Prop; interface LampOptions { seed?: number; height?: number; /** Add a real PointLight. Off by default — lights are a budget. */ light?: boolean; lightIntensity?: number; palette?: Palette; } /** A street lamp: post, head, glowing bulb, optional real PointLight. */ declare function createLamp(options?: LampOptions): Prop; interface GrassOptions { seed?: number; /** Blades per tuft. Default 4–6 by seed. */ blades?: number; /** A WindField to sway the blades in. */ wind?: WindField; palette?: Palette; } /** * A tuft of grass blades — pure scatter fodder (zero obstacle footprint, * walk straight through). Sways beautifully under `applyWind`. */ declare function createGrassTuft(options?: GrassOptions): Prop; interface BushOptions { seed?: number; size?: number; /** A WindField to gently sway the foliage in. */ wind?: WindField; palette?: Palette; } /** A low foliage bush: two or three squashed blobs. Small footprint. */ declare function createBush(options?: BushOptions): Prop; type WallStyle = 'plaster' | 'brick' | 'ashlar'; type RoofStyle = 'tile' | 'shingle' | 'thatch'; interface HouseOptions { seed?: number; /** Footprint width (gable side). Default seeded 3.2–4.2. */ width?: number; depth?: number; wallHeight?: number; /** Wall surface. Default seeded (mostly plaster, some brick / ashlar). */ wall?: WallStyle; /** Roof surface. Default seeded (tile, wooden shingle or straw thatch). */ roof?: RoofStyle; palette?: Palette; } /** * A cottage: plastered walls, gabled roof, door, chimney and emissive * windows. Pass the house in `createDayCycle`'s `lamps` list and its * windows glow at night along with the street lamps. A stone foundation * extends below ground so sloped terrain never shows a gap. */ declare function createHouse(options?: HouseOptions): Prop; interface TowerOptions { seed?: number; height?: number; palette?: Palette; } /** A wooden watchtower: splayed legs, platform with railing, pyramid roof. */ declare function createTower(options?: TowerOptions): Prop; interface WellOptions { seed?: number; palette?: Palette; } /** A stone well: ring, posts, little gabled roof, hanging bucket. */ declare function createWell(options?: WellOptions): Prop; interface RuinOptions { seed?: number; /** Footprint width. Default seeded 3.5–5. */ size?: number; /** Moss reclaiming the up-facing stone. Default true. */ mossy?: boolean; palette?: Palette; } /** * A ruined building: a partial rectangle of crumbling wall segments with * seeded gaps and heights, and tumbled blocks around the floor. */ declare function createRuin(options?: RuinOptions): Prop; type StallGoods = 'produce' | 'pottery' | 'bakery' | 'textiles'; interface StallOptions { seed?: number; /** What the stall sells. Default seeded. */ goods?: StallGoods; /** Awning fabric colour (the coloured stripe). Default seeded. */ clothColor?: number; palette?: Palette; } /** * A market stall: four posts, a forward-sloping striped canvas awning with a * fringed valance, a plank counter and back shelf, stocked with seeded goods. * Four trades — `produce`, `pottery`, `bakery`, `textiles` — each stocks the * counter differently, so a market row reads as a bustling variety rather * than one stall repeated. */ declare function createStall(options?: StallOptions): Prop; type BannerStyle = 'flag' | 'banner' | 'pennant'; type BannerPattern = 'solid' | 'bands' | 'stripes' | 'bicolor' | 'cross' | 'saltire' | 'diamond'; interface BannerOptions { seed?: number; /** flag (flies from a pole), banner (hangs from a crossbar), pennant (triangular). */ style?: BannerStyle; /** Heraldic device. Default seeded. */ pattern?: BannerPattern; /** Field colour and charge colour (hex). Default seeded from a heraldic set. */ colors?: [number, number]; /** Pole height in metres. Default seeded ~3.2–3.8. */ poleHeight?: number; /** Wind strength (ripple amplitude multiplier). Default 1. */ wind?: number; palette?: Palette; } /** * A flag, banner or pennant on a pole — real cloth, not a stiff board. The * fabric is a subdivided plane rippled by a GPU vertex wave: a travelling * fold that grows from the fixed edge to the free fly, droops under its own * weight, and carries a seeded phase so a row of flags never waves in * lockstep. Flat-shaded facets catch the light of each fold, and the whole * thing animates itself from the render loop — no per-frame wiring, so it * works dropped straight into `scatter` or a village. Heraldic devices * (cross, saltire, bands, diamond…) are baked as vertex colours, so there * are no textures to fetch. */ declare function createBanner(options?: BannerOptions): Prop; interface FireOptions { seed?: number; /** Add a flickering warm PointLight. Default true. */ light?: boolean; /** Overall flame scale multiplier. Default 1. */ scale?: number; palette?: Palette; } /** * A standing brazier: a metal fire-bowl on splayed legs, filled with glowing * coals under a live flame, ringed by rising embers and casting a flickering * warm light. Self-animating. */ declare function createBrazier(options?: FireOptions): Prop; /** * A campfire: a ring of stones around charred logs stacked in a lean, a live * flame with rising embers, glowing under a flickering warm light. * Self-animating. */ declare function createCampfire(options?: FireOptions): Prop; /** * Interior furniture — the cottage set. Every piece follows the Prop * contract (seeded, palette-themed, origin at floor level, honest * obstacleRadius), so it scatters, steers and re-themes like any other * SCENA prop; it just happens to live indoors. */ type TableStyle = 'round' | 'trestle' | 'desk'; interface TableOptions { seed?: number; /** 'round' pedestal table, long 'trestle' board, or a small 'desk'. */ style?: TableStyle; palette?: Palette; } /** A wooden table: round pedestal, long trestle board, or writing desk. */ declare function createTable(options?: TableOptions): Prop; type SeatStyle = 'chair' | 'bench' | 'stool'; interface SeatOptions { seed?: number; /** A slat-back 'chair', a long 'bench', or a three-legged 'stool'. */ style?: SeatStyle; palette?: Palette; } /** Something to sit on: chair, bench or stool. Seat height ≈ 0.45. */ declare function createSeat(options?: SeatOptions): Prop; type BedSize = 'single' | 'double' | 'bunk'; interface BedOptions { seed?: number; /** 'single', wide 'double', or stacked 'bunk'. */ size?: BedSize; palette?: Palette; } /** A post bed with mattress, quilt and pillow. Bunks stack two. */ declare function createBed(options?: BedOptions): Prop; type ShelfStock = 'books' | 'pottery' | 'food' | 'empty'; interface ShelfOptions { seed?: number; /** What lines the boards: 'books', 'pottery', 'food' or 'empty'. */ stock?: ShelfStock; palette?: Palette; } /** A tall open shelf, boards lined with seeded books, pots or provisions. */ declare function createShelf(options?: ShelfOptions): Prop; interface ChestOptions { seed?: number; /** Tilt the lid open. Default false. */ open?: boolean; palette?: Palette; } /** A banded storage chest with a domed lid; `open` tilts it back. */ declare function createChest(options?: ChestOptions): Prop; type CandleStyle = 'single' | 'candelabra' | 'chandelier'; interface CandleOptions { seed?: number; /** * 'single' tabletop candle on a dish, a standing 'candelabra', or a * 'chandelier' meant to hang (origin at the hook — position it at the * ceiling and it hangs down). */ style?: CandleStyle; /** Add one real PointLight. Default false — glow is free, lights are not. */ light?: boolean; palette?: Palette; } /** Candlelight: glowing flames with a gentle flicker; real light opt-in. */ declare function createCandle(options?: CandleOptions): Prop; type RugShape = 'round' | 'square' | 'runner'; interface RugOptions { seed?: number; /** 'round' banded disc, 'square' bordered mat, or a long 'runner'. */ shape?: RugShape; palette?: Palette; } /** A woven rug: banded, palette-dyed, and walk-through (radius 0). */ declare function createRug(options?: RugOptions): Prop; /** * Props that seat *several* characters at once — the dining table, the * park bench, the game board, the ring of logs round a fire. * * The geometry is the easy half. What makes a group of bodies read as * people rather than mannequins is the detail these generators encode: * * - **Nothing is square.** Every chair is nudged off its ideal angle and * pushed back a different amount, because nobody in the history of * dining has left a chair exactly where they found it. The seat slot * inherits that crookedness, so the sitters land crooked too — and one * seeded radian of it does more for realism than another thousand * triangles. * - **Every seat has an approach.** A slot's `approach` anchor stands a * pace behind the chair. Characters walk *there*, then turn and lower. * - **Every gathering has a focus.** The bowl in the middle, the board, * the fire. Aim the occupants' gaze at it (ANIMA's `LookAt`, or a * `Conversation`) and adjacency becomes company. */ interface GatheringOptions { seed?: number; palette?: Palette; } interface DiningTableOptions extends GatheringOptions { /** How many places to lay. Round tables ring them; trestles line them up. */ seats?: number; /** 'round' pedestal or long 'trestle' board. */ style?: 'round' | 'trestle'; /** Lay plates and cups. Default true. */ settings?: boolean; } /** * A table with chairs round it and the meal laid out — the archetypal * gathering. Round tables seat everyone equally; trestles have a head. */ declare function createDiningTable(options?: DiningTableOptions): Gathering; interface PicnicTableOptions extends GatheringOptions { /** Places, split between the two benches. Default 6. */ seats?: number; } /** A trestle top with the benches built on — six round it, elbow to elbow. */ declare function createPicnicTable(options?: PicnicTableOptions): Gathering; interface LongBenchOptions extends GatheringOptions { /** Places along the bench. Default 3. */ seats?: number; /** Slatted back and armrests. Default true. */ back?: boolean; } /** * The park bench: several places on one continuous seat. Its slots are * deliberately *loose* — real strangers do not sit at even spacing, they * take the ends first and leave the middle for last. GAMA's `Occupancy` * does the choosing; the bench just offers the room. */ declare function createLongBench(options?: LongBenchOptions): Gathering; type BoardGame = 'chess' | 'cards' | 'dice'; interface GameTableOptions extends GatheringOptions { /** What is being played. Default 'chess'. */ game?: BoardGame; } /** * Two stools and a small table between them — the two-player prop. Both * seats face each other across a shared `focus` (the board), which is * what makes the pair read as *opponents* rather than two people who both * happen to be sitting down. */ declare function createGameTable(options?: GameTableOptions): Gathering; interface CampCircleOptions extends GatheringOptions { /** Log seats in the ring. Default 5. */ seats?: number; /** Ring radius in metres. Default 1.9. */ radius?: number; } /** * Logs and stumps ringing a fire pit — the oldest gathering there is. * The ring is deliberately gappy and uneven; drop a `createCampfire` at * the origin and the `focus` is already aimed at the flames. */ declare function createCampCircle(options?: CampCircleOptions): Gathering; interface BuntingOptions { seed?: number; /** Distance between the two poles (metres). Default seeded ~4.5–6. */ span?: number; /** Pole height. Default seeded ~2.6–3.2. */ poleHeight?: number; /** Number of hanging flaglets. Default scales with span. */ flags?: number; /** Festive colours to cycle through. Default a bright fair palette. */ colors?: number[]; palette?: Palette; } /** * A string of festive bunting slung between two poles: a rope in a natural * catenary droop, hung with little triangular pennants that flutter on the * same GPU cloth wave as the flags — each with its own phase, so the whole * line ripples like a real garland in a breeze. Self-animating: it advances * its own clock from the render loop, so it just flutters wherever you drop * it. */ declare function createBunting(options?: BuntingOptions): Prop; type StatueFigure = 'obelisk' | 'figure' | 'orb' | 'bust' | 'beast'; type StatueMaterial = 'stone' | 'bronze'; interface StatueOptions { seed?: number; /** What stands on the pedestal. Default seeded. */ figure?: StatueFigure; /** Sculpture material. Default seeded (mostly stone). */ material?: StatueMaterial; /** Overall height in metres (pedestal + figure). Default ~3.4. */ height?: number; palette?: Palette; } /** * A town statue or monument: a stepped stone pedestal carrying one of five * seeded figures — an `obelisk`, a robed `figure`, an `orb` monument, a * `bust`, or a guardian `beast`. Sculpted in weathered `stone` or patinated * `bronze`. A natural centrepiece for a plaza or the heart of a village. */ declare function createStatue(options?: StatueOptions): Prop; interface FountainOptions { seed?: number; /** Basin width (square). Default seeded ~3–3.6. */ size?: number; /** Centrepiece figure. Default seeded (small figures suit a fountain). */ figure?: StatueFigure; /** Material of the centrepiece. Default 'stone'. */ centrepiece?: 'stone' | 'bronze'; palette?: Palette; } /** * A tiered town fountain: a square stone basin brimming with animated water * (SCENA's own `createWater`, self-driven here), a central pedestal carrying * a small statue that spouts, an upper catch-bowl, sheets of water falling * between the tiers and a fine spray of droplets at the jet. Self-animating — * the water ripples and the spray falls with no per-frame code. */ declare function createFountain(options?: FountainOptions): Prop; type CartStyle = 'cart' | 'wagon'; type CartCargo = 'empty' | 'crates' | 'barrels' | 'sacks' | 'hay'; interface CartOptions { seed?: number; /** Two-wheel hand `cart` (with shafts) or four-wheel `wagon`. Default seeded. */ style?: CartStyle; /** What it carries. Default seeded. */ cargo?: CartCargo; palette?: Palette; } /** * A wooden cart or wagon: spoked wheels with iron tyres, a planked bed with * low sideboards, and either two pull-shafts (a hand `cart`) or four wheels * (a `wagon`). Optionally loaded with crates, barrels, sacks or hay. Forward * is +X. */ declare function createCart(options?: CartOptions): Prop; type SignKind = 'post' | 'hanging' | 'fingerpost' | 'milestone'; /** One arm of a fingerpost: a place name and the way to it. */ interface Direction { text: string; /** Compass-ish bearing in degrees the arm points (0 = +X, 90 = +Z). Default seeded. */ angle?: number; /** Arm height up the post, in metres. Default stacked automatically. */ height?: number; } interface SignOptions { seed?: number; /** post (board on a post), hanging (a swaying shop sign), fingerpost (direction arms), milestone (carved stone). */ kind?: SignKind; /** The words on the sign. Default a seeded place name. Ignored by fingerpost (use `directions`). */ text?: string; /** Fingerpost arms. Default three seeded directions. */ directions?: Direction[]; /** Lettering colour (hex). Default seeded gold/cream. */ inkColor?: number; /** Painted panel colour behind the lettering (hex). Default a seeded deep tone. */ panelColor?: number; /** Board timber colour (hex). Default palette wood. */ boardColor?: number; /** Overall height in metres (post top). Default per-kind. */ height?: number; palette?: Palette; } /** * A signpost with real, legible lettering — the "stylised text on props" * frontier. Letters are carved as bold, rounded relief from an embedded vector * font (no textures, no font files, no `three/examples` loaders) and set on a * painted panel so they read cleanly at a distance, day or dusk. * * - `post` a framed board on a post, lettered on both faces. * - `hanging` a shop sign on a bracket that sways gently on its hooks — * self-animated from the render loop, like the banners. * - `fingerpost` a cluster of pointed arms, each naming a place and pointing * the way ("MARKET →", "HARBOUR →"). * - `milestone` a weathered stone marker with the name painted onto it. */ declare function createSign(options?: SignOptions): Prop; /** * Luminous props — things that are lights, whether or not they get one. * * Every fixture here has three faces: a **body** (the mesh), a **glow** * (emissive bulb + additive halo sprite — always visible, costs nothing), * and a **claim** — the `{ anchor, color, intensity, radius, priority, * isLit }` it hands to a `LightBudget`, which may or may not grant it a * real PointLight. The claim's `isLit` closes over the fixture's own * state, so `setLit(false)` both darkens the prop *and* frees its slot in * the budget with no wiring. * * `setLit` is deliberately the same verb-shape a GAMA `linkMechanism` * boolean drives — a lever wired to a lamp is a lighting puzzle with no * imports between the libraries. */ interface LuminousClaim { anchor: Object3D; color: number; intensity: number; radius: number; priority: number; isLit: () => boolean; } interface Luminous extends Prop { readonly lit: boolean; setLit(on: boolean): void; /** Hand this to `createLightBudget().register(...)`. */ claim: LuminousClaim; /** Fixtures with motion (twinkle, buzz, beam sweep) advance here. */ update?(dt: number): void; } interface StreetLightOptions { style?: 'village' | 'modern'; seed?: number; height?: number; palette?: Palette; } /** * A street light. `village` is a dark post with a lantern cage and a warm * mantle; `modern` is a slim pole whose arm cranes a cool flat head over * the road (light the +x side). */ declare function createStreetLight(options?: StreetLightOptions): Luminous; interface LanternLightOptions { /** Hanging (hook at the origin, lantern below) or standing on its base. */ hanging?: boolean; seed?: number; color?: number; palette?: Palette; } /** A small warm lantern — the light a hand, a porch or a market stall carries. */ declare function createLanternLight(options?: LanternLightOptions): Luminous; interface NeonSignOptions { color?: number; /** Cap height of the letters, metres. Default 0.5. */ height?: number; seed?: number; /** Mount a dark backboard behind the tubes. Default true. */ backboard?: boolean; } interface NeonSign extends Luminous { /** Tube segments built — a legibility smoke signal for tests. */ segments: number; } /** * A neon sign: the vector font's glyph strokes re-materialized as glowing * tube runs. One seeded letter *buzzes* — every real neon sign has one — * dipping and reigniting on its own nervous rhythm in `update(dt)`. * Authored facing +z with the origin at the sign's center. */ declare function createNeonSign(text: string, options?: NeonSignOptions): NeonSign; interface StringLightsOptions { /** Horizontal span between the two hang points, metres. Default 6. */ span?: number; /** How far the middle droops. Default 0.45. */ sag?: number; count?: number; /** Bulb colors, cycled. Default a warm festival mix. */ colors?: number[]; seed?: number; /** Bulbs breathe brightness in update(dt). Default true. */ twinkle?: boolean; } /** * A sagging run of festival bulbs. Authored along local x, hang points at * (±span/2, 0, 0) — position and rotate the group to string it between * anything. Bulbs are one InstancedMesh with per-instance color; `update` * makes them breathe out of phase. */ declare function createStringLights(options?: StringLightsOptions): Luminous; interface RevolvingBeaconOptions { /** Height of the head above the origin. Default 3.5. */ height?: number; color?: number; /** Beam revolutions per second. Default 0.15. */ speed?: number; seed?: number; } /** * A rotating beacon — the lighthouse move at any scale. Two opposed * additive beam cones sweep with the head; the volumetric look is just * geometry, no shader tricks. Feed `update(dt)` to turn it. */ declare function createRevolvingBeacon(options?: RevolvingBeaconOptions): Luminous; interface PhotocellOptions { seed?: number; /** Longest ignition straggle after dusk, seconds. Default 3. */ spread?: number; /** Sun elevation where dusk trips. Default 0.04 (just above the horizon). */ threshold?: number; } interface Photocell { /** 'day' | 'night' — which side of dusk the cell believes it is. */ readonly state: 'day' | 'night'; /** Switch orders scheduled but not yet fired. */ readonly pending: number; update(dt: number): void; } /** * The photocell — why streets ripple alight instead of blinking on as one. * * Watches anything with a `sunElevation` (a `DayCycle`, structurally) and * flips each fixture's `setLit` when dusk or dawn trips, each after its own * seeded delay within `spread`. The thresholds are hysteretic — dusk trips * a touch above the horizon, dawn well after — so a sun grazing the * threshold can't make the street flap. */ declare function createPhotocell(sky: { sunElevation: number; }, fixtures: ReadonlyArray<{ setLit(on: boolean): void; }>, options?: PhotocellOptions): Photocell; /** * Aircraft — the vehicle kit grows wings. * * Same contract as the cars and boats: the prop renders and animates; * WHO flies it is GAMA's problem. Feed `update(dt, input)` the flight * state and the airplane *shows* it — the propeller spins and becomes a * translucent blur disc past a third throttle, the control surfaces * deflect with the intent (elevator, differential ailerons, rudder), * retractable gear folds away, and the wingtip nav lights (red port, * green starboard, white tail, a seeded strobe) are luminous CLAIMS * that drop into a `LightBudget` like any street lamp. * * Authored nose toward +z, origin at the ground contact — park it, * taxi it, or hand its pose to a flight controller. */ interface AircraftInput { /** Engine setting 0..1. Props spin with it; blur past ~0.35. */ throttle?: number; /** Elevator deflection, -1 (nose down) .. 1 (nose up). */ pitch?: number; /** Aileron deflection, -1 .. 1 (differential, left stick left = -1). */ roll?: number; /** Rudder deflection, -1 .. 1. */ yaw?: number; /** Landing gear down. Default true. Retractables fold over ~1.5 s. */ gearDown?: boolean; } interface AircraftProp extends Prop { update(dt: number, input?: AircraftInput): void; /** Nav lights + strobe — register each with a LightBudget. */ claims: LuminousClaim[]; /** Master switch for the nav lights. */ setLit(on: boolean): void; readonly lit: boolean; wingspan: number; length: number; } interface PlaneOptions { style?: 'prop' | 'airliner'; seed?: number; color?: number; palette?: Palette; } declare function createPlane(options?: PlaneOptions): AircraftProp; interface HelicopterInput { /** Rotor spool, 0..1 — blades blur past ~0.5, and droop when parked. */ rotor?: number; /** Cyclic: tilts the rotor disc to show intent, -1..1 each. */ cyclicPitch?: number; cyclicRoll?: number; /** The nose searchlight. */ light?: boolean; } interface HelicopterProp extends Prop { update(dt: number, input?: HelicopterInput): void; /** Nav lights + the searchlight — register with a LightBudget. */ claims: LuminousClaim[]; /** Aim pivot for the searchlight: rotate to sweep the beam. */ searchlight: Object3D; /** Searchlight on/off (the claim and beam follow). */ setSearchlight(on: boolean): void; readonly searchlightOn: boolean; /** Current rotor spool, 0..1 (lerps toward the input). */ readonly rotor: number; } interface HelicopterOptions { seed?: number; color?: number; palette?: Palette; } /** * A utility helicopter: cabin, boom, skids, a main rotor that droops * when parked and blurs into a disc when spooled, a tail rotor doing * the same sideways, and a nose SEARCHLIGHT — an aimable pivot with an * additive beam and a luminous claim, ready to be the visible half of a * GAMA `Flashlight` sweeping an `Illumination` field. */ declare function createHelicopter(options?: HelicopterOptions): HelicopterProp; interface FighterInput extends AircraftInput { /** Light the burner regardless of throttle (default: throttle > 0.8). */ afterburner?: boolean; } interface FighterProp extends Prop { update(dt: number, input?: FighterInput): void; claims: LuminousClaim[]; setLit(on: boolean): void; readonly lit: boolean; /** Under-wing anchors, each carrying a dummy round until launched. */ hardpoints: Object3D[]; /** Rounds still hanging. */ readonly armed: number; /** * Take the round off hardpoint `i`: hides the dummy and returns the * launch pose in WORLD space — hand it straight to GAMA `Missiles.fire`. * Null if that rail is already empty. */ launchFrom(i: number): { position: { x: number; y: number; z: number; }; direction: { x: number; y: number; z: number; }; } | null; /** Hang fresh rounds on every rail. */ rearm(): void; wingspan: number; length: number; } interface FighterOptions { seed?: number; color?: number; palette?: Palette; /** Rails under the wings. Default 2. */ hardpoints?: number; } /** * A delta-wing fighter: extruded delta, ELEVONS (each surface mixes * pitch and roll — that's what elevons are), a big fin, retractable * gear, and an afterburner whose flame lights past 80% throttle and * flickers on its own seeded nerve. The under-wing hardpoints carry * dummy rounds; `launchFrom(i)` hands GAMA's `Missiles` a world-space * launch pose and hides the round, so the missile the game flies is * the missile the wing stops carrying. */ declare function createFighterJet(options?: FighterOptions): FighterProp; /** * The airfield — everything an airplane parks beside. * * A runway with real markings (the vector font finally lying flat: the * numbers are the heading in tens of degrees, and the far end reads the * reciprocal, because that is what runways DO), a windsock that reads an * actual `WindField` structurally — its swing is the wind's direction * and its droop the wind's strength, which makes it weather * instrumentation you can unit-test — a hangar to hide from it in, and * a helipad for the release after this one. */ interface RunwayOptions { /** Strip length, metres. Default 60. */ length?: number; /** Strip width, metres. Default 8. */ width?: number; /** * Runway number at the NEAR end (heading in tens of degrees, 1–36). * The far end shows the reciprocal automatically. Default 27. */ number?: number; seed?: number; } interface Runway extends Prop { /** The number painted at the near end. */ number: number; /** The far end's designation — always the reciprocal. */ reciprocal: number; } declare function createRunway(options?: RunwayOptions): Runway; interface WindsockOptions { /** Pole height, metres. Default 4. */ pole?: number; seed?: number; } interface Windsock extends Prop { /** Where the sock points (radians, world XZ) — DOWNwind, like the real thing. */ readonly angle: number; /** How far the sock hangs off horizontal (0 = flying straight, ~1.2 = limp). */ readonly droop: number; /** Feed it the wind (a WindField, structurally) every frame. */ update(dt: number, wind?: { direction: { x: number; y: number; }; strength: number; }): void; } declare function createWindsock(options?: WindsockOptions): Windsock; interface HangarOptions { width?: number; depth?: number; seed?: number; palette?: Palette; } /** An open-front arch hangar — park the trainer out of the weather. */ declare function createHangar(options?: HangarOptions): Prop; interface HelipadOptions { radius?: number; seed?: number; } /** A round pad with the ring and the H — the font's flattest job yet. */ declare function createHelipad(options?: HelipadOptions): Prop; /** * Pickups — the things a game loop is made of. * * A pickup is a small seeded prop with a built-in idle (spin at a seeded * phase plus a sine bob — a field of coins must never tick in lockstep) * and two transitions: `collect()` pops it out of the world and * `respawn()` shimmers it back, each returning its duration so the caller * can schedule the consequences. The gem wears the `gemstone` surface — * dispersion doing the "this one is valuable" work games usually fake * with a glow sprite. * * The gameplay handshake is the house rule: `trigger` is structurally an * `Obstacle` (`{center, radius}`), with `center` a LIVE reference to the * prop's root position — hand it to GAMA's proximity queries and neither * library imports the other. SCENA renders the pickup; who gets credit * for touching it is the game loop's business. * * ```ts * const gem = createPickup('gem', { seed: 7 }); * gem.group.position.set(4, 0.8, -2); * scene.add(gem.group); * // per frame: * gem.update(dt); * // when the game loop says so: * const wait = gem.collect(); * ``` */ type PickupKind = 'coin' | 'gem' | 'key' | 'heart' | 'star' | 'orb' | 'potion'; interface PickupOptions { seed?: number; /** Overall size multiplier. Default 1. */ scale?: number; /** Tint override for the body material. */ color?: number; /** Bob amplitude in metres. Default 0.07. */ bob?: number; /** Idle spin speed, radians/second. Default 1.6. */ spin?: number; } type PickupState = 'idle' | 'collecting' | 'collected' | 'respawning'; interface Pickup { group: Group; kind: PickupKind; /** Where the game loop should test proximity: live centre + pick radius. */ trigger: Obstacle; readonly state: PickupState; /** Pop out of the world. Returns the animation's seconds; 0 if not idle. */ collect(): number; /** Shimmer back in. Returns the animation's seconds; 0 unless collected. */ respawn(): number; update(dt: number): void; } declare function createPickup(kind: PickupKind, options?: PickupOptions): Pickup; interface PickupFieldOptions { seed?: number; scale?: number; color?: number; bob?: number; spin?: number; } interface FieldTrigger extends Obstacle { /** Which instance this trigger belongs to. */ index: number; } interface PickupField { mesh: InstancedMesh; /** One live trigger per position; collected entries stay but stop mattering. */ triggers: FieldTrigger[]; /** How many are still collectable. */ readonly remaining: number; isActive(index: number): boolean; collect(index: number): number; respawn(index: number): number; update(dt: number): void; } /** * A hundred coins, one draw call. Positions are fixed at creation (they * are the level design); each instance idles at its own seeded phase and * collapses when collected. Composite kinds (key, heart, potion) need a * mesh per pickup — use `createPickup` for those; this throws rather than * silently rendering the wrong thing. */ declare function createPickupField(kind: PickupKind, positions: readonly Vector3[], options?: PickupFieldOptions): PickupField; /** * Markers — the furniture of objectives. A checkpoint arch that knows * whether it is next, a capture zone that turns on the ground, a beacon * readable across a whole map, and a chequered finish gate. Almost pure * state machine: the geometry is cheap and all the value is in the * transitions, because a player reads a checkpoint's STATE at a glance * or not at all. * * Every marker exposes `trigger` — structurally an `Obstacle` * (`{center, radius}`, centre a live reference to the marker's root) — * so GAMA's Circuit and trigger queries consume them without imports. */ type CheckpointState = 'upcoming' | 'active' | 'passed'; interface CheckpointOptions { seed?: number; /** Clear width between the pillars, metres. Default 4. */ width?: number; /** Height to the underside of the beam. Default 3. */ height?: number; /** Emissive colour when active. Default 0x53c7f0. */ color?: number; } interface Checkpoint { group: Group; trigger: Obstacle; readonly state: CheckpointState; /** upcoming = slow pulse · active = bright · passed = dim green. */ setState(state: CheckpointState): void; update(dt: number): void; } declare function createCheckpoint(options?: CheckpointOptions): Checkpoint; interface ZoneOptions { seed?: number; /** Zone radius, metres. Default 2.2. */ radius?: number; color?: number; /** Rotating dash count. Default 14. */ dashes?: number; } interface Zone { group: Group; trigger: Obstacle; /** 0..1 — how "captured"/charged the zone reads. Drives the fill ring. */ setProgress(value: number): void; update(dt: number): void; } /** * A flat ground ring with rotating dashes — spawn pad, capture area, * charge circle. `setProgress` fills an inner ring so the game loop can * show how long you have stood in it. */ declare function createZone(options?: ZoneOptions): Zone; interface BeaconOptions { seed?: number; /** Pillar height, metres. Default 9. */ height?: number; color?: number; } interface Beacon { group: Group; trigger: Obstacle; update(dt: number): void; } /** * A pillar of light readable across the map — the "go HERE" a radar can * only hint at. Additive, double-sided, fading with height; a small base * ring anchors it to the ground so it reads as placed, not painted. */ declare function createBeacon(options?: BeaconOptions): Beacon; interface FinishGateOptions { seed?: number; /** Clear width between posts. Default 6. */ width?: number; /** Height to the banner's underside. Default 3.2. */ height?: number; } interface FinishGate { group: Group; trigger: Obstacle; update(dt: number): void; } /** * The chequered line. The banner is real geometry — one InstancedMesh of * alternating cells, no texture — so it stays crisp at any distance and * dresses both sides. */ declare function createFinishGate(options?: FinishGateOptions): FinishGate; /** * Hazards — the props where movement itself is the game. * * Platforms that carry you, floors that give way, pads that launch you, * blades that swing where you wanted to walk, belts that move the ground, * and the pressure plate that turns all of SCENA's doors and gates into * puzzle vocabulary. Everything speaks the trilogy's structural dialect: * `{center, radius}` triggers for proximity, live `delta`/`velocity` * vectors for riders, and the plate is shaped exactly like GAMA's * `MechanismSource` — `linkMechanism(plate, door)` and the level has its * first circuit, with no imports between the libraries. */ type PlatformMotion = 'linear' | 'orbit' | 'pendulum'; interface PlatformOptions { seed?: number; /** Top surface size, metres. Default [2.4, 1.8] (x, z). */ size?: [number, number]; motion?: PlatformMotion; /** linear: the two ends. Defaults ±3 on x. */ from?: Vector3; to?: Vector3; /** orbit/pendulum: swing radius, metres. Default 3. */ radius?: number; /** Seconds for a full cycle (there AND back for linear). Default 6. */ period?: number; } interface MovingPlatform { group: Group; /** Height of the standing surface above the group origin. */ top: number; /** * How far the platform moved LAST update — add it to whoever stands on * top and they ride; skip it and they moonwalk off the edge. */ delta: Vector3; /** Current velocity, m/s — for launching off the edge with momentum. */ velocity: Vector3; trigger: Obstacle; update(dt: number): void; } declare function createPlatform(options?: PlatformOptions): MovingPlatform; type CrumbleState = 'solid' | 'shaking' | 'falling' | 'gone' | 'returning'; interface CrumbleOptions { seed?: number; size?: [number, number]; /** Seconds of warning shudder after being disturbed. Default 0.7. */ delay?: number; /** Seconds gone before it returns. Default 3. */ respawn?: number; } interface CrumblingPlatform { group: Group; top: number; trigger: Obstacle; readonly state: CrumbleState; /** Someone stood on it. Starts the shudder (once). */ disturb(): void; /** Is it currently safe to stand on? */ readonly solid: boolean; update(dt: number): void; } declare function createCrumblingPlatform(options?: CrumbleOptions): CrumblingPlatform; interface BouncePadOptions { seed?: number; /** Pad radius, metres. Default 0.9. */ radius?: number; /** Launch speed handed back by bounce(), m/s. Default 11. */ strength?: number; color?: number; } interface BouncePad { group: Group; trigger: Obstacle; /** Squash, stretch, and return the launch speed for the caller to apply. */ bounce(): number; update(dt: number): void; } declare function createBouncePad(options?: BouncePadOptions): BouncePad; interface PendulumOptions { seed?: number; /** Arm length, metres. Default 3. */ length?: number; /** Swing half-angle, radians. Default 1.05 (~60°). */ amplitude?: number; /** Seconds per full swing there and back. Default 2.6. */ period?: number; } interface Pendulum { group: Group; /** The blade's LIVE world-offset circle (relative to group position). */ hazard: Obstacle; update(dt: number): void; } /** Hang the group from a beam; the blade swings below and `hazard` follows it. */ declare function createPendulum(options?: PendulumOptions): Pendulum; interface SpikeTrapOptions { seed?: number; /** Plate size, metres. Default [1.6, 1.6]. */ size?: [number, number]; /** 'cycling' extends on a timer; 'triggered' waits for spring(). Default 'cycling'. */ mode?: 'cycling' | 'triggered'; /** Cycling: seconds per full cycle. Default 2.4. */ period?: number; } interface SpikeTrap { group: Group; trigger: Obstacle; /** True while the spikes are OUT — the only time this square hurts. */ readonly dangerous: boolean; /** Triggered mode: spring the trap now. */ spring(): void; update(dt: number): void; } declare function createSpikeTrap(options?: SpikeTrapOptions): SpikeTrap; interface ConveyorOptions { seed?: number; /** Belt length and width, metres. Default 6 × 1.6. */ length?: number; width?: number; /** Surface speed along local +x, m/s (negative reverses). Default 1.6. */ speed?: number; } interface Conveyor { group: Group; /** Live surface velocity in WORLD space — add `velocity * dt` to riders. */ velocity: Vector3; trigger: Obstacle; /** Change the belt speed (chevrons and velocity follow). */ setSpeed(speed: number): void; update(dt: number): void; } declare function createConveyor(options?: ConveyorOptions): Conveyor; interface PressurePlateOptions { seed?: number; /** Plate size, metres. Default [1.3, 1.3]. */ size?: [number, number]; /** * Latching plates stay pressed once stood on (a puzzle solved); momentary * plates release when everyone steps off (a door held). Default false. */ latching?: boolean; } interface PressurePlate { group: Group; trigger: Obstacle; /** GAMA MechanismSource, structurally: pressed = open = powering the link. */ readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; onChange?: (open: boolean) => void; /** Tell the plate how many stand on it this frame (0 releases momentary). */ occupy(count: number): void; update(dt: number): void; } /** * The keystone: it depresses under weight and speaks GAMA's mechanism * dialect, so `linkMechanism(plate, door)` wires it to every door, gate * and drawbridge SCENA already ships. Feed it an occupancy count each * frame — GAMA's `Occupancy` or a plain trigger test both know it. */ declare function createPressurePlate(options?: PressurePlateOptions): PressurePlate; type BreakableKind = 'crate' | 'barrel' | 'pot'; type BreakableState = 'intact' | 'breaking' | 'debris'; interface BreakableOptions { seed?: number; /** Overall size, metres. Default 0.9. */ size?: number; /** Shard count. Default 9. */ shards?: number; } interface Breakable { group: Group; kind: BreakableKind; trigger: Obstacle; readonly state: BreakableState; /** * Where dropped loot belongs, in the group's local frame — hand it to * a pickup's position when the shards fly. */ loot: Vector3; /** Come apart. `impulse` biases the shards' flight (a hit direction). */ break(impulse?: { x: number; y?: number; z: number; }): void; /** Back in one piece — pooled levels reuse their props. */ reset(): void; update(dt: number): void; } declare function createBreakable(kind: BreakableKind, options?: BreakableOptions): Breakable; interface TargetDummyOptions { seed?: number; /** Total height, metres. Default 1.7. */ height?: number; } interface TargetDummy { group: Group; trigger: Obstacle; readonly toppled: boolean; /** Wobble away from the blow. `power` scales the swing. */ hit(from?: { x: number; y?: number; z: number; }, power?: number): void; /** Over it goes — the KO. */ topple(): void; reset(): void; update(dt: number): void; } /** * The training-yard prop: a post, a torso, a head, and a spring. Hits * wobble it (a damped pendulum about its base); `topple()` lays it * down and it stays down. Wire its trigger to GAMA's Projectiles and * its `hit` to the impact event, and the yard teaches aim. */ declare function createTargetDummy(options?: TargetDummyOptions): TargetDummy; interface ScoreboardOptions { seed?: number; /** Digit count. Default 3. */ digits?: number; /** Digit height, metres. Default 0.42. */ size?: number; /** Board colour. Default deep green, like the ground's own boards. */ color?: number; digitColor?: number; } interface Scoreboard { group: Group; /** Show a value (clamped to what the digits can hold). Flips animate. */ set(value: number): void; readonly value: number; update(dt: number): void; } declare function createScoreboard(options?: ScoreboardOptions): Scoreboard; interface StumpsOptions { seed?: number; /** Stump height, metres. Default 0.71 (the laws'). */ height?: number; } interface Stumps { group: Group; trigger: Obstacle; readonly struck: boolean; /** * The ball arrives. Bails FLY (each on its own arc and spin), the hit * stumps lean. `direction` is the ball's travel; `power` scales it. */ strike(direction?: { x: number; y?: number; z: number; }, power?: number): void; reset(): void; update(dt: number): void; } /** * Three stumps, two bails, and the single most satisfying piece of * feedback in cricket: the bails coming off. Until now the trilogy's * wickets were scenery; these are an event. */ declare function createStumps(options?: StumpsOptions): Stumps; /** * Trade utilities — the props that give a room a job: a smith's forge, a * baker's oven, a weaver's loom, a taverner's counter. Same Prop contract * as everything else; `furnishRoom` places them by role. */ interface WorkshopOptions { seed?: number; /** Forge only: add the real flickering PointLight. Default true. */ light?: boolean; palette?: Palette; } /** A smith's corner: coal forge, anvil on a stump, quench barrel. */ declare function createForge(options?: WorkshopOptions): Prop; /** A baker's dome oven: stone dome, ember-lit mouth, chimney stub. */ declare function createOven(options?: WorkshopOptions): Prop; /** A weaver's upright loom: frame, warp threads, cloth growing up it. */ declare function createLoom(options?: WorkshopOptions): Prop; /** A taverner's counter: paneled bar with mugs and a jug on top. */ declare function createCounter(options?: WorkshopOptions): Prop; interface TreadmillOptions { seed?: number; /** Belt speed in m/s — feed the same number to the runner's Locomotion. */ speed?: number; palette?: Palette; } interface TreadmillProp extends Prop { /** Current belt speed (m/s). */ speed: number; /** Change the belt speed; the tread bars follow. */ setSpeed(speed: number): void; } /** * A gym treadmill with a genuinely moving belt — instanced tread bars * marching toward the runner. Stand a character on the `run` slot and * drive its `Locomotion` with `treadmill.speed`: running without going * anywhere, which is the whole idea. */ declare function createTreadmill(options?: TreadmillOptions): TreadmillProp; interface GuitarOptions { seed?: number; /** Body finish. Default warm 'teak'; try palette colors for electrics. */ color?: number; palette?: Palette; } /** * An acoustic guitar, origin at the body's centre, neck up +y — sized to * ANIMA's GRIPS.guitar so it sits right across a strumming character's * chest (position it there and add the `strum` loop), or lean it on a * wall/stand as décor. */ declare function createGuitar(options?: GuitarOptions): Prop; interface BathroomOptions { seed?: number; palette?: Palette; } /** A ceramic toilet: pedestal, bowl, seat at sit height, tank. */ declare function createToilet(options?: BathroomOptions): Prop; /** A pedestal sink with a chrome tap. */ declare function createSink(options?: BathroomOptions): Prop; /** A freestanding bathtub on feet — with a `soak` slot (the sleep pose). */ declare function createBathtub(options?: BathroomOptions): Prop; /** * Land vehicles — low-poly, palette-themed, and ALIVE: every vehicle * exposes a kinematic `update(dt, { speed, steer })` that spins the wheels * radius-correctly, turns the fronts and twirls the steering wheel, plus * GRIPS-conformant `slots` so an ANIMA character drops into the driver's * seat with the `drive` (or `cycle`) pose and their hands land on the * controls by construction. GAMA steering output plugs straight in: * speed from `agent.velocity.length()`, steer from the heading change. */ interface VehicleInput { /** Ground speed in m/s (wheels spin to match). */ speed?: number; /** Steering angle in radians (front wheels + steering wheel follow). */ steer?: number; } interface VehicleProp extends Prop { /** Advance the running gear. Call from your game loop when it moves. */ update(dt: number, input?: VehicleInput): void; } interface VehicleOptions { seed?: number; /** Body colour. Defaults to a seeded pick from the palette. */ color?: number; palette?: Palette; } /** A compact modern car: powder-coat body, glass cabin, driver's slot. */ declare function createCar(options?: VehicleOptions): VehicleProp; /** A bicycle: frame, saddle at GRIPS height, handlebar, cranking pedals. */ declare function createBike(options?: VehicleOptions): VehicleProp; /** A farm tractor: big rears, small steering fronts, open perch, stack. */ declare function createTractor(options?: VehicleOptions): VehicleProp; /** A box truck: cab, cargo box, six wheels, driver's slot up front. */ declare function createTruck(options?: VehicleOptions): VehicleProp; /** * Watercraft — hulls that genuinely ride the sea. `float(heightAt)` binds a * water sampler (`createOcean(...).heightAt` or `createWater`'s level) and * `update(dt, { speed })` bobs, pitches and rolls the hull on the waves * under it while it makes way. Helm slots seat an ANIMA character. */ interface CraftInput { /** Way through the water, m/s. */ speed?: number; } interface CraftProp extends Prop { /** Bind the water: a sampler like `ocean.heightAt(x, z)`. */ float(heightAt: (x: number, z: number) => number): void; /** Ride the waves + advance. Call from the game loop. */ update(dt: number, input?: CraftInput): void; } interface CraftOptions { seed?: number; color?: number; palette?: Palette; } /** An open motor boat: planked hull, bench seats, outboard, helm slot. */ declare function createBoat(options?: CraftOptions): CraftProp; /** A small coastal ship: high hull, deckhouse, mast, railed deck. */ declare function createShip(options?: CraftOptions): CraftProp; /** * Modern building components — the steel-railing tier. Everything is on the * standard Prop contract and themed by the Tier-4 surfaces: brushed steel, * powder-coat, teak, corten, concrete and architectural glass. */ type RailingStyle = 'bars' | 'cable' | 'glass' | 'panel'; interface RailingOptions { seed?: number; /** Vertical 'bars', horizontal 'cable', frameless 'glass', or a decorative laser-cut 'panel'. */ style?: RailingStyle; /** Run length along local +x. Default 4. */ length?: number; /** Rail height. Default 1.05. */ height?: number; palette?: Palette; } /** A modern railing run — balconies, terraces, stairs. Like `createFence`, * the run lies along local +x and the obstacle circle spans it. */ declare function createRailing(options?: RailingOptions): Prop; type ModernWindowStyle = 'fixed' | 'sliding'; interface ModernWindowOptions { seed?: number; width?: number; height?: number; /** Mullion grid: [columns, rows] of panes. Default [2, 1]. */ mullions?: [number, number]; /** 'fixed' glazing or 'sliding' (two offset panes on a track). */ style?: ModernWindowStyle; /** Glass options passthrough. */ tint?: number; frosted?: boolean; /** Emissive pane the day cycle ignites at dusk. Default true. */ nightGlow?: boolean; palette?: Palette; } interface ModernWindowProp extends Prop { /** The glass material — hand it to a day cycle via the building's lamps. */ pane: MeshStandardMaterial; } /** A framed modern window standing on local origin, facing ±z. */ declare function createModernWindow(options?: ModernWindowOptions): ModernWindowProp; type GateStyle = 'slat' | 'bars' | 'panel'; interface GateOptions { seed?: number; /** Horizontal 'slat' (teak or steel), vertical 'bars', or motif 'panel'. */ style?: GateStyle; /** Clear opening width. Default 3.2. */ width?: number; /** Leaf height. Default 1.6. */ height?: number; /** Masonry pillars flanking the gate (with warm cap lamps). Default true. */ pillars?: boolean; /** 0 closed … 1 fully open. Default 0. */ open?: number; /** One sliding leaf instead of two swing leaves. */ sliding?: boolean; palette?: Palette; } interface GateProp extends Prop { /** Drive the gate: 0 closed … 1 open (swing leaves rotate, sliders slide). */ setOpen(fraction: number): void; } /** A modern driveway gate between concrete pillars. */ declare function createGate(options?: GateOptions): GateProp; type CladdingStyle = 'slats' | 'louvers' | 'stone'; interface CladdingOptions { seed?: number; /** Vertical teak 'slats', angled 'louvers', or a 'stone' feature panel. */ style?: CladdingStyle; width?: number; height?: number; palette?: Palette; } /** A facade accent panel — the modern-bungalow signature. Flat against ±z. */ declare function createCladding(options?: CladdingOptions): Prop; interface PergolaOptions { seed?: number; width?: number; depth?: number; palette?: Palette; } /** A teak pergola: four posts, doubled beams, rafter slats. Walk-through * (obstacleRadius 0) — feed the posts to steering yourself if needed. */ declare function createPergola(options?: PergolaOptions): Prop; interface PlanterOptions { seed?: number; /** Trough length. Default 1.6. */ length?: number; palette?: Palette; } /** A corten planter trough with low greenery. */ declare function createPlanter(options?: PlanterOptions): Prop; /** * Manipulables — props with a STATE that a character (or GAMA) actuates, and * that animate in response: doors swing, drawers slide, levers throw, valves * spin, hatches hinge, portcullises rise. This is the "operate and the world * responds" verb the interaction system was missing — until now props were * inert (except vehicles, which only spin wheels). * * Every manipulable exposes a small, uniform control surface: * * ```ts * const door = createDoor(); * door.toggle(); // flip open/closed * door.onChange = (open) => …; // fires when the target flips * game.onUpdate((t) => door.update(t.delta)); // eases the joint toward target * ``` * * `state` is the live eased position (0 = closed/rest, 1 = open/actuated); * `open` is the boolean target. The shape is structurally identical to GAMA's * `Mechanism`, so `Interactable`/`linkMechanism`/`Trigger` drive these without * either library importing the other. Props that a character stands at to * work (lever, valve, drawer, hatch) publish an `operate` slot at ANIMA's * floor-level anchor convention; doors and portcullises are pass-through. */ interface Manipulable extends Prop { /** Live eased position: 0 = closed/rest … 1 = open/actuated. */ readonly state: number; /** The current target: true once set/toggled past halfway open. */ readonly open: boolean; /** Flip open↔closed. Returns the new `open`. */ toggle(): boolean; /** Drive to open (`true`/1), closed (`false`/0), or a partial target in [0,1]. */ set(target: number | boolean): void; /** Ease the joint toward the target. Call every frame. */ update(dt: number): void; /** Fired when the target flips open↔closed (after `set`/`toggle`). */ onChange?: (open: boolean) => void; } interface MechanismOptions { seed?: number; palette?: Palette; /** Body/paint colour; defaults to a seeded or preset pick. */ color?: number; /** How fast the joint travels toward its target (1/sec). Default 3. */ speed?: number; } interface DoorOptions extends MechanismOptions { width?: number; height?: number; /** Hinge on the 'left' (−x) or 'right' (+x) post. Default 'left'. */ hinge?: 'left' | 'right'; /** Swing-open angle in radians. Default ~1.9 (just past 90°). */ swing?: number; /** Two leaves meeting in the middle (a gateway). Default false. */ double?: boolean; } /** * A framed door that swings on its hinge — the workhorse manipulable. Set * `double` for a two-leaf gateway (both leaves swing apart). Walk-through * when open; the frame is thin scenery. */ declare function createDoor(options?: DoorOptions): Manipulable; interface DrawerOptions extends MechanismOptions { width?: number; height?: number; depth?: number; } /** A cabinet with a single drawer that slides out toward the front (+z). */ declare function createDrawer(options?: DrawerOptions): Manipulable; interface LeverOptions extends MechanismOptions { /** Handle length. Default 0.6. */ length?: number; /** Mount on a floor 'base' or a 'wall' plate. Default 'base'. */ mount?: 'base' | 'wall'; } /** * A throw lever — the canonical switch. The handle swings from back to * forward as it actuates; wire its `onChange` to a gate/portcullis with * GAMA's `linkMechanism` for switch-driven level logic. */ declare function createLever(options?: LeverOptions): Manipulable; interface ValveOptions extends MechanismOptions { /** Wheel radius. Default 0.28. */ radius?: number; /** Full turns from closed to open. Default 3. */ turns?: number; } /** A pipe valve — a hand-wheel that spins through several turns as it opens. */ declare function createValve(options?: ValveOptions): Manipulable; interface HatchOptions extends MechanismOptions { width?: number; depth?: number; /** Lid open angle in radians. Default ~2.0. */ angle?: number; } /** * A hinged lid — a chest, a crate top, a floor trapdoor. The lid hinges up * and back off its rear edge; a character stands at the front to open it. */ declare function createHatch(options?: HatchOptions): Manipulable; interface PortcullisOptions extends MechanismOptions { width?: number; height?: number; } /** * A castle portcullis — a barred iron grille in a stone gateway that rises to * open. The medieval-village payoff for the switch/lever wiring: throw a * lever, raise the gate. */ declare function createPortcullis(options?: PortcullisOptions): Manipulable; /** * Things to climb, and the tack that goes on a horse. * * Both exist to be *met* by an ANIMA character: the ladder publishes the * anchors its `Climb` controller needs, and the saddle and bridle are * built to the fixtures a `createQuadruped` already carries, so a rider * lands in the seat and the reins run to the mouth without any runtime IK. */ type LadderStyle = 'wooden' | 'steel' | 'rope'; interface LadderOptions { seed?: number; /** Height to the top rung, metres. Default 3.2. */ height?: number; /** 'wooden' rungs, a 'steel' fixed ladder, or a 'rope' ladder. */ style?: LadderStyle; /** Width between the rails. Default 0.44. */ width?: number; palette?: Palette; } /** * A climbable ladder. Publishes `bottom`, `top` and `rungSpacing` — * structurally ANIMA's `Climbable`, so it drops straight into * `new Climb(rig, loco).start(ladder)` with no cross-imports. * * `rungSpacing` is the contract that matters: ANIMA drives the body up by * exactly that much per half-cycle of the climb loop, so hands land on * rungs rather than sliding past them. */ interface Ladder extends Prop { /** Floor-level anchor at the foot; +z faces INTO the rungs. */ bottom: Object3D; /** Anchor level with the top rung, where the climber steps off. */ top: Object3D; rungSpacing: number; rungs: number; } declare function createLadder(options?: LadderOptions): Ladder; type TackStyle = 'english' | 'western' | 'bareback'; interface TackOptions { seed?: number; /** An 'english' saddle, a 'western' one with a horn, or 'bareback' pad. */ style?: TackStyle; /** Withers height of the horse it goes on, metres. Default 1.62. */ horseHeight?: number; /** Leather colour. */ color?: number; palette?: Palette; } /** * A saddle, built to sit on ANIMA's `QuadrupedRig.saddle` fixture. * * ```ts * const tack = createSaddle({ horseHeight: horse.height }); * horse.saddle.add(tack.object); // it lands where the seat is * ``` * * The stirrups hang where the rider's foot goes, which is the whole point: * ANIMA's ride pose puts the heel down at that height, so the two meet by * construction rather than by fiddling. */ declare function createSaddle(options?: TackOptions): Prop; /** * A bridle: headstall, browband, bit and reins. Add it to the horse's * `Head` bone so it follows every nod. */ declare function createBridle(options?: TackOptions): Prop; /** * Electronics — the props that make an interior read as *now* rather than as * a period set. Everything here is a body plus a `ScreenPanel`: the panel is * the whole point, the chassis exists to hold it at the right height and * angle. * * Every one of these publishes `screen`, which is structurally ANIMA's * `Viewable` (a character can look at it) and GAMA's `DisplayTarget` (a * device can drive what it shows) — so the three libraries compose here with * no imports between them, the same handshake pattern as seats and ladders. * * ```ts * const tv = createTelevision({ diagonal: 1.4, mode: 'video' }); * scene.add(tv.object); * const glow = createScreenLight(tv.screen); // the room flickers with it * game.onUpdate((t) => { tv.screen.update(t.delta); glow.update(); }); * ``` */ interface ScreenProp extends Prop { /** The lit panel: gaze target, glow source, display target. */ screen: ScreenPanel; } interface ScreenPropOptions { seed?: number; /** Panel diagonal in metres. */ diagonal?: number; /** What it shows on creation. */ mode?: ScreenMode; /** UI accent colour. */ accent?: number; /** Emissive gain — a television is brighter than a watch. */ brightness?: number; /** Rows per second for 'feed'. */ scrollRate?: number; palette?: Palette; } /** * A desk monitor. Origin at the base of the foot, so it stands on a desk * surface at y = 0. */ declare function createMonitor(options?: ScreenPropOptions): ScreenProp; interface TelevisionOptions extends ScreenPropOptions { /** * 'stand' rests on a pedestal (origin at the pedestal base — put it on a * media unit); 'wall' has no support (origin at the panel's bottom edge, * so you position it at the height you want it hung). */ mount?: 'stand' | 'wall'; } /** A television: a big thin panel, either on a pedestal or hung on a wall. */ declare function createTelevision(options?: TelevisionOptions): ScreenProp; interface LaptopOptions extends ScreenPropOptions { /** Lid angle: 0 shut, 1 fully open (~105°). Default 1. */ open?: number; } /** * A laptop. Origin at the base of the deck, so it sits on a desk at y = 0. * The lid hinges at the rear edge; `open` drives the angle. */ declare function createLaptop(options?: LaptopOptions): ScreenProp; /** A smart speaker with a face: fabric body, screen raked back to be read. */ declare function createSmartDisplay(options?: ScreenPropOptions): ScreenProp; /** A tablet: a screen you can pick up. Carryable + ScreenProp, no new verbs. */ interface ScreenCarryable extends Carryable { screen: ScreenPanel; } /** * A tablet. It is a `Carryable` with a `Screen` — which means pick-up, * carry-while-walking, put-down and hand-off all already work on it, from * the carryables track, with nothing added here. */ declare function createTablet(options?: ScreenPropOptions): ScreenCarryable; interface ScreenLightOptions { /** Multiplier over the panel's own glow. Default 1. */ gain?: number; /** Metres in front of the panel face. Default 0.04 — it emits AT the glass. */ distance?: number; /** Falloff range. Default generous: a screen washes a whole room dimly. */ range?: number; /** Cone half-angle, radians. Default 1.2 (~69°) — a screen is not a torch. */ spread?: number; /** Attach to the panel surface so it tracks the prop. Default true. */ attach?: boolean; } /** * A real light that copies what a screen is showing. * * This is the whole reason to bother drawing content procedurally: because * the CPU knows the colour and level of the current shot, a light can carry * exactly that, and a television lights a dark room in flickers timed to its * own cuts. A static blue point light reads as a lamp; this reads as a TV. * * Lights are a budget, as everywhere else in SCENA — this is opt-in per * panel, and a room full of monitors should light one or two faces and let * the rest glow on their emissive alone. */ interface ScreenLight { light: SpotLight; update(): void; } declare function createScreenLight(panel: ScreenPanel, options?: ScreenLightOptions): ScreenLight; /** * A phone. Portrait, unlike everything else here — modern handsets are about * 19.5:9 the tall way, and a 16:9 landscape panel scaled down reads as a * tiny television. * * It is a `Carryable` with a `Screen`, which means pick-up, carry, put-down * and hand-off already work on it. Handing someone your phone to show them a * photo needs no new verb: it is `handTo`. */ declare function createPhone(options?: ScreenPropOptions): ScreenCarryable; /** * A smartwatch: the smallest screen the system draws, and a useful proof that * one content shader covers a 55" television and a 40 mm watch face. Parent * it to an ANIMA hand socket. */ declare function createSmartwatch(options?: ScreenPropOptions): ScreenProp; /** * Terminals — the machines you have to queue for. * * These are the first props in the kit that a character does not simply walk * up to and use: there may already be somebody at it, and a second person has * to wait. So besides the usual slot, every terminal publishes a **line** — * an anchor at the head of the queue, with the queue running back along its * local -z. * * ```ts * const atm = createTerminal({ style: 'atm' }); * const queue = new Queue({ service: 14, spacing: atm.spacing }); * // distance from GAMA's Queue -> a world position on the line * const at = atm.line.localToWorld(new Vector3(0, 0, -queue.distanceOf(person))); * ``` * * That pairing is the whole handshake: SCENA says where the line is, GAMA * says who is where along it, and neither imports the other. */ interface Terminal extends Prop { /** The machine's display. */ screen: ScreenPanel; /** Where the user stands to operate it. */ slot: PropSlot; /** * Head of the queue. The line runs BACK along this anchor's local -z, so a * distance `d` from a queue maps to `line.localToWorld(new Vector3(0,0,-d))`. * It faces the machine, so a character copying its rotation faces the right * way while they wait. */ line: Object3D; /** Suggested metres between people in the line. */ spacing: number; } type TerminalStyle = 'atm' | 'kiosk' | 'vending'; interface TerminalOptions { style?: TerminalStyle; seed?: number; /** What the display shows. Defaults suit the style (an ATM shows a keypad). */ mode?: ScreenMode; palette?: Palette; /** Metres between people queueing. Default 0.62. */ spacing?: number; } /** * A machine with a screen, a place to stand, and a queue behind it. * * - `atm` — a wall unit with a hooded screen at eye height and a keypad shelf. * - `kiosk` — a freestanding pillar with the screen raked back to be read * standing over it. * - `vending` — a glass-fronted cabinet with a small screen and a delivery * flap at knee height. */ declare function createTerminal(options?: TerminalOptions): Terminal; /** * Fixtures — the small wall-mounted things a smart home is made of. * * These are deliberately tiny. A switch is 8 cm across and a sensor smaller * than that, so almost none of the modelling budget goes on shape: what makes * them read is the **indicator** — a single lit pip whose colour says what the * device thinks is happening. That pip is the whole prop at any honest camera * distance, which is why every one of these publishes `setIndicator`. * * ```ts * const sensor = createFixture({ style: 'sensor' }); * home.on('motion', (v) => sensor.setIndicator(v > 0.5 ? 0x44ff88 : 0x223026)); * ``` */ interface Fixture extends Prop { /** Set the indicator colour, and how hard it burns (0 = dark). */ setIndicator(color: number, strength?: number): void; /** The panel, on the fixtures that carry one (thermostat only). */ screen?: ScreenPanel; /** Where a character stands to reach it, on the ones you touch. */ slot?: PropSlot; /** The mounting height this was built for, in metres. */ height: number; } type FixtureStyle = 'switch' | 'thermostat' | 'doorbell' | 'camera' | 'sensor'; interface FixtureOptions { style?: FixtureStyle; seed?: number; palette?: Palette; /** Indicator colour to start with. */ indicator?: number; } /** * A wall fixture. The origin is at the **wall face**, with the device facing * +z — so parenting it to a wall and pushing it to the right height is the * whole placement job. * * - `switch` — a rocker plate at hand height, with a slot to stand at. * - `thermostat` — a small round dial with a screen in it. * - `doorbell` — a button with a bright ring, up beside a door. * - `camera` — a stub body angled down, with a lens and a status pip. * - `sensor` — a corner-mounted wedge; the smallest thing in the kit. */ declare function createFixture(options?: FixtureOptions): Fixture; /** * A desk set: keyboard, mouse and a mug. What a character actually puts their * hands on, which the monitors and laptops so far did not give them. * * Origin at the desk surface. Publishes a `keyboard` anchor at the home row, * so ANIMA's desk poses have something to aim the wrists at rather than a * number somebody guessed. */ interface DeskSet extends Prop { /** Centre of the home row, on the desk surface. */ keyboard: Object3D; /** Where the mouse sits. */ mouse: Object3D; } declare function createDeskSet(options?: { seed?: number; }): DeskSet; /** * Wall art — the things that go on a wall so it stops being plaster. * * Every room built with this kit so far has had bare walls, and in every * screenshot that is the loudest thing wrong with it: furniture, characters * and lighting all read, and then the background is a flat sheet of colour * that no inhabited room has ever had. * * All of these share one convention, the same one `createFixture` uses: the * **origin sits at the wall face and the art faces +z**, centred. So placing * one is a position and nothing else — and `hangOn` does even that for you. * * ```ts * const picture = createPainting({ width: 0.7, style: 'landscape', seed: 4 }); * hangOn(room.walls[2], picture, { height: 1.55, seed: 4 }); * ``` */ interface WallArt extends Prop { /** Overall size including the frame, in metres. */ width: number; height: number; /** The image, on the pieces that carry one. */ picture?: Picture; } /** * Frame profiles. * * - `none` — a stretched canvas with its edges showing. Modern, cheap, right. * - `thin` — a narrow dark moulding. The safe default for prints and photos. * - `wide` — a broad flat face, painted. Reads as a gallery print. * - `ornate` — stepped gilt. The only one with any real geometry in it. * - `box` — a deep shadow box with the image recessed. * - `clip` — no frame at all: a bare sheet with four small clips. */ type FrameStyle = 'none' | 'thin' | 'wide' | 'ornate' | 'box' | 'clip'; interface PaintingOptions { /** Image width in metres (excluding the frame). Default 0.62. */ width?: number; /** Image height. Defaults to a seeded portrait/landscape proportion. */ height?: number; /** What it is a picture of. Defaults to a seeded pick. */ style?: PictureStyle; /** Moulding. Default 'thin'. */ frame?: FrameStyle; /** Frame colour. Defaults to gilt for `ornate`, dark wood otherwise. */ frameColor?: number; /** Yellowing and darkened varnish (0–1). Default 0.25. */ age?: number; /** A glass front that catches highlights. Default false for paintings. */ glazed?: boolean; /** An inset mount board between frame and image, in metres. Default 0. */ mount?: number; seed?: number; palette?: Palette; } /** * A picture on a wall. * * The proportion is seeded rather than square, because a wall of squares is * as obviously generated as a wall of identical images. Portrait and * landscape formats both turn up, and the `style` follows the format when * it is not asked for — a portrait picture in a landscape frame is a tell. */ declare function createPainting(options?: PaintingOptions): WallArt; interface FramedPhotoOptions { /** Long edge in metres. Default 0.16 — a photo is small. */ size?: number; style?: PictureStyle; /** Add a hinged back strut so it stands on a surface instead of hanging. */ standing?: boolean; seed?: number; palette?: Palette; } /** * A framed photograph: small, glazed, with a mount board. * * With `standing` its origin moves to the **base** rather than the wall face, * because a photo on a shelf is placed on the floor of that shelf and a photo * on a wall is placed on the wall. Getting that wrong buries it half a frame * into whatever it sits on. */ declare function createFramedPhoto(options?: FramedPhotoOptions): WallArt; interface MirrorOptions { /** Glass width. Default 0.5. */ width?: number; /** Glass height. Default 0.72. */ height?: number; frame?: FrameStyle; frameColor?: number; seed?: number; palette?: Palette; } /** * A wall mirror. * * The glass is painted, not reflective — see `PictureStyle.mirror`. A real * one is a second render pass each, and a metal surface without an * environment map is simply black. */ declare function createMirror(options?: MirrorOptions): WallArt; interface WallClock extends WallArt { /** Advance the hands. */ update(dt: number): void; /** Set the displayed time. Seconds are optional. */ setTime(hours: number, minutes: number, seconds?: number): void; /** The displayed time in hours since midnight. */ readonly time: number; } interface WallClockOptions { /** Face diameter. Default 0.3. */ diameter?: number; /** Starting time, hours since midnight. Default 10.17 (a photogenic 10:10). */ time?: number; /** * How fast the hands run relative to real time. Default 60 — one minute of * clock per second, so a clock is visibly moving in a demo. Set 1 for real * time, 0 to stop it. */ rate?: number; /** Include a sweeping second hand. Default true. */ seconds?: boolean; frameColor?: number; seed?: number; } /** * A wall clock — the only piece here that moves, and worth the geometry for * exactly that reason. A room where nothing at all changes reads as a * photograph; one ticking hand is enough to break that. */ declare function createWallClock(options?: WallClockOptions): WallClock; interface TapestryOptions { /** Cloth width. Default 0.9. */ width?: number; /** Cloth drop. Default 1.4. */ height?: number; /** Hang it from a visible rod. Default true. */ rod?: boolean; seed?: number; palette?: Palette; } /** * A hanging cloth: tapestry, wall rug, banner-on-a-wall. * * The origin is at the wall face level with the **rod**, so the cloth drops * below it — which is how a hanging is actually positioned. Everything else * here is centred on itself; this one is not, and it would be wrong if it * were. */ declare function createTapestry(options?: TapestryOptions): WallArt; /** * Soft furnishing — curtains, cushions, throws. * * Curtains are the highest-value item in the whole decoration set and cost * almost nothing, because the hard part is already built: `clothWave` has * driven the flags, banners and bunting since 0.9. A curtain is that same * material stood on its end — fixed along the top edge, free at the hem — * and a curtain stirring in a draught is one of the very few things that * makes an interior read as *alive* rather than as a photograph of one. * * ```ts * const curtains = createCurtains({ width: 1.2, drop: 1.6, seed: 3 }); * window.add(curtains.object); * game.onUpdate((t) => curtains.update(t.delta)); * ``` */ type CurtainStyle = /** Two panels drawn back to either side. */ 'open' /** Two panels meeting in the middle. */ | 'closed' /** A single sheer panel across the whole opening. */ | 'sheer'; interface CurtainsOptions { /** Width of the opening being dressed, in metres. Default 1.2. */ width?: number; /** Drop from the rail to the hem. Default 1.6. */ drop?: number; style?: CurtainStyle; /** Cloth colour. Defaults to a seeded pick. */ color?: number; /** Show the rail and rings. Default true. */ rail?: boolean; /** How hard the draught blows, 0–1. Default 0.5. */ stir?: number; seed?: number; palette?: Palette; } interface Curtains extends Prop { /** Advance the stir. Nothing moves without this. */ update(dt: number): void; width: number; drop: number; } /** * Curtains at a window. * * The origin is at the **rail**, centred, with everything hanging below — * the same convention as the tapestry, and for the same reason: a hanging * thing is placed by where it hangs from. */ declare function createCurtains(options?: CurtainsOptions): Curtains; interface CushionOptions { /** Edge length in metres. Default 0.4. */ size?: number; color?: number; seed?: number; palette?: Palette; } /** * A cushion. * * A box is a brick. What makes a cushion is that it is **fatter in the * middle than at the corners**, so the geometry is a box with its corner * vertices pulled in and its face centres pushed out. */ declare function createCushion(options?: CushionOptions): Prop; interface ThrowOptions { /** How wide the throw lies. Default 0.9. */ width?: number; /** How far it hangs down the front of whatever it is over. Default 0.35. */ hang?: number; color?: number; seed?: number; palette?: Palette; } /** * A throw or blanket draped over an edge: flat along the top, folding over, * then hanging down the front with the hem uneven. * * The origin is the top surface it lies on, so it drops straight onto the * end of a bed or the arm of a sofa. */ declare function createThrow(options?: ThrowOptions): Prop; /** * Waterworks — water that is doing something. * * The porcelain in a bathroom is trivial: a basin is a lathe and a tub is a * box with a hole in it. What makes any of it read is **water behaving** — * a stream that falls and breaks up, a shower's cone, a level that rises in * a bowl while it fills and settles when it stops. Without these, the whole * bathroom set is dry ceramic, which is exactly what it has been. * * Everything here takes a **flow or a level from outside**, so a tap * (`createValve` is already a `Manipulable` with an eased `state`) or GAMA's * `Automation` can drive it with no library importing another: * * ```ts * const stream = createStream({ height: 0.3 }); * const basin = createFill({ radius: 0.18, depth: 0.1 }); * game.onUpdate((t) => { * stream.setFlow(tap.state); * basin.fillBy(tap.state * t.delta * 0.4); * stream.update(t.delta); * basin.update(t.delta); * }); * ``` */ interface StreamOptions { /** Fall height in metres. Default 0.25. */ height?: number; /** Radius at the lip. Default 0.012 — a tap, not a waterfall. */ radius?: number; /** Flow to start at, 0–1. Default 1. */ flow?: number; /** Add a splash where it lands. Default true. */ splash?: boolean; color?: number; seed?: number; palette?: Palette; } interface Stream extends Prop { /** How hard it is running, 0–1. At 0 nothing is drawn at all. */ setFlow(flow: number): void; readonly flow: number; update(dt: number): void; height: number; } /** * A falling column of water: a tap, a spout, a weir. * * The origin is at the **lip**, with the water falling to `-height`, because * a stream is positioned by where it comes out. * * The column **narrows as it falls**, which is not decoration: falling water * accelerates, and the same volume per second through a faster-moving column * means a thinner one. Straight-sided falling water looks like a pipe. */ declare function createStream(options?: StreamOptions): Stream; interface SprayOptions { /** How far the spray reaches down, in metres. Default 1.6. */ height?: number; /** Radius of the head. Default 0.06. */ radius?: number; /** Radius the cone has opened to at the bottom. Default 0.26. */ spread?: number; flow?: number; color?: number; seed?: number; palette?: Palette; } interface Spray extends Prop { setFlow(flow: number): void; readonly flow: number; update(dt: number): void; height: number; } /** * A shower's cone of water. * * A stream and a spray are the same material on different geometry — one is * a narrowing tube, the other a widening cone that comes apart far sooner, * because a shower head is *designed* to break the water up. Getting that * backwards gives a shower that looks like a poured bucket. */ declare function createSpray(options?: SprayOptions): Spray; interface FillOptions { /** Round surface of this radius. Give this OR width/depth. */ radius?: number; /** Rectangular surface. */ width?: number; length?: number; /** How deep the container is: level 1 sits this far above the origin. */ depth?: number; /** Starting level, 0–1. Default 0. */ level?: number; color?: number; seed?: number; palette?: Palette; } interface Fill extends Prop { /** Where the surface sits, 0 (empty) to 1 (brim). */ readonly level: number; /** Set the level directly. */ setLevel(level: number): void; /** Add (or, negative, drain) this much level. Disturbs the surface. */ fillBy(amount: number): void; /** Splash it — a hand going in, something dropped. */ disturb(amount?: number): void; update(dt: number): void; } /** * The water inside a container — a basin, a tub, a bucket, a pool. * * The level is the whole prop. But the detail that actually sells it is that * **the surface is agitated while it is filling and settles when it stops**: * a still disc of blue is a disc of blue, and a rippling one that goes calm * a few seconds after the tap closes is water. `fillBy` and `disturb` both * stir it; the stir decays on its own. * * The origin is the **bottom** of the container, so `depth` is the height of * the brim above it. */ declare function createFill(options?: FillOptions): Fill; interface SteamOptions { /** Radius of the source. Default 0.3. */ radius?: number; /** How high it rises. Default 1.2. */ height?: number; /** How many puffs. Default 14. */ count?: number; /** Starting density, 0–1. Default 0. */ density?: number; seed?: number; } interface Steam extends Prop { /** How thick it is, 0–1. Builds and clears over `update`. */ readonly density: number; /** Where it is heading. Steam takes time to fill a room and time to clear. */ setTarget(density: number): void; update(dt: number): void; } /** * Steam. * * The only particle system in the water set, so the only piece with a real * frame cost. It is worth it for one reason: steam is the only thing that * shows a shower has been running for a *while*. It **builds and clears * slowly** — a room that fogs the instant the tap opens is a smoke machine. */ declare function createSteam(options?: SteamOptions): Steam; /** * Washing — basins, taps and the vessels you carry water in. * * The era axis here is a **gameplay** axis, not a styling one, and that is * the whole reason this track is worth its own file: * * - `medieval` — no plumbing at all. Water arrives *in a vessel*, is poured * in by hand, and is thrown out. `pour()` is the entire interface, and the * loop is a fetch-and-carry chore. * - `victorian` — a pair of taps over a pedestal, and a plug. Two controls, * because hot and cold arrived separately and mixing was your problem. * - `modern` — one mixer lever and a drain. Water on demand. * * The same three meshes with different textures would be a re-skin. These * differ in *what the player does*, which is why `taps` is empty on one of * them and `pour` does nothing on another. */ type BasinEra = 'medieval' | 'victorian' | 'modern'; declare const BASIN_ERAS: BasinEra[]; /** * A tap. Structurally a `Manipulable`, the same as doors and valves, so * anything that can operate one of those can operate this. */ interface Tap extends Prop { /** Live eased position: 0 shut … 1 wide open. */ readonly state: number; readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; update(dt: number): void; onChange?: (open: boolean) => void; } type TapStyle = /** A crossed capstan handle that turns. Victorian, and always in pairs. */ 'crosshead' /** A single lever that lifts. Modern. */ | 'mixer' /** A small round knurled knob. */ | 'pillar' /** A long pump handle that swings down — the thing before plumbing. */ | 'pump'; interface TapOptions { style?: TapStyle; /** Metal colour. Defaults per style. */ color?: number; /** How fast the handle travels toward its target (1/sec). Default 3.5. */ speed?: number; seed?: number; palette?: Palette; } /** A tap, valve or pump handle you can turn. */ declare function createTap(options?: TapOptions): Tap; interface BasinOptions { era?: BasinEra; /** How fast a fully open tap fills it, in levels per second. Default 0.25. */ rate?: number; /** How fast an open drain empties it. Default 0.4. */ drainRate?: number; seed?: number; palette?: Palette; } interface Basin extends Prop { era: BasinEra; /** The water in the bowl. */ fill: Fill; /** Water coming out, on the eras that have plumbing. */ stream: Stream | null; /** Taps to operate. **Empty on `medieval`** — that is the point of it. */ taps: Tap[]; /** Where a character stands to use it. */ slot: PropSlot; /** Height of the rim above the floor — where the hands go. */ rim: number; /** Tip water in by hand. The medieval loop; works on any era. */ pour(amount: number): void; /** Open or close the plug. Medieval basins have none, and ignore this. */ setDrain(open: boolean): void; readonly draining: boolean; /** Runs the taps, the stream and the level. Nothing happens without it. */ update(dt: number): void; } /** * A wash basin, of its era. * * The whole tap → stream → level loop is wired **inside** the prop, so the * caller only ever operates the taps — `basin.taps[0].toggle()` — and calls * `update`. It still composes outward: the taps are `Manipulable`s, so * GAMA's `Automation` or an interaction system drives them without knowing * what a basin is. */ declare function createBasin(options?: BasinOptions): Basin; interface EwerOptions { seed?: number; palette?: Palette; } /** * A ewer — the jug water arrives in before plumbing does. * * A `Carryable`, so ANIMA's `Carry` picks it up with no adapter: this is the * medieval half of the era axis, and it is a carry loop rather than a switch. */ declare function createEwer(options?: EwerOptions): Carryable; /** * Bathing — showers, tubs and hot tubs. * * Everything expensive here was built already: the spray and the steam came * with the water layer, the enclosure screen is a `createCurtains` panel or * a sheet of `createGlass`, and a hot tub is a `Gathering` — seats around a * rim with a shared focus, which is what one actually is socially. * * What is new is **the wait**. A shower does not produce hot water the * instant you open it, and that pause is most of what makes one feel * plumbed rather than switched. The state machine below is deliberately the * same shape as GAMA's `Device` (off → warming → running), so a device * graph drives a shower with nothing importing anything. */ type ShowerState = 'off' | 'warming' | 'running' | 'cooling'; type ShowerStyle = /** A glass cubicle with a tray. */ 'enclosure' /** A head and a rail over the end of a bath, with a curtain. */ | 'overBath' /** A wet room: a head, a drain, and nothing else. */ | 'open'; interface ShowerOptions { style?: ShowerStyle; /** Tray/footprint width in metres. Default 0.9. */ width?: number; /** Head height above the floor. Default 2.05. */ head?: number; /** * Seconds of cold before it runs warm. Default 3.5. Zero makes it a * switch, which is exactly what a shower is not. */ warmUp?: number; seed?: number; palette?: Palette; } interface Shower extends Prop { readonly state: ShowerState; /** Turn it on or off. The warm-up happens on its own. */ setRunning(on: boolean): void; readonly running: boolean; spray: Spray; steam: Steam; /** The curtain, on the styles that have one. */ curtain: Curtains | null; /** Where a character stands under it. */ slot: PropSlot; /** Fires as the state changes — wire it to a sound or a light. */ onState?: (state: ShowerState) => void; update(dt: number): void; } /** * A shower. * * The origin is on the floor at the centre of the tray, facing +z out of the * enclosure. */ declare function createShower(options?: ShowerOptions): Shower; type TubStyle = /** A roll-top on four feet. */ 'clawfoot' /** A modern panelled bath built into the wall. */ | 'modern' /** A tub set into the floor. */ | 'sunken' /** A short high-sided hip bath — the one you fill by hand. */ | 'hip'; interface TubOptions { style?: TubStyle; /** Length in metres. Defaults per style. */ length?: number; /** How fast an open tap fills it. Default 0.12 — a bath takes a while. */ rate?: number; seed?: number; palette?: Palette; } interface Tub extends Prop { style: TubStyle; fill: Fill; /** Empty on `hip` — you fill that one from a jug. */ taps: Tap[]; /** Lie in it. The reclined sleep pose. */ slot: PropSlot; /** Rim height above the floor. */ rim: number; pour(amount: number): void; setDrain(open: boolean): void; readonly draining: boolean; update(dt: number): void; } /** A bath. */ declare function createTub(options?: TubOptions): Tub; interface JacuzziOptions { /** How many people it seats. Default 4. */ seats?: number; /** Radius in metres. Default 1.1. */ radius?: number; seed?: number; palette?: Palette; } interface Jacuzzi extends Gathering { fill: Fill; steam: Steam; /** How hard the jets are running, 0–1. */ readonly jets: number; setJets(power: number): void; update(dt: number): void; } /** * A hot tub. * * A `Gathering` — seats around a rim with a shared focus — because that is * what one is socially, and it means GAMA's `Occupancy` fills it and ANIMA's * `Conversation` runs in it with nothing new written. * * The jets are the whole prop: they **agitate the surface** continuously * while they run, so the water is visibly churning rather than being a still * blue disc with bubbles drawn over it. */ declare function createJacuzzi(options?: JacuzziOptions): Jacuzzi; /** * Swimming pools. * * The prop is the easy half. A pool is a hole with walls, and the trilogy * has built holes before — the mistakes are already catalogued: build the * shell as **walls around a floor**, never as a solid with a smaller solid * inside it; frame the coping, never slab it; and remember a default * `CylinderGeometry` has a lid on it. * * What a pool has that a tub does not is a **floor that slopes**, and that * one difference is what makes it a gameplay prop rather than a big bath. * `depthAt` is the whole handshake: ANIMA asks how deep the water is where * a body is standing, and decides for itself whether that body wades or * swims. A pool with one depth everywhere cannot pose the question, which * is why the shallow and deep ends are not decoration. * * ```ts * const pool = createPool({ style: 'lido' }); * scene.add(pool.object); * game.onUpdate((t) => pool.update(t.delta)); * pool.depthAt(swimmer.x, swimmer.z); // ANIMA's Swimming reads this * ``` */ type PoolStyle = /** A small stone plunge bath: steep sides, one depth, steps all round. */ 'plunge' /** A mosaic-lined bathing hall pool, shallow and wide. */ | 'bathhouse' /** A mid-century tiled lido: lanes, a sloping floor, a board at the deep end. */ | 'lido' /** A modern deck-level pool with a spill edge. */ | 'infinity'; interface PoolOptions { style?: PoolStyle; /** Long axis (x), metres. Defaults per style. */ length?: number; /** Short axis (z), metres. Defaults per style. */ width?: number; /** Water depth at the -x end. Defaults per style. */ shallow?: number; /** Water depth at the +x end. Defaults per style. */ deep?: number; /** Lane markings on the floor. Defaults per style; 0 turns them off. */ lanes?: number; /** * Paved apron around the coping, metres. Defaults per style; 0 for none. * * A pool is a **hole**, and a hole cannot be dropped onto a solid ground * plane — the ground is a lid over it, and every pool in the first render * of this prop was an empty frame lying on the tarmac for exactly that * reason. The apron is the prop bringing its own surround, so it reads * correctly the moment it is added to a scene; a caller with real ground * still has to leave a hole for it. */ deck?: number; seed?: number; palette?: Palette; } /** * Anything with a bottom, a top and rungs — structurally ANIMA's * `Climbable`, the same contract `createLadder` publishes. */ interface PoolLadder { bottom: Object3D; top: Object3D; rungSpacing: number; } interface Pool extends Prop, WaterBody { style: PoolStyle; /** Water depth at each end, in metres. */ shallow: number; deep: number; length: number; width: number; /** Standing on the deck at the top of the steps, facing the water. */ entry: PropSlot; /** A pool ladder at the deep end, on the styles that have one. */ ladder: PoolLadder | null; /** The end of the springboard, on the styles that have one. */ board: PropSlot | null; /** Sitting on the edge with your legs in — the most-used pool pose there is. */ edges: PropSlot[]; update(dt: number): void; } /** A pool. The origin is at the centre, at **deck** level. */ declare function createPool(options?: PoolOptions): Pool; /** * Heat: the hearth-to-induction axis. * * The era is not a texture here, it is **what you have to do to cook**, and * it is the same discovery the basins turned on. A medieval fire has no * dial: you control it by feeding it and by moving the pot nearer or further * away, so heat has to be a **field in space** rather than a number on a * device. A gas ring has a knob and heat exists only in a 9 cm circle. Those * are different games, not different materials. * * So the handshake is a spatial query, and it is deliberately the same shape * as the pool's: * * ```ts * heatAt(x, z): number // 0 (cold) .. 1 (full), 0 anywhere out of reach * ``` * * mirroring `WaterBody.depthAt`. SCENA answers where the heat is; GAMA and * whatever is in the pot decide what that means. * * The state machine — cold → heating → hot → cooling — is the same shape as * the shower's and as GAMA's `Device`, for the third time, because it keeps * being the right shape. * * ```ts * const stove = createHeatSource({ era: 'gas' }); * stove.setPower(1); * game.onUpdate((t) => stove.update(t.delta)); * stove.heatAt(pot.x, pot.z); * ``` */ type HeatEra = /** An open fire on a stone hearth, with a swinging crane to hang a pot. */ 'hearth' /** A cast-iron range: enclosed firebox, a damper, graded hotplates, an oven. */ | 'range' /** A gas hob: four rings, four knobs, a visible flame, instant. */ | 'gas' /** Induction: no flame at all, and heat that outlives the switch. */ | 'induction'; type HeatState = 'cold' | 'heating' | 'hot' | 'cooling'; /** * Where the heat is, in **world** coordinates. * * The cooking mirror of `WaterBody.depthAt`, and for the same reason: a * fire's heat is a place, not a property of a device, and anything that * wants to know whether it is cooking should be able to ask about a point. */ interface HeatField { /** 0 (cold) to 1 (full heat) at a world point. 0 anywhere out of reach. */ heatAt(x: number, z: number): number; } /** * Somewhere a pot goes. * * A zone is a **place**, not a source. Its `heat` is sampled from the field * at wherever the zone currently is, which is what lets the hearth's crane * work at all: swing the hook away from the fire and it cools, with nothing * about it special-cased. Conflate the two — let the hook carry its own heat * around with it — and a pot swung out over the flagstones is still boiling. */ interface HeatZone { /** Free label: 'hook', 'plate', 'ring'. */ kind: string; /** Sits at the surface a pan rests on (or the hook a pot hangs from). */ anchor: Object3D; /** How far the zone's heat reaches, in metres. */ radius: number; /** This zone's own heat right now, 0–1. */ readonly heat: number; /** Demand for this zone, 0–1. Ignored where the era has no separate controls. */ setPower(level: number): void; readonly power: number; } /** A control you can operate — structurally a `Manipulable`, like a tap. */ interface HeatControl { readonly state: number; readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; update(dt: number): void; onChange?: (open: boolean) => void; object: Object3D; } interface HeatSource extends Prop, HeatField { era: HeatEra; readonly state: HeatState; /** The hottest reading anywhere on it, 0–1. */ readonly temperature: number; /** Demand, 0–1. On a fire this is how hard it is burning, not a setting. */ readonly power: number; /** Set the demand. With a zone index, on the eras whose rings are separate. */ setPower(level: number | boolean, zone?: number): void; zones: HeatZone[]; /** * Does it burn fuel? On `hearth` and `range` this is the whole loop: no * fuel, no fire, and `feed` is how it gets there. */ readonly burnsFuel: boolean; /** Fuel left, 0–1. Always 1 on the eras that are plumbed or wired. */ readonly fuel: number; /** Put another log on. A no-op where there is nothing to burn. */ feed(amount?: number): void; /** The knob, damper or crane. Null on an open hearth with neither. */ control: HeatControl | null; /** Swing the pot off the fire — the medieval heat control. Hearth only. */ crane: HeatControl | null; /** The oven cavity door, on the eras that have an oven. */ ovenDoor: HeatControl | null; /** Where a cook stands. */ slot: PropSlot; onState?: (state: HeatState) => void; update(dt: number): void; } interface HeatOptions { era?: HeatEra; /** Rings/plates/hooks. Defaults per era. */ zones?: number; /** Start with the fuel bunker full. Default true. */ fuelled?: boolean; seed?: number; palette?: Palette; } /** * A stove, hob, range or hearth. * * The origin is on the floor at the centre of the front face, facing +z out * into the room. */ declare function createHeatSource(options?: HeatOptions): HeatSource; /** An open cooking hearth: a fire on a stone, with a crane to hang a pot. */ declare function createHearth(options?: Omit): HeatSource; /** A cast-iron range: one firebox, one damper, graded plates and an oven. */ declare function createRange(options?: Omit): HeatSource; /** A modern hob and oven — `gas` burns visibly, `induction` does not burn. */ declare function createHob(options?: Omit & { era?: 'gas' | 'induction'; }): HeatSource; /** * Cookware, and what is in it. * * The heat track made a field; this is the thing that reads it. A pan is a * container with **contents that change**, and the change is the whole prop: * raw → cooking → done → burnt, driven entirely by how hot it is where the * pan is standing. * * Two things make it behave rather than tick: * * - **The pan has its own temperature**, and it lags. Food does not start * cooking the instant a ring is lit, and a cauldron takes far longer to * come up than a frying pan, because there is a great deal more iron in * it. Drive `progress` straight off `heatAt` and everything cooks the * moment it is put down, which is a timer, not a stove. * - **Water boils away.** A pot left on goes dry, and a dry pot burns. That * one rule is what turns "wait for the bar to fill" into something you * have to watch. * * ```ts * const pot = createCookware({ kind: 'pot' }); * pot.add(0.8, { cookFor: 40 }); * game.onUpdate((t) => pot.update(t.delta, stove)); // reads heatAt itself * ``` */ type CookwareKind = /** A deep lidded pot with two loop handles. */ 'pot' /** A shallow frying pan with one long handle. */ | 'pan' /** A kettle: spout, swing handle, and it whistles. */ | 'kettle' /** A big bellied cauldron on three feet, for hanging over a fire. */ | 'cauldron' /** An oven tray. */ | 'tray'; type CookState = 'raw' | 'cooking' | 'done' | 'burnt'; interface CookwareOptions { kind?: CookwareKind; /** Seconds at a good heat to go from raw to done. Default 30. */ cookFor?: number; /** Starting contents, 0–1. Default 0 (empty). */ level?: number; seed?: number; palette?: Palette; } interface Cookware extends Carryable { kind: CookwareKind; /** How hot the VESSEL is, 0–1. Lags the heat under it by its own mass. */ readonly temperature: number; readonly state: CookState; /** 0 raw … 1 done, and on past 1 toward burnt. */ readonly progress: number; /** How full it is, 0–1. Falls while it boils. */ readonly level: number; readonly boiling: boolean; /** A kettle at the boil, until somebody takes it off. */ readonly whistling: boolean; fill: Fill; steam: Steam; /** The lid, on the kinds that have one. Closed cooks faster. */ lid: HeatControl | null; /** Put food or water in. `cookFor` overrides how long this batch takes. */ add(amount?: number, options?: { cookFor?: number; }): void; /** Tip it out and start again. */ empty(): void; onState?: (state: CookState) => void; /** Fires once each time a kettle comes to the boil. */ onWhistle?: () => void; /** * Advance it. Pass the stove — it reads `heatAt` at its own world * position, so moving the pan is all it takes — or a number if the caller * has already sampled the field. */ update(dt: number, heat?: HeatField | number): void; } /** A pot, pan, kettle, cauldron or tray, with contents that cook. */ declare function createCookware(options?: CookwareOptions): Cookware; declare const COOKWARE_KINDS: CookwareKind[]; /** * Preparation: the two-handed half of a kitchen. * * Every work loop the trilogy has so far is **one-handed or symmetric** — an * axe, a pick, a saw, a spoon. Preparing food is neither. One hand does the * work and the other **steadies it and gets out of the way**, and that * asymmetry is the entire read: a cook chopping an onion with two identical * hands is a cook hammering an onion. * * So a prep station publishes two anchors rather than one: * * ```ts * station.work // where the knife, pestle or handle is * station.guide // where the other hand holds the thing steady * ``` * * ANIMA's `Prepping` poses to that pair. It is the same division of labour * as `heatAt` and `depthAt`: SCENA says where things are, ANIMA decides what * a body does about it. * * ```ts * const board = createPrepStation({ kind: 'board' }); * board.onYield = (n) => console.log('chopped', n); * game.onUpdate((t) => board.update(t.delta, cook.working)); * ``` */ type PrepKind = /** A chopping board with a knife and something to cut. */ 'board' /** A mortar and pestle: one hand braces the bowl, the other grinds. */ | 'mortar' /** A hand quern — two stones and a crank. Turning it must be visible. */ | 'quern' /** A dough trough: both hands push, out of phase. */ | 'trough' /** A mixing bowl held at an angle while the other hand whisks. */ | 'bowl'; /** * A station a cook works at with **both hands doing different things**. * * `work` and `guide` are the pair. Everything else is the `WorkStation` * contract from the rhythmic-work track, so these drop into the same * machinery, the same slots and the same yield loop. */ interface PrepStation extends WorkStation { kind: PrepKind; /** Where the working hand is — knife, pestle, crank handle. */ work: Object3D; /** Where the steadying hand is — on the food, the rim, the bowl. */ guide: Object3D; /** How much is left to prepare, 1 → 0. Refill with `load`. */ readonly remaining: number; /** Put more on the board. */ load(amount?: number): void; } interface PrepOptions { kind?: PrepKind; /** How many cycles a full load takes. Default 8. */ batch?: number; seed?: number; palette?: Palette; } /** A bench, board, mortar, quern or trough — somewhere to prepare food. */ declare function createPrepStation(options?: PrepOptions): PrepStation; declare const PREP_KINDS: PrepKind[]; /** * Sail — and why you cannot go where you are pointing. * * `createWindField` has been in the library since the flora track, and its * `sample(x, z)` has never once been read by anything that moves. This is * the thing that reads it, and it produces the deepest movement constraint * in the whole trilogy from one function. * * A sail's drive is not a throttle. It is a **curve against the angle to * the wind**, and the interesting part of that curve is where it goes to * zero: * * ```ts * driveAt(angleOffWind): number // 0 inside the no-go, peak on a reach * ``` * * Inside roughly forty-five degrees of the wind — more for a square rig, * much more — a sailing vessel makes **no ground at all**. She stops, the * sails flog, and she is in irons. So the shortest path from here to there * stops being a straight line: to go upwind you sail across it, twice, and * every steering system in GAMA has only ever known how to point at a * target and drive. * * ```ts * const rig = createSailRig({ kind: 'lateen' }); * ship.object.add(rig.object); * rig.setWind(wind); * game.onUpdate((t) => { * rig.update(t.delta); * ship.update(t.delta, { speed: rig.drive * 9, turn: helm }); * }); * rig.layline(bearingToPort); // …and which way to point to get there * ``` */ type RigKind = /** A square sail: magnificent downwind, hopeless anywhere near the wind. */ 'square' /** A lateen yard — the Mediterranean answer, and it points far better. */ | 'lateen' /** Gaff: four-sided fore-and-aft, the working rig of the age of steam. */ | 'gaff' /** Bermudan: the modern triangle, and the closest-winded of the four. */ | 'bermudan'; /** Anything that can tell you the wind. Structurally SCENA's `WindField`. */ interface WindSource { sample(x: number, z: number, time?: number): Vector2; } interface SailRig extends Prop { kind: RigKind; /** * Radians off the wind inside which she will not sail — the **no-go**. * * Published rather than inferred, because a helmsman needs it to plan * and an AI needs it to avoid steering into a stall it cannot recover * from by pointing harder. */ readonly noGo: number; /** How much canvas is set, 0 (furled) to 1 (everything). */ readonly set: number; /** Set or shorten sail. */ setSail(amount: number): void; /** Shorten by this much — `reef(0.3)` takes a third of it in. */ reef(amount?: number): void; /** Angle of the apparent wind off the bow, 0 (dead ahead) to π (astern). */ readonly windAngle: number; /** Drive along the hull's heading, 0–1. Multiply by your hull speed. */ readonly drive: number; /** * Sideways force, 0–1 — what heels her over. * * **Not a fixed fraction of `drive`.** The rig's force is roughly square * to the canvas and the canvas is trimmed at about half the angle to the * wind, so drive is that force's forward component and this is its * sideways one: the RATIO between them is `cot(windAngle / 2)` times how * tender she is. Dead downwind that is zero and she does not heel at all * however hard she is driving; hard on the wind a tender rig is over one * and she lies down further than she goes. * * Where the force itself peaks falls out of the two curves together, and * it is not where intuition puts it: on a **close reach**, not * close-hauled — and further aft the older the rig, because a square sail * makes nothing at all up near the wind to be pressed by. */ readonly heelForce: number; /** Are the sails flogging? True in irons, and while sheets are let fly. */ readonly luffing: boolean; /** Bind the wind. */ setWind(wind: WindSource | null): void; /** * The polar curve itself, for anybody planning a course. * * `angleOffWind` in radians, 0 = straight into it. Returns 0 inside the * no-go, and peaks on a reach for everything except a square rig — whose * best point of sailing is dead astern. */ driveAt(angleOffWind: number): number; /** * What heading to steer to make ground toward `bearing`. * * **The function the track exists for.** If the bearing is sailable it * hands it straight back. If it is inside the no-go it returns the closer * of the two close-hauled headings instead — which is to say it tells you * to tack, and the straight line was never available. The course it gives * back is always one she will actually sail: laid a couple of degrees * outside the no-go, never on the boundary itself. */ layline(bearing: number, currentHeading?: number): number; update(dt: number): void; } interface SailOptions { kind?: RigKind; /** Overall scale of the rig. Default 1. */ scale?: number; /** Start with sail set. Default 1. */ set?: number; seed?: number; palette?: Palette; } /** * A mast and its canvas. * * Parent it to a hull: it reads its own world heading and position, so it * needs telling nothing about the ship it is on. */ declare function createSailRig(options?: SailOptions): SailRig; declare const RIG_KINDS: RigKind[]; /** Degrees, for the tables and the tests, because radians read badly there. */ declare const noGoDegrees: (kind: RigKind) => number; /** * Alongside — mooring lines, fenders and the gangway. * * The decked-ship track made a vessel a **frame**: `ride` carries whatever * is standing on her, and a sailor who never takes a step still travels at * six knots. This is what happens when that frame meets one that does not * move, and there are exactly two ideas in it. * * **A rope is a one-way constraint.** It can pull and it can never push. A * fender is the same thing backwards: it pushes and can never pull. Neither * one alone holds a ship — a line by itself lets her grind along the wall, a * fender by itself lets her drift away — and neither is a spring, because a * spring would haul her *back* when she came in and shove her *out* when she * went away, which is not what either object does. She is held in the gap * between two constraints that each only act in one direction, and the whole * reason a ship alongside is never quite still is that inside that gap * nothing is acting on her at all. * * ```ts * const berth = createBerth({ era: 'harbour' }); * const lines = moor(ship, berth); * game.onUpdate((t) => ship.update(t.delta, lines.hold(t.delta))); * ``` * * **A gangway is where two frames blend.** It is walkable ground with a * `DeckField` on it exactly like a deck — but somebody halfway up it is * carried half as much by the ship as somebody standing on her deck, and * not at all at the shore end. Get that wrong in either direction and they * are either dragged off the quay or left behind by the ship. * * ```ts * const brow = createGangway({ berth, ship }); * legs.update(t.delta, aboard ? ship : onBrow ? brow : berth); * ``` * * All three — quay, gangway, deck — publish the same three functions, and * only one of them moves. Fixed ground is a moving frame whose delta is the * identity, which is why walking ashore needs no special case anywhere. */ /** Anything that can carry what is standing on it — SCENA's `DeckField`. */ interface Carrier { deckAt(x: number, z: number, near?: number): number | null; normalAt(x: number, z: number): Vector3; ride(position: Vector3): Vector3; } type BerthEra = /** Timber piles and a plank deck — a river wharf. */ 'wharf' /** Dressed stone with iron rings — a harbour wall. */ | 'harbour' /** Concrete, steel bollards and rubber fenders — a container quay. */ | 'quay'; declare const BERTH_ERAS: BerthEra[]; /** Something to make fast to. */ interface Bollard { anchor: Object3D; kind: 'ring' | 'bollard' | 'bitt'; /** Distance along the quay from its centre, metres. */ along: number; } interface Berth extends Prop, Carrier { era: BerthEra; /** Along the quay face, metres. */ length: number; /** Coping height above the water. */ height: number; bollards: Bollard[]; /** Fenders hung on the face — where the hull is allowed to touch. */ fenders: Object3D[]; /** * How far clear of the quay face a world point is. * * Positive is out in the harbour, negative is inside the wall. Everything * a fender does is a reaction to this going negative. */ clearance(x: number, z: number): number; /** Outward normal of the face, in world space. */ faceNormal(out?: Vector3): Vector3; /** Where a gangway would land on the shore side. */ brow: PropSlot; slots: PropSlot[]; } interface BerthOptions { era?: BerthEra; /** Along the face. Default 34. */ length?: number; /** Coping above the water. Default per era. */ height?: number; /** How many bollards. Default 4. */ bollards?: number; seed?: number; palette?: Palette; } /** * A wall to lie alongside. * * The face is the quay's local **+x** plane at `x = 0`, running along z, and * the harbour is out toward +x. Everything about clearance is measured from * that plane in world space, so the berth can be placed and turned like any * other prop. */ declare function createBerth(options?: BerthOptions): Berth; interface MooringLine { /** Where it leaves the ship. */ from: Object3D; /** What it is made fast to. */ to: Object3D; /** * How much line is out, metres. * * Shorter than the gap and she is hauled in; longer and it does nothing * whatever. There is no setting at which it pushes. */ scope: number; /** 0 while there is slack, rising as it comes bar-taut. */ readonly tension: number; readonly taut: boolean; /** Pay out or heave in to a given scope. */ set(scope: number): void; /** Take in this much — `heave(0.5)` shortens by half a metre. */ heave(by?: number): void; /** Let it go. */ cast(): void; /** Take a turn again. */ makeFast(): void; readonly fast: boolean; } interface Mooring { /** The ropes themselves. Parent this to your scene, not to the ship. */ object: Object3D; lines: MooringLine[]; /** Alongside and held — every fast line taut or nearly so, and touching. */ readonly alongside: boolean; /** * How much she is working, 0 (dead still) to 1 (ranging about). * * This is the number the gangway and the crew care about, and it is a * SPEED, not a distance — a ship two metres off the wall and steady is a * fine place to work, and one an inch off it and surging is not. */ readonly surge: number; /** Distance from the hull's inboard side to the fenders, metres. */ readonly gap: number; /** Make another line fast. */ add(from: Object3D, to: Object3D, scope?: number): MooringLine; /** Let go — everything, or one line. */ cast(line?: MooringLine): void; /** * Work out what the lines are doing to her, and give it back as helm. * * Returns a `ShipInput` to hand straight to `ship.update` — merged with * anything you were already asking for. It goes through `update` rather * than writing her position afterwards because `ride` depends on the * frame delta covering every bit of a frame's movement. */ hold(dt: number, input?: { speed?: number; turn?: number; }): { speed?: number; turn?: number; drift: { x: number; z: number; }; }; /** Redraw the ropes. `hold` does this too; this is for when you don't. */ update(dt: number): void; } /** The vessel a mooring can hold: structurally a `DeckedShip`. */ interface Moorable { object: Object3D; length: number; beam: number; /** * Height of her rail above the waterline, if she knows it. * * Lines are led from the DECK, over the bulwark, and down to the bollard. * Led from the waterline instead they run up the inside of the gap where * nothing can see them, and a ship apparently moored by nothing at all is * the sort of thing no test notices. */ freeboard?: number; } interface MooringOptions { /** * Where she wants to lie: distance from the fenders, metres. * * Not zero. A ship resting hard against her fenders all watch is a ship * with no lines on her — she is pinned there by whatever is pushing her, * and if nothing is, she lies off. */ standoff?: number; /** How many lines, if none are given. Default 4 — head, stern and springs. */ lines?: number; /** How hard the lines pull, per metre of stretch. Default 0.9. */ stiffness?: number; /** How much the water damps her. Default 1.4. */ damping?: number; seed?: number; palette?: Palette; } /** * Make a vessel fast to a berth. * * Lines are run from fairleads at her bow and stern to the nearest bollards, * plus springs crossed the other way — which is what actually stops a ship * ranging fore and aft, and the reason four lines is the smallest number * that holds anything. */ declare function moor(ship: Moorable, berth: Berth, options?: MooringOptions): Mooring; interface Gangway extends Prop, Carrier { /** Slope, radians. Positive is uphill from the shore. */ readonly angle: number; /** Span between its two ends, metres. */ readonly span: number; /** Down and usable. False when raised, or when she has ranged too far. */ readonly rigged: boolean; /** Put it over, or take it in. */ lower(): void; raise(): void; /** Follow the ship. Call it after hers. */ update(dt: number): void; } interface GangwayOptions { /** The shore end. Usually `berth.brow.anchor`. */ shore: Object3D; /** The moving end, and the frame it belongs to. */ ship: Carrier & { object: Object3D; }; /** Where it lands aboard. Defaults to a point on her inboard side. */ landing?: Object3D; /** Widest span it will still bridge, metres. Default 1.6× its own length. */ reach?: number; width?: number; seed?: number; palette?: Palette; } /** * A plank between two frames. * * The shore end is fixed and the ship end is not, so it re-solves every * frame — but the part that matters is `ride`. A gangway carries somebody * standing on it in **proportion to how far along it they are**: not at all * at the shore end, entirely at the ship end. Carry them all the way and * they get dragged off the quay; carry them not at all and the ship leaves * without them halfway across. */ declare function createGangway(options: GangwayOptions): Gangway; /** * Oars — and why a rowed boat does not travel at a steady speed. * * A sail is a curve against the angle to the wind. An engine is a throttle. * An oar is neither: it is a **duty cycle**. The blade is in the water for * something under half of every stroke and out of it for the rest, so the * thrust is a pulse and everything downstream of it inherits that. * * ```ts * bank.thrust // ZERO for most of every stroke * bank.way // …so her speed SURGES: drive, coast, drive, coast * ``` * * That is the first idea and it is not a detail — a galley under oars * lurches, and the lurch is at the stroke rate, and you can feel the rate * from the deck without seeing an oar. Publish thrust as an average and the * whole thing becomes an engine with a wooden skin on it. * * The second idea is that it takes **several bodies agreeing**. Nothing * else in the trilogy needs that. Every rower drives off one shared number: * * ```ts * bank.phaseAt(seat) // 0 at the catch, ~0.4 at the finish, 1 back again * ``` * * and ANIMA's rowing controller takes that same number and writes a body * with it. Neither library imports the other; the handshake is a **scalar** * — a shared clock rather than a shared field or a shared frame, which is a * third kind and the only one that can say "together". * * And they are not quite together, ever. A rower does not watch the * coxswain, he watches the blade in front of him, so the stroke propagates * down the boat with a delay and `phaseAt` is a different number per seat. * Close it up and she runs; let it spread and the blades go in at different * moments, the thrusts no longer add, and she slows down — which falls out * of averaging the oars rather than being a penalty anybody wrote. * * ```ts * const bank = createOarBank({ kind: 'longship', seats: 8, beam: 4 }); * ship.object.add(bank.object); * game.onUpdate((t) => { * bank.update(t.delta); * ship.update(t.delta, { speed: bank.way, turn: bank.yaw * 0.35 }); * }); * ``` */ type OarKind = /** A pair of oars in a small boat — one person, both hands. */ 'skiff' /** A longship's benches: heavy oars, slow deep strokes. */ | 'longship' /** A war galley — many oars, driven hard, and a rate you can hear. */ | 'galley' /** A racing eight: light, long, and rowed at forty to the minute. */ | 'racing'; declare const OAR_KINDS: OarKind[]; /** One oar in the bank. */ interface Oar { seat: number; /** −1 port, +1 starboard. */ side: -1 | 1; /** The whole oar, pivoting at its rowlock. */ object: Object3D; /** * The handle, in world space, wherever it is this instant. * * ANIMA's props conform to the pose rather than the pose reaching for the * prop, so this is published for anything that wants to know rather than * driven at — but it is the honest answer to "where are his hands". */ grip: Object3D; /** Somewhere to sit. */ seatSlot: PropSlot; /** This oar's own phase, which is not the bank's. */ readonly phase: number; /** Is the blade in the water right now? */ readonly buried: boolean; /** What this one is contributing, −1 to 1. */ readonly thrust: number; /** Fouled: the blade did not come clear and she is dragging it. */ readonly crabbing: boolean; } interface OarBank extends Prop { kind: OarKind; oars: Oar[]; seats: PropSlot[]; slots: PropSlot[]; /** The stroke's own phase: 0 at the catch, ~0.4 at the finish. */ readonly phase: number; /** Strokes per minute. */ readonly rate: number; setRate(spm: number): void; /** * How hard they are pulling, −1 (backing water) to 1. * * Give it two numbers to pull harder on one side than the other, which is * how a boat with no rudder turns and how one with a rudder turns quickly. */ setEffort(port: number, starboard?: number): void; /** * How together they are, 0 (a shambles) to 1 (as one blade). * * Not a multiplier on the output. It sets how far the stroke smears down * the boat, and the loss of thrust comes out of the oars disagreeing. */ together: number; /** * Thrust this instant, −1 to 1, and **zero through every recovery**. * * The mean over the bank, so a ragged crew makes less of it without * anybody applying a penalty. */ readonly thrust: number; /** * Her speed through the water, m/s. * * Integrated from `thrust` against drag, which is the only place the * surge can live: hand a hull the instantaneous thrust and she jerks to a * stop twice a second. */ readonly way: number; /** Turning effect from one side out-pulling the other, −1 to 1. */ readonly yaw: number; /** Fraction of the bank currently fouled. */ readonly crabbing: number; /** Where a given seat is in the stroke. */ phaseAt(seat: number): number; /** Catch a crab: the blade fails to come clear and she drags it. */ crab(seat: number): void; /** Ship oars — everybody stops, blades in. */ ship(): void; /** Out oars again. */ out(): void; readonly rowing: boolean; update(dt: number): void; } interface OarBankOptions { kind?: OarKind; /** Benches a side. Default per kind. */ seats?: number; /** Beam of the hull it is bolted to — where the rowlocks go. */ beam?: number; /** Height of the gunwale above the vessel's origin. */ gunwale?: number; /** Rowers only down one side, for a sculling boat. Default both. */ sides?: 1 | 2; together?: number; seed?: number; palette?: Palette; } /** * Where the handle of an oar sits relative to the thwart a rower is on. * * Published in the same spirit as ANIMA's `GRIPS`, and identical to that * library's `ROW_GRIP`: the prop is built to the body's expectations rather * than the body reaching for the prop, so an oar and a rowing pose meet * without any runtime IK between them. * * `height` is what puts the thwart below the gunwale. An oar pivots at the * rowlock, so cocking the blade down to reach the water swings the inboard * end UP by half a metre — and if the handle is assumed to be low, the seat * derived from it ends up above the sheerstrake with the rower perched on * the rail. */ declare const OAR_GRIP: { /** Height of the handle above the thwart at the catch. */ readonly height: 0.4; /** How far in front of the chest the hands go at the catch. */ readonly reach: 0.58; /** …and how far past the body they come at the finish. */ readonly finish: -0.16; }; /** * A bank of oars. * * Parent it to a hull. The origin is the hull's origin, +z forward, so the * rowlocks land on the gunwale and the blades reach out over the water. */ declare function createOarBank(options?: OarBankOptions): OarBank; /** Where the handle should be, for a given phase, in the rower's own frame. */ declare function oarGripAt(phase: number, out?: Vector3): Vector3; /** * Smoke, and getting rid of it. * * This track exists because of one fact about the steam in `waterworks`: * it is drawn with **additive blending**, and its fragment shader writes * white. Additive can only ever *add* light. It is not that the steam is * the wrong colour for smoke — it is that no choice of colour or opacity in * an additive pass can produce something that makes the wall behind it * darker, and a plume that brightens what it covers is steam whatever you * call it. Smoke needs its own material, and that is the whole reason this * is a file rather than a parameter. * * The second thing smoke does that nothing else in the library does is * **stratify**. Heat is a field over a surface, cold is a field inside a * box, water is a depth — smoke is a **layer that fills a room from the * ceiling down**, so the reading depends on how high up you ask: * * ```ts * smokeAt(x, y, z): number // 0–1, and y is the interesting argument * ``` * * Thick at the ceiling long before it is anything at head height, which is * why extractors are mounted high, why you crawl, and why an alarm on the * ceiling goes off before anybody in the room notices. * * ```ts * const room = createSmokeLayer({ width: 5, depth: 4, height: 2.6 }); * room.add(createSmoke({ style: 'grease' })); * room.vent(createExtractor({ era: 'hood' })); * game.onUpdate((t) => room.update(t.delta)); * room.smokeAt(cook.x, 1.6, cook.z); // can he still see? * ``` */ type SmokeStyle = /** Pale wood smoke, from a hearth. */ 'wood' /** Grey soot — a stove that needs its damper opening. */ | 'soot' /** Near-black grease smoke. A pan that has caught. */ | 'grease' /** Thin blue haze — something scorching, not yet burning. */ | 'scorch'; /** * How thick the smoke is at a point, in **world** coordinates. * * The fourth spatial handshake, after `depthAt`, `heatAt` and `chillAt`, and * the first one where **y is the interesting argument**. The others are * about where you are standing; this one is about how tall you are. */ interface SmokeField { /** 0 (clear) to 1 (solid) at a world point. 0 anywhere outside the room. */ smokeAt(x: number, y: number, z: number): number; } interface SmokeOptions { style?: SmokeStyle; /** How high the plume climbs before it joins the layer. Default 1.4. */ height?: number; /** Radius at the base. Default 0.16. */ radius?: number; /** Puffs. Default 18. */ count?: number; /** m³ of smoke a second at full rate. Default per style. */ output?: number; seed?: number; } interface SmokeSource extends Prop { style: SmokeStyle; /** How hard it is smoking, 0–1. Eases toward the target. */ readonly rate: number; setRate(rate: number): void; /** What it is putting into the room right now, m³/s. */ readonly output: number; update(dt: number): void; } /** A plume. */ declare function createSmoke(options?: SmokeOptions): SmokeSource; type ExtractorEra = /** A hole in the roof with a louvre over it. Only helps what is under it. */ 'hole' /** A masonry flue. Its draw depends on how hot the fire below it is. */ | 'chimney' /** A canopy hood with a fan, over a hob. Its filter clogs. */ | 'hood' /** A slot that rises out of the worktop and pulls sideways. */ | 'downdraft'; /** The fan — structurally a `Manipulable`, like every switch in the library. */ interface ExtractorFan { readonly state: number; readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; update(dt: number): void; onChange?: (open: boolean) => void; object: Object3D; } interface Extractor extends Prop { era: ExtractorEra; /** * The opening — where it actually catches, and what you stand the pan * under. * * Published for the same reason the stove publishes `zones` and the prep * bench publishes `work`: the prop's origin is on its front face, and a * caller measuring from there is measuring from the one point beneath a * canopy that a fire never is. Nobody should have to guess. */ mouth: Object3D; /** * How much of a plume at a world point it intercepts **before the smoke * ever reaches the room**, 0–1. * * The half of extraction that matters. A hood over the hob catches the pan * that has caught fire; the same hood does nothing at all about a pan on * the other side of the kitchen, however hard the fan runs. */ catches(x: number, z: number): number; /** What it clears from the room's standing layer, m³/s, right now. */ readonly draw: number; /** Demand, 0–1. Fixed at 1 on the eras with no controls. */ readonly power: number; setPower(level: number | boolean): void; fan: ExtractorFan | null; /** Grease in the filter, 0 (clean) to 1 (blocked). It chokes the draw. */ readonly clogged: number; clean(): void; slot: PropSlot; /** * Advance it. Pass the heat below it: a **cold flue does not draw**, which * is why a fire smokes into the room when you first light it. */ update(dt: number, heat?: HeatField | number): void; } /** * A note on the `draw` column, which was wrong by a factor of five. * * The first table gave a hood 0.9 m³/s of room-scavenging — more than a * smoking pan produces — so the extractor cleared the room no matter where * the pan was standing, and the entire distinction the track is built on * quietly stopped existing. Every test about capture still passed, because * they tested `catches` directly. * * Scavenging a standing layer through one small opening is SLOW. Catching a * plume that is rising straight into that opening is fast. Keeping the * second number much larger than the first is the only reason it matters * that the hood is over the hob. */ interface ExtractorOptions { era?: ExtractorEra; seed?: number; palette?: Palette; } /** A smoke hole, flue, hood or downdraft vent. */ declare function createExtractor(options?: ExtractorOptions): Extractor; interface SmokeLayerOptions { /** Room footprint, metres. */ width?: number; depth?: number; /** Floor to ceiling. Default 2.6. */ height?: number; /** * How thick the layer has to get at `alarmY` before the alarm sounds. * Default 0.35. 0 disables it. */ alarmAt?: number; /** Where the alarm is listening, metres above the floor. Default 2.3. */ alarmY?: number; /** How fast it leaks out through doors and gaps, per second. Default 0.02. */ leak?: number; seed?: number; palette?: Palette; } interface SmokeLayer extends Prop, SmokeField { /** How much is in the room, 0 (clear) to 1 (solid). */ readonly level: number; /** How far the layer has come DOWN from the ceiling, in metres. */ readonly descent: number; /** World Y of the underside of the layer. Everything above it is in it. */ readonly baseY: number; /** Sources feeding it. */ add(source: SmokeSource): void; /** Extractors fighting it. */ vent(extractor: Extractor): void; /** Clear the room — throw the windows open. */ clear(): void; smokeAt(x: number, y: number, z: number): number; /** Fires once when it gets thick at `alarmY`; re-arms once it clears. */ onAlarm?: (sounding: boolean) => void; update(dt: number): void; } /** * The room's standing smoke. * * A source puts smoke in, an extractor takes it out, and this is the thing * they argue over. The origin is on the floor at the centre of the * footprint — a room, not a prop, so it is placed like a rug. */ declare function createSmokeLayer(options?: SmokeLayerOptions): SmokeLayer; declare const SMOKE_STYLES: SmokeStyle[]; declare const EXTRACTOR_ERAS: ExtractorEra[]; /** * A pressure gauge — the first instrument in SCENA. * * Everything else in this library is a thing. This is a thing that *tells you * about* another thing, and the difference shows up in two places. * * The first is that a gauge is a **readout, not a Manipulable**. It has no * `open` and no `toggle`, because you cannot operate a gauge — you can only * read it. Its one input is a number and its one output is where the needle * happens to be, which lags behind because a real Bourdon tube has a spring * and a linkage in it. * * The second is subtler and it is the whole reason this is its own file. A * dial with a full ring of marks on it is a **clock**, and no amount of * captioning it "boiler pressure" will stop a viewer reading it as one. What * makes a disc read as an instrument is the **270° sweep with a dead zone at * the bottom** — the needle goes round most of the way and then stops, which * a clock never does — plus a red band at each end for the two numbers that * matter, and a needle that is off-centre, asymmetric and the wrong colour * for a clock hand. * * ```ts * const gauge = createPressureGauge({ max: 16, redline: 13.2, lowMark: 5 }); * pipe.add(gauge.object); * game.onUpdate((t) => { * gauge.setValue(plant.pressure); * gauge.update(t.delta); * }); * ``` * * It faces **+z with its origin at the mounting face**, the fixtures/wallArt * convention, so the caller rotates it onto whatever it is standing on. */ interface PressureGauge extends Prop { /** Where the needle IS, in bar. Eases toward `target`. */ readonly value: number; /** Where it has been told to go. */ readonly target: number; setValue(bar: number): void; /** Past the upper red band. Lights the pip beside the dial. */ readonly overRange: boolean; /** Below the lower red arc — she has not enough to work with. */ readonly low: boolean; update(dt: number): void; } interface PressureGaugeOptions { /** Dial radius. Default 0.16. */ radius?: number; /** Full scale, bar. Default 16. */ max?: number; /** Where the upper red band starts. Default 0.82 × max. */ redline?: number; /** Where the lower red arc ends. Default 0.25 × max. */ lowMark?: number; /** Numbered marks around the sweep. Default 11. */ ticks?: number; /** Starting reading. Default 0. */ value?: number; seed?: number; palette?: Palette; } declare function createPressureGauge(options?: PressureGaugeOptions): PressureGauge; /** * A steam plant — and the one propulsion in this trilogy whose output is * **not monotonic in its own control.** * * A sail gives you more the better you trim it. Oars give you more the harder * you pull. Open a steam engine right up and she goes **slower**, and that is * not a penalty anybody wrote: * * ```ts * plant.setLink(1.0); // full gear, longest cut-off * plant.setLink(plant.linkFor(3600)); // …and this one beats it, by a third * ``` * * The reason is that the regulator spends a store the fire fills a hundred * times more slowly than the engine empties it. Full gear admits steam for * most of the stroke, which is enormous torque and enormous consumption; the * boiler cannot keep up, the pressure sags, and half an hour later she is * making less power at 4 bar than she would have made all day at 9. Notch her * up — admit steam for a fifth of the stroke and let it *expand* — and she * settles at a speed she can hold. The whole nineteenth century is in that. * * So the store is the model. One integrated number, the boiler temperature, * with a **signed balance** across it: * * ``` * balance = raised − lost − engine − auxiliaries − vent − dumped * ``` * * and everything else is a read of that number. The pressure is not stored, * it is `pressureFor(temperature)` every time you ask — which is why the * needle sits flat on its stop for the first stretch of a cold light-up while * the funnel is already black. Below 100 °C there is no steam to have. * * ```ts * const ship = createDeckedShip({ era: 'steamer' }); * const plant = createSteamPlant({ kind: 'triple' }); * ship.object.add(plant.object); * * plant.setDraught(1); * plant.setRegulator(1); * plant.setLink(plant.linkFor(3600)); // a setting she will actually keep * * game.onUpdate((t) => { * plant.update(t.delta); // FIRST * ship.update(t.delta, { speed: plant.way, drift: plant.walk }); * }); * ``` * * `way` goes straight into `ShipInput.speed` — it is already in hull units, * like `OarBank.way` and unlike `SailRig.drive`. */ type SteamKind = 'sidelever' | 'compound' | 'triple' | 'launch'; /** * Era order — and the axis is **what she asks of you to give you power.** * * A sidelever wants a man with a shovel and gives you 20 rpm at one and a bit * bar. A triple wants a stokehold watch and gives you a fortnight at sea. And * `launch` is the inversion at the end of it: she asks nothing — no bed, no * bunker, no black smoke, steam in eleven minutes — and then refuses to start * at all, because she has one cylinder and can stop on dead centre. Same * table, opposite gameplay. */ declare const STEAM_KINDS: SteamKind[]; /** * rest / transitioning-toward / at-target / drifting-back, classified from * `balance` at the end of every update — never a transition table. * * `blowing` and `turning` are booleans *beside* it rather than states of their * own: a boiler blowing off is still `'up'`, and four states have no room for * over-range. */ type SteamState = 'cold' | 'raising' | 'up' | 'falling'; /** * The control duck-type again — deliberately not imported from `mechanisms`, * so a caller can hand any object with this shape to anything that wants one. */ interface SteamControl { readonly state: number; readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; update(dt: number): void; onChange?: (open: boolean) => void; object: Object3D; } interface SteamPlant extends Prop { kind: SteamKind; /** Boiler contents, °C. THE ONE INTEGRATED NUMBER. */ readonly temperature: number; /** Gauge pressure, bar. NOT STORED — derived from `temperature` every read. */ readonly pressure: number; /** °C/s, SIGNED. The value that was integrated this step, so the needle's * velocity and a planner's number cannot drift apart. */ readonly balance: number; readonly working: number; readonly blowOff: number; /** The red mark at the bottom of the dial. Read by the gauge, by `state` * and by `endurance` — and by NOTHING in the physics. Back pressure * already brings her smoothly to a stand. */ readonly low: number; /** How much of her power she can give you now, 0–1. */ readonly readiness: number; /** The pressure the fire NOW ON THE GRATE would settle at, capped at * `blowOff` because that is where the valve puts her. A getter, never a * table row — a written-down banked-hold figure is the single easiest * number in this family to get wrong. */ readonly reach: number; /** Seconds to `bar` at the firing order she has now. `Infinity` if that * fire will never get her there. * * Assumes you are NOT steaming while you wait, which is what notice means * — the same honest omission as `SailRig.layline` ignoring the tide. It * also ignores the fire's own catch time and the scale she lays down on * the way, so it runs a few per cent optimistic on a long light-up: under * 2% on a hand-fired boiler, more like 7% on a compound. */ noticeFor(bar: number): number; /** Seconds she will still have `bar` if nobody touches anything else. A * projection under held settings, not a promise. `Infinity` if she holds. */ holdsFor(bar: number): number; /** === `holdsFor(low)`. One integrator, so the two cannot disagree. */ readonly endurance: number; readonly state: SteamState; onState?: (state: SteamState) => void; /** What it is DOING, 0–1, eased. */ readonly firing: number; /** What it was TOLD. */ readonly draught: number; setDraught(level: number): void; /** Bank her: sugar for `setDraught(this era's banked level)`. Vocabulary, * not model. */ bank(): void; /** Coal on. Raises the bed and sets `green`, which is the black puff. A * SILENT NO-OP on `launch` — and that no-op is the era axis. */ stoke(amount?: number): void; /** Fire bed 0–1; caps `firing`. Always 1 on `launch`. */ readonly bed: number; readonly fuel: number; bunker(amount?: number): void; /** Scale on the tubes, 0–1. A MULTIPLIER on the fire, not a state: a * fouled boiler still gets there, it just never stops working for it. */ readonly scale: number; blowDown(): void; fireDoor: SteamControl; readonly regulator: number; setRegulator(open: number): void; /** Where the link IS: −1 (full astern gear) … 0 (mid-gear) … +1. NOT where * it was ordered — it travels, and it travels heavier under steam. * Cut-off AND direction in one number, because on a real engine they are * one lever. */ readonly link: number; readonly linkOrder: number; setLink(target: number): void; ahead(gear?: number): void; astern(gear?: number): void; stopEngine(): void; /** Fraction of the stroke steam is admitted for. */ readonly cutoff: number; /** Revolutions per second, SIGNED. */ readonly rev: number; /** Crank angle, rad, wrapping. The visible clock. */ readonly crank: number; readonly mep: number; /** Includes the ripple: it dips twice a rev on one cylinder. */ readonly torque: number; /** Stopped on dead centre with steam on and nothing happening. Only ever * true where `cyls === 1`, and that is not a special case — it is the * crank effort sum reaching zero. */ readonly onCentre: boolean; /** Bar her over by hand: a quarter turn, so she can start. */ barOver(): void; /** The longest cut-off she can hold for `seconds`, as a reverser position * you hand straight back to `setLink`, signed to her current direction. * 0 if she cannot hold anything that long. * * THE FUNCTION THE WHOLE THING EXISTS FOR, and the exact analogue of * `SailRig.layline`: ask for a passage, get a setting she will keep. */ linkFor(seconds: number): number; /** Her way through the water, m/s. Straight into `ShipInput.speed`. */ readonly way: number; /** Instantaneous. TELEMETRY — do not hand this to the hull. */ readonly thrust: number; /** Transverse thrust in WORLD m/s, ready for `ShipInput.drift`. MUTATED IN * PLACE each frame: a live view, not a snapshot. */ readonly walk: { x: number; z: number; }; /** How deep the screw is, 0 (racing in air) to 1. Feed it from the sea and * she races over the crests with nothing written for it. */ setImmersion(fraction: number): void; readonly immersion: number; readonly blowing: boolean; readonly turning: boolean; /** [soot, grease], parented at the funnel top and cross-faded. */ plumes: SmokeSource[]; /** The safety valve's white feather. ADDITIVE, driven with `setTarget`. */ feather: Steam; /** TRUE until you call `plumesInto`. */ readonly stepsPlumes: boolean; /** Hand the plumes to a room AND STOP STEPPING THEM. `SmokeLayer.update` * already calls `source.update` on everything add()ed; step them twice and * the rate easing and the shader clock both run at 2× with nothing * anywhere reporting it. Do NOT parent that layer to a moving hull — * the layer samples parent space. */ plumesInto(layer: SmokeLayer): void; /** Top of the funnel. Call `updateMatrixWorld` before reading its world * position, like every other mouth in SCENA. */ funnelTop: Object3D; gauge: PressureGauge; stokehold: PropSlot; platform: PropSlot; slots: PropSlot[]; update(dt: number): void; /** Explicit fast-forward for authoring. Places the shaft and hull at their * closed-form fixed points each coarse step and emits endpoints only. */ settle(seconds: number): void; } interface SteamPlantOptions { kind?: SteamKind; /** Where the crank starts, rad. Random by seed otherwise — and on a * single-cylinder engine, 0 is dead centre. */ crank?: number; /** Where she starts, in bar. DEFAULTS TO THE ERA'S WORKING PRESSURE — a * ship in a scene has steam up, the way a fridge in a kitchen is cold. * Pass 0 for a cold ship and be prepared to wait, or to call `settle`. */ pressure?: number; fuel?: number; /** Funnel top above the plant's origin, m. Default 16 — far enough up that * a deck-level camera is not standing inside the plume. */ funnelHeight?: number; /** Suppress the casing, keep the plumes. */ funnel?: boolean; push?: number; drag?: number; /** Exposed so a test can wire the valve shut and prove the cap is an * OBJECT and not a `Math.min`. */ reliefArea?: number; seed?: number; palette?: Palette; } /** * Saturated-steam pressure, bar GAUGE, from water temperature. Antoine, and * the `max(0, …)` is the whole reason the needle sits on its stop: below * 100 °C the absolute pressure is under one atmosphere and the gauge — which * measures the difference — reads nothing at all. */ declare function pressureFor(celsius: number): number; /** The inverse. Gauge bar in, °C out. */ declare function tempFor(bar: number): number; /** * Mean effective pressure as a fraction of boiler pressure, for a cut-off. * * `c·(1 + ln(1/c))` — admit for a fifth of the stroke and the steam still * does 0.52 of the work it would do admitted all the way, on a fifth of the * steam. That logarithm IS the argument for expansion, and it is also why * mid-gear produces no torque: the limit at c → 0 is zero, reached smoothly, * with nothing written to stop her. */ declare function expansionRatio(cutoff: number): number; /** Steam spent per unit of work: the reciprocal of the expansion gain. */ declare function steamPerWork(cutoff: number): number; declare function createSteamPlant(options?: SteamPlantOptions): SteamPlant; /** * Can the fireman see the fire? * * A ray from where he stands to the grate. It has to arrive without meeting * boiler plating on the way, which it only does if the firebox was built as * WALLS AROUND A VOID rather than as a solid drum with a door painted on it — * the failure that has already bitten half the containers in this library, * and that no test looking at numbers can catch. */ declare function firesVisibleFrom(plant: SteamPlant): boolean; /** * Below decks — and the first thing in this library where **where** you put * something changes what the whole object does. * * Everything else here that has a mass has it at its origin. A hold does not. * Put fifty tonnes in the fore hold and she goes down by the head; put it to * starboard and she lists; put it aboard at all and she sits deeper and her * propeller bites harder. The load is a position, not a number. * * ```ts * const hold = createHold({ kind: 'steamer' }); * ship.object.add(hold.object); * * hold.load('fore', 300); * game.onUpdate((t) => { * hold.update(t.delta); * ship.update(t.delta, { speed: plant.way, loading: hold.loading }); * plant.setImmersion(hold.immersion); * }); * ``` * * ## The weight is not the problem. The fact that it can move is. * * A hold **full** of water is safer than a hold **half full** of it, and the * reason has nothing whatever to do with how much water there is. A liquid * with a free surface runs to the low side as she leans, and the weight goes * with it — so her centre of gravity effectively rises, and she leans further. * The size of that virtual rise depends on the **width of the surface cubed** * and not at all on the depth of the liquid: * * ```ts * hold.pump('ballast', 1.0); // pressed full — no surface, no problem * hold.pump('ballast', 0.5); // slack — and this is the dangerous one * hold.pump('ballast', 0.0); // empty — safe again, and now she is light * ``` * * So `pump` is a verb that can kill her in either direction, and the right * answer is almost never "some". It is why tankers are loaded full or in * ballast and rarely between, and why a slack tank is a thing officers count. * * ## And an empty ship is not a safe ship * * Light, she floats high, her metacentric height is enormous, and she snaps * back from every roll hard enough to throw people off their feet and start * cargo moving. That is what ballast is *for* — you take weight aboard on * purpose, low down, to make her worse at standing up. `'light'` is a state * this module warns about, not a state it treats as empty and fine. */ type HoldKind = 'carrack' | 'steamer' | 'liner' | 'tanker'; /** * The era axis is **what you can do about it.** * * A carrack has one open hold and no pumps: your only lever is to move the * cargo, by hand, and a cargo that shifts on its own in a seaway is how ships * were lost. A steamer has separate holds and a double bottom you can pump — * one wide tank, so slack it is a menace. A liner subdivides everything, * which makes each free surface small and is the whole reason she is a place * people are willing to sleep. A tanker's cargo **is** the free surface, and * she is only ever safe pressed up or empty. */ declare const HOLD_KINDS: HoldKind[]; /** rest / transitioning-toward / at-target / drifting-back, on the GM axis. */ type TrimState = 'light' | 'laden' | 'tender' | 'lost'; /** * One space below decks. * * `slack` is the whole module in one boolean: a liquid compartment that is * neither empty nor pressed up. */ interface Compartment { name: string; /** Centre, fore-and-aft, in vessel metres. Positive is forward. */ z: number; /** Centre athwartships. Positive is to starboard. */ x: number; length: number; width: number; depth: number; /** Height of its floor below the waterline, m. */ floor: number; /** Does it hold liquid? Only liquids have a free surface. */ liquid: boolean; /** Tonnes it will take. */ capacity: number; /** Tonnes in it now. */ readonly load: number; /** How full, 0–1. */ readonly level: number; /** Liquid, and neither empty nor pressed up. */ readonly slack: boolean; /** Its own free-surface moment, tonne·m⁴ — `width³ × length / 12`, and it * does not depend on how much is in it. */ readonly surfaceMoment: number; /** Where its contents actually sit across it, in metres from its own * centreline. Cargo stowed to one side is the commonest way a ship gets * a list, and it is nobody's decision — it is a mistake. */ readonly offset: number; } /** * What the hull takes from a load. * * The same shape as `ShipInput.drift` and passed the same way — but it is the * first thing in that channel that is a **state of the vessel** rather than a * force on her. A drift stops when the tide slackens. A list does not stop. */ interface Loading { /** Radians of bow-down pitch. Positive is down by the head. */ trim: number; /** Radians of list. Positive is to starboard. */ list: number; /** Extra metres she is sitting down in the water. */ sink: number; /** Multiplier on how fast she answers the sea. A stiff ship SNAPS. */ stiffness: number; } interface Hold extends Prop { kind: HoldKind; compartments: Compartment[]; /** By name, because `compartments[2]` is not a thing anybody says. */ at(name: string): Compartment | undefined; /** * Put weight in. Returns the tonnes that would not fit. * * `side` is where it goes ACROSS the compartment, −1 hard to port through 0 * on the centreline to +1 hard to starboard. It is the whole reason a ship * with her cargo correctly distributed fore and aft can still be lying over * at ten degrees, and there is no total tonnage that says so. */ load(name: string, tonnes: number, side?: number): number; /** Take it out. Returns the tonnes that actually came. */ unload(name: string, tonnes: number): number; /** Move it — the only lever a carrack has, and the slow one. */ shift(from: string, to: string, tonnes: number): number; /** * Pump a liquid compartment toward a fraction of its depth. * * DANGEROUS IN BOTH DIRECTIONS. Emptying a pressed-up tank takes her * through slack on the way, and slack is where the free surface is. */ pump(name: string, to: number): void; /** Water where it should not be — and it has the widest surface aboard. */ readonly bilge: number; /** Start her making water, tonnes/s. */ holed(rate: number): void; /** Suction on the bilge. Slower than the sea, which is the point. */ pumpBilge(on: boolean): void; readonly pumping: boolean; /** Tonnes aboard, cargo and ballast and bilge. */ readonly deadweight: number; /** Tonnes of hull and everything in her. */ readonly displacement: number; /** How deep she floats, m. */ readonly draught: number; /** Metres of side left above the sea. */ readonly freeboard: number; /** How much of the screw is in the water, 0–1. Straight into * `SteamPlant.setImmersion`. */ readonly immersion: number; /** Metacentric height WITH the free-surface correction applied, m. THE * number: it is what decides everything else here. */ readonly gm: number; /** What she would have if nothing aboard could move. `gm` is this minus * `freeSurface`, and the difference is the module. */ readonly solidGm: number; /** Virtual rise in her centre of gravity from every slack surface, m. */ readonly freeSurface: number; /** Seconds for one complete roll. Short is STIFF and violent; long is * tender and she hangs at the end of each one. */ readonly rollPeriod: number; /** Loaded to her marks: 0 light, 1 down to the load line, >1 overloaded. */ readonly toMarks: number; readonly state: TrimState; onState?: (state: TrimState) => void; /** She has taken an angle of loll and is lying there. A boolean BESIDE the * state, because a ship lolling is still `'lost'` by the same measure. */ readonly lolling: boolean; /** * Hang an EXTERNAL heeling moment on her, tonne·metres, positive to * starboard. Named, so several can be live at once and each updated on its * own; pass 0 to take one off. * * A working boat is heeled by her gear rather than by her cargo, and the * wire is not aboard her — but the arithmetic is identical the moment it * reaches her deck, and so is the angle past which she does not come back. * Everything that capsizes a badly stowed ship capsizes a tug girted by her * own tow, through this. */ heel(name: string, tonneMetres: number): void; /** Past the angle of vanishing stability — over, and not coming back. */ readonly capsized: boolean; /** Her angle of vanishing stability, radians. Published so a caller can * say how close she is rather than only that she has gone. */ readonly vanishing: number; /** Hand this straight to `ShipInput.loading`. Mutated in place. */ readonly loading: Loading; hatch: PropSlot; slots: PropSlot[]; update(dt: number): void; } interface HoldOptions { kind?: HoldKind; /** Override the vessel's dimensions, if the hold is going into something * other than the hull its kind is sized for. */ length?: number; beam?: number; /** Tonnes of hull, engines and everything that is not cargo. */ lightship?: number; /** * How deep the HULL she is going into is drawn, m — `DeckedShip.draft`. * * `sink` is measured from this, because it is the datum the hull mesh was * built to. Leave it out and the hold measures from its own load line * instead: a ship loaded exactly to her marks is then lifted clear of the * water by the difference between the two, a light one is lifted almost * out of it altogether, and there is no number anywhere that says so. * * ```ts * createHold({ kind: 'steamer', draft: ship.draft }); * ``` */ draft?: number; /** Start her loaded. Names not in the kind are ignored. */ cargo?: Record; seed?: number; palette?: Palette; } declare function createHold(options?: HoldOptions): Hold; /** * What one slack tank costs her, in metres of metacentric height. * * Published on its own because it is the number the whole module turns on and * because it is worth being able to ask before you pump: a surface **eight * metres** wide costs eight times what a **four metre** one does, at any * depth of liquid whatever. Subdivide a tank in two and you have divided its * penalty by four. */ declare function freeSurfaceCost(width: number, length: number, displacement: number): number; /** Where a point in the hold is, in the vessel's frame. Handy for placing * lights, ladders and people down there. */ declare function holdPoint(hold: Hold, name: string, out?: Vector3): Vector3; /** * Stabilisers — and the only thing in this library that **stops working when * you stop.** * * A fin stabiliser is a wing. It makes its righting moment out of lift, and * lift comes out of water going past it, so a ship lying stopped has none at * all — she rolls exactly as badly as she would with no fins fitted, and the * fins are still there costing her the drag. Get her moving and they come * alive; the faster she goes the better they work, because lift goes as the * **square** of the speed. * * ```ts * const fins = createStabilisers({ kind: 'activeFin' }); * ship.object.add(fins.object); * * fins.deploy(true); * game.onUpdate((t) => { * fins.setWay(plant.way); // …and this is what they run on * fins.update(t.delta); * ship.update(t.delta, { * speed: plant.way - fins.drag, // they are not free * damping: fins.damping, * }); * }); * ``` * * That is backwards from every other comfort in a ship. A wider hull is calm * at anchor. Deep loading is calm at anchor. Fins are the one thing that is * worst exactly when she is least able to do anything about it — riding out a * gale hove to, which is the moment you want them most. * * ## They take the roll out and leave the pitch * * `damping` touches `ShipInput.damping`, which touches the roll and nothing * else. A stabilised ship in a head sea pitches exactly as hard as an * unstabilised one, and that is the commonest complaint about them rather * than a simplification here. */ type StabiliserKind = 'bilgeKeel' | 'fin' | 'activeFin' | 'gyro'; /** * The era axis is **what it needs from you.** * * A bilge keel needs nothing whatever — welded on, never moves, works at any * speed including none — and takes about a quarter of the roll out. A fin * needs way. An active fin needs way *and* power and gives you nearly all of * it back. And the gyro is the inversion at the end: it asks for no speed, no * water, no drag and no thought, and then simply cannot lift a big ship — * which is why it is on yachts and not on liners. */ declare const STABILISER_KINDS: StabiliserKind[]; interface Stabilisers extends Prop { kind: StabiliserKind; /** Run them out, or house them. A no-op on a bilge keel, which is welded on * — and that no-op is the era axis. */ deploy(out: boolean): void; /** Where the fins ARE, 0 housed to 1 fully out. They travel. */ readonly out: number; readonly ordered: boolean; /** Her way through the water, m/s. What they run on. */ setWay(speed: number): void; readonly way: number; /** * How much of her roll they are ACTUALLY taking out, 0–1 — straight into * `ShipInput.damping`. * * Not what they are rated at: what they are managing, at this speed, at * this much deployment. Zero when she is stopped, for everything but a * gyro. */ readonly damping: number; /** What they are rated at, with all the way in the world. */ readonly rated: number; /** Speed they are costing her, m/s. Subtract it from what you hand the * hull — comfort is not free and this is the bill. */ readonly drag: number; /** Working, and not merely deployed. */ readonly biting: boolean; /** The speed below which they do essentially nothing, m/s. Published so a * bridge can say "we need eight knots to steady her" rather than a * caller having to know it. */ readonly bites: number; update(dt: number): void; } interface StabiliserOptions { kind?: StabiliserKind; /** Her beam, m — the fins are sized off it. Default per kind. */ beam?: number; /** How far below the waterline they come out. Default per kind. */ depth?: number; /** Start them out. Default false, because a fin housed is a fin that has * not yet been bent on somebody's quay. */ deployed?: boolean; seed?: number; palette?: Palette; } declare function createStabilisers(options?: StabiliserOptions): Stabilisers; /** * How much roll a set of fins would take out at a given speed — without * building any. * * Published for the same reason `freeSurfaceCost` is: it is the question you * want to ask *before* you commit, and the answer is a curve rather than a * yes. At half their biting speed they are managing a fifth of what they are * rated at; at twice it, four fifths. */ declare function dampingAt(kind: StabiliserKind, speed: number): number; /** * Plumbing — the first thing in this library where **what you get depends on * what somebody else is doing.** * * Everything up to here is local. A boiler makes steam out of its own fire; a * hull floats on its own displacement; a light is a fact about one observer. * None of them has any opinion about what else is happening in the world. A * water supply is a *network*, and a network's defining property is that it is * shared — so this is the first module in the trilogy where two objects * interfere with each other without either one knowing the other exists. * * ## The shower goes scalding when the lavatory is flushed * * Everybody has had this happen and almost nobody has the mechanism right. It * is not a temperature failure. **It is a pressure failure that arrives as a * temperature.** * * A mixer set to 40 °C from 60 °C hot and 10 °C cold is running 60 % hot. A WC * cistern draws from the COLD branch only. The cold manifold's pressure drops, * so the cold flow through the mixer drops — and the hot does not, because * nothing happened to the hot branch. Same hot, less cold, and the mixture * climbs: * * ```ts * cold at 100% -> 40.0 °C * cold at 72% -> 43.8 °C a basin tap opens * cold at 55% -> 46.6 °C a WC fills SCALD * cold at 25% -> 52.9 °C the cold branch nearly dies burns in seconds * cold at 0% -> 60.0 °C you are standing under the cylinder * ``` * * Nothing in the shower changed. Nothing in the mixer changed. Somebody in * another room pressed a lever. * * ## What happens to the person in the shower * * The era axis, and it is not about how much water you get: * * | kind | what a flush does to the shower | * | --- | --- | * | `bucket` | nothing. There is no network, so there is nothing to share. | * | `gravity` | takes the flow away. A cistern in the loft has a third of a bar and nothing to spare. | * | `mains` | takes the TEMPERATURE away. There is flow to spare and the scald arrives instead. | * | `thermostatic` | takes a little flow, and holds the temperature. | * * A bucket has no contention because it has no network — which is not * primitive, it is *uncoupled*, and it is the only supply here that cannot * scald anybody. * * And `thermostatic` is the inversion at the end, the same shape as a gyro * stabiliser that needs no way from you and a sectored light that navigates * instead of you: **it does not stop the contention. It stops the contention * from reaching you.** The pressure still collapses; the mixer gives up flow * to hold the temperature, and if the cold fails altogether it shuts off * rather than deliver sixty degrees. * * ## The store empties seven times faster than it fills * * A 120-litre cylinder at 60 °C, and a shower drawing six and a half litres a * minute of hot: * * ```ts * plumbing.hotLastsFor() / 60; // 20 minutes * plumbing.reheatTakes() / 60; // 140 minutes, on a 3 kW immersion * ``` * * Which is the steam plant again in a different trade: a store the heater * fills far slower than the outlet empties it. There is no way to have a long * shower and a hot bath afterwards, and no setting anywhere that changes it. * * And it does not cool — it **runs out**. The cylinder is stratified: hot * floats on the cold feed coming in underneath and is drawn off the top at * very nearly full temperature until it is gone. So the shower stays perfect, * and stays perfect, and then falls off a cliff — which is what everybody has * actually stood in, and is nothing like the gentle fade a stirred-tank model * produces. * * ## Height is pressure, and that is the whole argument for a pump * * On gravity the pressure at an outlet is the head above it and nothing else — * so the same house gives a different shower on each floor: * * ```ts * // a cistern at 8 m * ground floor (0.0 m): 0.79 bar -> 7.5 L/min * first floor (2.7 m): 0.52 bar -> 6.1 L/min * second floor (5.4 m): 0.26 bar -> 4.3 L/min * ``` * * Pass an outlet's `height` and it is worked out for you. It is the one number * in this module that a fitter cannot argue with. */ type SupplyKind = 'bucket' | 'gravity' | 'mains' | 'thermostatic'; declare const SUPPLY_KINDS: SupplyKind[]; /** idle / comfortable / noticeably down / not enough to use. */ type SupplyState = 'idle' | 'easy' | 'strained' | 'starved'; /** What kind of thing is on the end of the pipe. */ type OutletKind = 'shower' | 'tap' | 'bath' | 'wc'; interface Draw { name: string; kind: OutletKind; /** How far open, 0–1. */ open: number; /** Litres a minute, both branches together. */ flow: number; /** …of which this much is coming out of the cylinder. */ hot: number; /** Degrees C at the outlet. */ temp: number; /** * Over 44 °C, which is the limit for water a person stands under. * * Fifty and above scalds a child in well under a minute; sixty does it in * about a second, and sixty is simply the cylinder temperature — the number * you get when the cold gives up entirely. */ scalding: boolean; /** Enough flow to be worth having. A shower under five litres a minute is a * drip you stand in. */ usable: boolean; /** * Bar AT THE OUTLET — the manifold pressure less the lift to get up there. * * Reporting the manifold instead reads backwards: raise an outlet and it * draws less, so the manifold pressure goes UP while the shower gets worse. */ pressure: number; } interface OutletOptions { kind?: OutletKind; /** Where it is, for the pipework to be drawn to. */ at?: Vector3; /** * Metres above the supply datum. * * On gravity this is subtracted from the head, so it decides everything. An * outlet above the cistern gets nothing at all, which is why the cistern is * in the loft and why a shower in a loft conversion does not work. */ height?: number; /** Fraction of the flow taken from the hot branch, 0–1. A WC ignores it. */ mix?: number; } interface Plumbing extends Prop { kind: SupplyKind; /** Add something on the end of a pipe. */ outlet(name: string, options?: OutletOptions): void; open(name: string, fraction?: number): void; close(name: string): void; /** Move the mixer. 0 is all cold, 1 is all cylinder. */ setMix(name: string, hotFraction: number): void; /** * Set it to a TEMPERATURE, which is what a person actually does. * * Nobody turns a shower to 'sixty per cent hot'. They turn it until it feels * right, with whatever else in the house happens to be running at that * moment — and the setting is then fixed, so when the conditions change the * temperature does. Calibrating by mix fraction quietly assumes the tap * knows something it cannot know, and hides the entire failure. */ setTarget(name: string, celsius: number): void; /** What that outlet is getting, right now, given everything else. */ drawAt(name: string): Draw | null; readonly draws: Draw[]; readonly outlets: string[]; /** Bar at the cold manifold. */ readonly pressure: number; /** Bar at the hot manifold — a different number, and that is the module. */ readonly hotPressure: number; /** Litres a minute leaving the system. */ readonly demand: number; readonly state: SupplyState; onState?: (state: SupplyState) => void; /** Litres of usable hot water left in the cylinder. */ readonly hot: number; /** What is in the cylinder now, °C. It falls as it is drawn. */ readonly hotTemp: number; readonly cylinder: number; /** * Seconds until the water stops being warm enough to stand under, at the * present draw — or `Infinity` if nothing is drawing hot. * * A cylinder does not run out, it cools: a tank model falls exponentially * and never reaches the cold feed, so 'litres of hot left divided by the * rate' overstates it badly. This integrates the thing forward until the * delivered temperature drops below `warm`, which is the number a person * standing in it would recognise. */ hotLastsFor(warm?: number): number; /** Seconds to bring a cold cylinder back up. */ reheatTakes(): number; /** Switch the immersion on and off. */ setHeater(on: boolean): void; readonly heating: boolean; /** * Tip water in by hand, litres. * * The bucket loop, and it works on every kind — because it is the one way of * getting water that no supply failure can take away from you. */ pour(name: string, litres: number): void; readonly poured: number; station: PropSlot; slots: PropSlot[]; update(dt: number): void; } interface PlumbingOptions { kind?: SupplyKind; /** Metres of head, for `gravity`. The loft, usually. */ head?: number; /** Bar at the stopcock, for the pressurised kinds. */ mains?: number; /** Cylinder size, litres. */ cylinder?: number; /** Immersion rating, kW. */ heater?: number; /** What the cylinder is held at, °C. */ stored?: number; /** What comes out of the ground, °C. */ cold?: number; seed?: number; palette?: Palette; } /** Above this, water is not something a person can stand under. */ declare const SCALD = 44; /** Bar from metres of head. A loft cistern is a third of a bar and no more. */ declare function headPressure(metres: number): number; /** * Litres a minute through an orifice at a given pressure. * * `Q = k√p`, calibrated so a shower head at two bar gives twelve litres a * minute — which is what a decent mains shower actually does. */ declare function orificeFlow(k: number, bar: number): number; declare function createPlumbing(options?: PlumbingOptions): Plumbing; /** * The temperature a mixer actually delivers when the cold has been taken away. * * Published on its own because it is the one sum in this module worth knowing * without a plumbing system attached: `mix` is what the tap was set to, and * `coldLeft` is the fraction of the cold flow that survived somebody else * opening something. */ declare function mixedAt(mix: number, hotTemp: number, coldTemp: number, coldLeft?: number): number; /** The mixer setting that gives a wanted temperature. */ declare function mixFor(want: number, hotTemp: number, coldTemp: number): number; /** * A public address — the first prop in this library that reaches the ear. * * Everything until now has been seen. A hull, a light, a derrick, a boiler: * all of them are things you look at, and the ones that answer questions * about a point in space — `heatAt`, `smokeAt`, `depthAt` — answer them about * where a *body* is. Sound is the first field where the interesting number is * about what a **person** can do there: whether they can talk, whether they * can stay, how long before it costs them something. * * ```ts * const pa = createPA({ era: 'array', power: 118 }); * pa.levelAt(0, 100); // 93 dB(A), a hundred metres back * pa.stateAt(0, 100); // 'harmful' * pa.earshotAt(0, 100); // 0.18 m — you shout into an ear or you don't talk * pa.exposureAt(0, 100); // 4 500 s before the day's dose is gone * ``` * * ## Distance is a filter, not a volume knob * * The one fact everybody has met and almost nobody models: from far enough * away, a band is **all bass**. That is not an artistic choice about mixing, * it is air. Absorption is strongly frequency-dependent — about 0.004 dB/m at * 125 Hz and 0.15 dB/m at 4 kHz — so over 800 m the bass loses 3 dB and the * top loses a hundred and twenty: * * ```ts * 3 m bass 110 mid 111 treble 110 a band * 100 m bass 85 mid 92 treble 83 a band, further off * 300 m bass 74 mid 77 treble 47 a PA, over there * 800 m bass 64 mid 55 treble 33 a thud, two streets away * ``` * * Model sound as one number that falls off with range and you get a quiet * band. It is not a quiet band. It is a **different band**, and that is why * `bandsAt` exists next to `levelAt`. * * ## What it costs the person standing there * * The states are not `'off' | 'low' | 'high'` — that is a fact about the * amplifier. They are what a person has to do: * * | state | | * | --- | --- | * | `quiet` | you can talk | * | `raised` | you are raising your voice and have not noticed | * | `shouting` | you shout into an ear, or you do not talk | * | `harmful` | the day's safe dose runs out in under four hours | * * ## The era axis: what the front row pays for the back row * * A PA has exactly one hard problem, and it is not power. It is that the * front row and the back row are the same system. Cover 200 m to a usable * 75 dB(A) at the back and ask what that does to the people at the barrier: * * | era | front row | safe for | * | --- | --- | --- | * | `horn` | 113 dB(A) | 49 seconds | * | `hifi` | 114 dB(A) | 35 seconds | * | `array` | 105 dB(A) | 5 minutes | * | `delayed` | 91 dB(A) | 2 hours | * * `delayed` is the inversion at the end of the axis, in the same shape as the * gyro stabiliser and the thermostatic mixer: **it does not make the PA * louder, it stops the loudness having to reach that far** — and the bill is * paid in a completely different currency, which is *time*. Get a delay wrong * and every one of those people hears an echo. */ /** How the sound is thrown. */ type PAEra = /** Horns on a pole. Efficient, directional, and no bass at all. */ 'horn' /** A pair of full-range stacks. A point source: 6 dB per doubling. */ | 'hifi' /** A line array. Cylindrical near field: 3 dB per doubling, while it lasts. */ | 'array' /** An array plus delay towers downfield. The front row stops paying. */ | 'delayed'; /** * Three bands, because one number cannot say what distance does to sound. * * Centred at 125 Hz, 1 kHz and 4 kHz — low enough to diffract round a wall, * the region speech lives in, and the region air eats. */ type Band = 'bass' | 'mid' | 'treble'; /** What it costs to stand there. Measured in the person, not the amplifier. */ type LoudnessState = 'quiet' | 'raised' | 'shouting' | 'harmful'; /** * What two arrivals of the same sound do to each other. * * `clean` — one source, nothing to interfere with. * `comb` — under 5 ms apart: not an echo, a *filter*. Hollow and phasey. * `fused` — the precedence effect. Two arrivals, one apparent source. * `echo` — over 40 ms: you hear the sound twice, and you cannot unhear it. */ type EchoState = 'clean' | 'comb' | 'fused' | 'echo'; /** Unweighted sound pressure level in each band, dB re 20 µPa. */ interface BandLevels { bass: number; mid: number; treble: number; } /** * How loud it is at a world point. * * The fifth spatial handshake, after `depthAt`, `heatAt`, `chillAt` and * `smokeAt`, and deliberately the same shape. SCENA says how loud it is * there; ANIMA decides whether a character has to shout and GAMA decides * whether an agent wants to be there. */ interface SoundField { /** A-weighted sound pressure level at a world point, dB(A). */ levelAt(x: number, z: number): number; } /** A hang, a stack, or a delay tower. */ interface SourceSpec { /** World position. Defaults to the PA's own origin. */ x?: number; z?: number; /** Height of the acoustic centre above ground, metres. */ height?: number; /** Facing, radians, 0 = +z. Defaults to the PA's facing. */ facing?: number; /** On-axis sound pressure level at 1 m, dB. */ power?: number; /** * Vertical extent of the radiating source, metres. 0 for a point source. * A line array's near field — where it loses 3 dB per doubling instead of * 6 — extends to about `length² · f / 2c`, so a longer hang keeps its level * further out, and keeps it further out *for the top than for the bottom*. */ length?: number; /** Electronic delay, seconds. See `alignDelays`. */ delay?: number; } /** * A wall between a source and an ear. * * A line in plan with a height — which is all a barrier is acoustically. It * is not drawn: this is a fact about the sound, in the same way `heightAt` is * a fact about the ground, and whatever put the wall there draws it. */ interface BarrierSpec { /** One end, in world coordinates. */ x1: number; z1: number; /** The other end. */ x2: number; z2: number; /** Height of the top edge above ground, metres. */ height: number; } interface PAOptions { era?: PAEra; /** On-axis SPL at 1 m of the main hang, dB. Defaults per era. */ power?: number; /** World position of the main hang. */ x?: number; z?: number; /** Height of the main hang's acoustic centre, metres. */ height?: number; /** Facing, radians. 0 = +z, which is the direction the crowd is in. */ facing?: number; /** Ear height for every query, metres. */ earHeight?: number; /** Background level with the PA silent, dB(A). */ ambient?: number; /** How much of full output the programme is asking for, 0–1. */ program?: number; /** For `delayed`: how many towers, and how far downfield the field runs. */ towers?: number; fieldLength?: number; } /** What one source contributes at a point. */ interface SoundArrival { name: string; /** A-weighted contribution, dB(A). */ level: number; /** When it gets there: flight time plus electronic delay, seconds. */ arrival: number; } interface EchoReading { /** Milliseconds between the first and last audible arrival. */ spread: number; state: EchoState; /** How many arrivals are within 15 dB of the loudest. */ arrivals: number; } interface PublicAddress extends Prop, SoundField { readonly era: PAEra; /** Every source, mains first. */ readonly names: string[]; /** A-weighted level at a world point, dB(A). */ levelAt(x: number, z: number): number; /** Unweighted level in each band at a world point, dB. */ bandsAt(x: number, z: number): BandLevels; /** What it costs to stand there. */ stateAt(x: number, z: number): LoudnessState; /** Seconds of the daily noise dose that point spends per second. */ exposureAt(x: number, z: number): number; /** Metres at which a shout is still intelligible there. */ earshotAt(x: number, z: number): number; /** What the arrivals do to each other there. */ echoAt(x: number, z: number): EchoReading; /** Every source's contribution at a point, loudest first. */ arrivalsAt(x: number, z: number): SoundArrival[]; /** How far down the axis the level stays at or above `target` dB(A). */ reach(target?: number): number; /** The level at the barrier — `distance` metres down the axis. */ frontRow(distance?: number): number; /** Add a source. Returns its name. */ tower(name: string, spec?: SourceSpec): string; /** Register a wall. Barriers shadow every source. */ barrier(name: string, spec: BarrierSpec): void; /** Forget a wall. */ clearBarrier(name: string): void; setPower(name: string, dB: number): void; setDelay(name: string, seconds: number): void; /** How hard the programme is driving it, 0–1. 0 is silence. */ setProgram(level: number): void; /** * Set every tower's delay from its distance to the mains, plus `haas`. * * The extra offset is the whole trick: aligned to the arithmetic and no * more, the two arrivals land within a millisecond of each other and comb. * Ten or fifteen milliseconds late and the mains arrive first, so the sound * still comes from the stage. */ alignDelays(haas?: number): void; /** * Turn every source down as far as it will go and still cover the field. * * Each source is sized for the end of its own zone, in order — which is * what a system tech does, and what makes the era axis a fair comparison * rather than four different volume settings. */ cover(length: number, target?: number): void; /** Paint the field. Grey-blue → green → amber → red, by state. */ showCoverage(on: boolean, opts?: { width?: number; depth?: number; cell?: number; }): void; update(dt: number): void; } /** Speed of sound, m/s, at 20 °C. */ declare const SPEED_OF_SOUND = 343; /** Band centre frequencies, Hz. */ declare const BAND_HZ: Record; /** * Air absorption, dB per metre, at 20 °C and 50 % relative humidity. * * Nearly forty times worse at 4 kHz than at 125 Hz, and that ratio is the * whole reason a distant PA is a thud. */ declare const AIR_ABSORPTION: Record; /** * A-weighting at the band centres, dB. * * The ear is nearly deaf to bass at low levels, and the weighting says so: * −16 dB at 125 Hz. Which is also why a festival can measure legal on a * dB(A) meter at the site boundary while the people two streets away lie * awake — everything they can hear is in the band the meter discounts. */ declare const A_WEIGHTING: Record; /** * Most a thin barrier can take off, per band. * * Diffraction over the top is not the only path: some of it comes straight * through the panel, and transmission loss follows the mass law — 6 dB per * doubling of frequency. So a wall has a floor, and the floor is lowest * exactly where the diffraction is weakest. */ declare const BARRIER_CAP: Record; /** Below this a person can hold a conversation. */ declare const QUIET = 62; /** Above this they must shout to be heard at all. */ declare const SHOUTING = 78; /** Above this the day's dose runs out in under four hours. */ declare const HARMFUL = 88; /** A shout, at one metre, dB(A). */ declare const SHOUT_AT_1M = 78; /** Energy sum of decibel values. */ declare function sumDecibels(values: number[]): number; /** * Spreading loss at range `r`, dB. * * A point source loses 6 dB per doubling — the surface of the sphere the * energy is spread over goes as r². A line source loses **3**, because near * enough to it the wavefront is a cylinder, not a sphere. That is the entire * argument for a line array, and it holds only out to the array's near-field * limit, which is proportional to frequency: a 6 m hang holds the top up to * 210 m and the bass to 6 m. Which is why the back of a festival gets a thin, * mid-heavy sound and the subs are a separate problem. */ declare function spreadingLoss(r: number, length: number, band: Band): number; /** * Maekawa's barrier attenuation, dB, from the path-length difference. * * The number that matters is the **Fresnel number** `N = 2δ/λ`: how many * half-wavelengths of extra path the sound has to take to get over the top. * A 3 m wall against a 4 kHz wavelength of 86 mm is an obstacle; against a * 125 Hz wavelength of 2.7 m it is barely there. `blocked` is false when the * ear can see the source over the wall — attenuation then falls away over the * transition zone rather than stopping dead, because being in line of sight * is not the same as being clear of the first Fresnel zone. */ declare function barrierLoss(delta: number, band: Band, blocked: boolean): number; /** * Seconds at `dBA` before the day's noise dose is used up. * * 85 dB(A) for eight hours, and a 3 dB exchange rate — every 3 dB halves the * time, because 3 dB is twice the energy. Which makes 100 dB(A) a quarter of * an hour and the front row of a badly designed PA a matter of seconds. */ declare function exposureLimit(dBA: number): number; /** Metres at which a shout is still intelligible against `dBA` of noise. */ declare function earshot(dBA: number): number; /** Classify a level in what it costs the person standing there. */ declare function loudnessState(dBA: number): LoudnessState; declare function createPA(options?: PAOptions): PublicAddress; /** * The woofer — the first prop in this library that is not all here. * * Everything else in the trilogy is simulated: a boiler makes steam out of * numbers this library owns, a hull floats on arithmetic, and even the PA is * a field computed from a power figure. Operate this and it plays **web * radio** — a stream from outside the process and outside the frame clock, * that keeps playing whether or not anybody is looking, that buffers, drops * and dies, and that no amount of correct local code can make reliable. * * ```ts * const rig = createWoofer({ seed: 7 }); * const floor = createDanceTiles({ cols: 10, rows: 8 }); * scene.add(rig.object, floor.object); * * // THE interaction. First touch starts the radio; the next touches tune. * canvas.addEventListener('pointerdown', () => rig.operate()); * * game.onUpdate((t) => { * rig.update(t.delta); * floor.feed(rig.pulse()); // and the DJ tiles come alive * floor.update(t.delta); * }); * ``` * * ## The dropout is the design problem * * A stream fails four ways — it takes a moment to start, it rebuffers, the * station dies, the CORS handshake refuses — and a dance floor that freezes * whenever the network hiccups is a network monitor wearing a glitter ball. * So the rig has a **bed**: a seeded, deterministic beat that runs under * everything, takes the floor whenever the stream cannot, and hands back the * moment it can. The states say who is driving: * * | state | who has the floor | * | --- | --- | * | `off` | nobody. The cones are still. | * | `demo` | the bed, by choice — no radio was asked for. | * | `tuning` | the bed, while the stream buffers its first seconds. | * | `live` | **the radio.** The one state this module cannot fake. | * | `holding` | the bed, because the stream stalled — and the tiles never knew. | * * `holding` is the inversion at the end of this axis, in the same shape as * the thermostatic mixer and the delay tower: it does not stop the dropout, * it stops the dropout **reaching the floor**, and the bill is paid in * honesty — what you are hearing is not the radio, and `state` says so. * * ## Where the pulse comes from * * Live, the pulse is measured off the actual audio with an analyser: bass / * mid / treble energy plus a beat detector watching for the kick. In every * other state it comes from the bed. Either way `pulse()` has the same shape, * which is the whole point — the tiles do not know, and must not know, where * the music is coming from. * * ## What runs where * * - **A browser, after a user gesture** — the real thing. Autoplay policy * means nothing sounds until somebody interacts, which is not a bug to * fight: the rig is off until somebody turns it on. * - **A browser, no gesture yet / headless** — `demo`. The bed drives the * tiles, deterministically, so the picture is alive and verifiable with no * network and no audio device. * - **Node (the tests)** — there is no `Audio` here at all. The bed is pure * arithmetic and the whole state machine is testable through an injected * fake stream. * * The default stations are SomaFM channels, which send the CORS header that * lets a `MediaElementAudioSourceNode` actually read the samples. Point * `tune()` at a station without that header and the element itself refuses * to load — which arrives as an error, which is a `holding`, which the floor * survives. That failure path is the module. */ /** Who currently has the floor. */ type WooferState = 'off' | 'demo' | 'tuning' | 'live' | 'holding'; /** A web radio channel. */ interface RadioStation { name: string; /** Stream URL. Must send CORS headers for the analyser to read anything. */ url: string; /** Rough words for what plays there. */ genre: string; } /** * SomaFM, because they are listener-supported, run for decades, and — the * property that matters here — send `Access-Control-Allow-Origin` on their * streams, so the analyser can actually see the music it is playing. */ declare const RADIO_STATIONS: RadioStation[]; /** One reading of the music, wherever it is coming from. */ interface AudioPulse { /** Band energies, 0–1. */ bass: number; mid: number; treble: number; /** True on the frame a kick landed. */ beat: boolean; /** Estimated tempo, beats per minute. 0 until there is one. */ bpm: number; } /** * The seam a live stream plugs in through — and the seam a test fakes. * * Everything above this line is deterministic and runs anywhere; only an * implementation of this interface ever touches the network or an audio * device. If the module needed a real stream to be exercised, the module * would be designed wrong. */ interface RadioMedia { /** Point at a stream and start loading. */ tune(url: string): void; /** Try to start. Rejects when autoplay policy or the network says no. */ play(): Promise; pause(): void; /** 'playing' | 'waiting' | 'error' — the three transitions that matter. */ on(event: 'playing' | 'waiting' | 'error', cb: () => void): void; /** Measured band energies, or null while there is nothing to measure. */ bands(): { bass: number; mid: number; treble: number; } | null; } interface WooferOptions { seed?: number; /** Stations `operate()` cycles through. Default: `RADIO_STATIONS`. */ stations?: RadioStation[]; /** Bed tempo, BPM. Default seeded 118–128. */ bpm?: number; /** On-axis dB at 1 m when flat out, for `levelAt`. Default 106. */ power?: number; /** Inject a stream implementation (tests). Default: Web Audio, if present. */ media?: RadioMedia; } interface Woofer extends Prop, SoundField { readonly state: WooferState; readonly stations: RadioStation[]; /** The station tuned, or null before the first `play`. */ readonly station: RadioStation | null; /** Bed tempo (and the reported bpm while the bed drives). */ readonly bpm: number; /** * THE interaction. Off → start the radio; playing → tune the next station. * Wire this to the click/use — it is what a person does to a sound system. */ operate(): void; /** Start the radio (or the bed, where there is no radio to be had). */ play(station?: RadioStation | number): void; /** Silence, and the cones stop. */ stop(): void; /** Next station round the dial. */ tune(): void; /** The dial, both ways. `next()` is `tune()` with its partner. */ next(): void; prev(): void; /** Fired whenever the tuned station changes. Returns the unsubscribe. */ onStation(cb: (station: RadioStation) => void): () => void; /** The music, now. Same shape whoever has the floor. */ pulse(): AudioPulse; /** Overall drive 0–1 — feed it to a PA's `setProgram`. */ level(): number; /** dB(A) at a world point, while she plays. The AQ handshake, kept. */ levelAt(x: number, z: number): number; /** Fired on every kick. Returns the unsubscribe. */ onBeat(cb: () => void): () => void; update(dt: number): void; } /** * Four to the floor, hats on the off-beat, a mid line that moves — all a pure * function of accumulated time, so the same seed is the same night out and a * headless verifier sees the same frames a browser does. */ declare function bedPulse(t: number, bpm: number): { bass: number; mid: number; treble: number; }; declare function createWoofer(options?: WooferOptions): Woofer; interface DanceTilesOptions { seed?: number; cols?: number; rows?: number; /** Tile pitch, metres. Default 0.9. */ size?: number; } interface DanceTiles { object: Group; /** True once something has fed it a live pulse. */ readonly activated: boolean; /** Hand the floor this frame's music. */ feed(pulse: AudioPulse): void; /** Fade the lit pattern along. Call every frame. */ update(dt: number): void; /** How many tiles are lit past half right now. */ litCount(): number; } /** * The floor the woofer is for. A grid of emissive tiles: the kick throws a * ring out from the centre, the treble sparkles the corners, the mid sets * how warm the floor idles. It knows nothing about radios, networks or * states — it eats `AudioPulse` and that is the entire coupling, which is * why a dropout upstream never reaches it. */ declare function createDanceTiles(options?: DanceTilesOptions): DanceTiles; /** * The shala — a place to practice. * * A yoga centre is the quietest gathering this library has: no seats, no * table, no fire — a deck, a grid of mats, and an orientation. The * orientation is the point. Surya namaskar faces the sun, so the shala * takes a **sunrise bearing** and lays every student mat facing it, with * the instructor's mat out front facing back at the class — the same * geometry a class controller (ANIMA's `YogaClass`) produces on its own, * offered here as a *place* instead of a formula. * * ```ts * const shala = createShala({ era: 'retreat', students: 8, sunrise: 0.4 }); * shala.object.position.set(10, 0, -4); * scene.add(shala.object); * * // The handshake: one spot per mat, in WORLD space, instructor first. * for (const [i, spot] of shala.matSpots().entries()) { * rigs[i].object.position.set(spot.x, deckTop, spot.z); * rigs[i].object.rotation.y = spot.facing; * } * ``` * * ## Eras * * Like the PA stacks, the shala has eras — the same practice, four rooms: * * - **ashram** — sandstone deck between carved columns, a low ashlar wall * at the back, bronze finials. The oldest room. * - **studio** — parquet floor, a mirror wall with a barre. The room that * rents by the hour. * - **rooftop** — concrete pad, perimeter railing, string lights sagging * between the posts. The room with a skyline. * - **retreat** — teak planks under a bamboo pergola, planters at the * corners, open on every side. The room that is barely a room. * * ## The spots are world-space, on purpose * * `matSpots()` converts through the prop's current transform at call time: * move or rotate the shala and the spots move with it, facing included. * Index 0 is always the instructor's mat. The deck is walk-through * (`obstacleRadius` 0) — a platform is a floor, not an obstacle. */ type ShalaEra = 'ashram' | 'studio' | 'rooftop' | 'retreat'; declare const SHALA_ERAS: ShalaEra[]; interface ShalaOptions { seed?: number; /** Which room. Default: a seeded pick. */ era?: ShalaEra; /** Student mats. Default 8. */ students?: number; /** Mats per row. Default 4. */ perRow?: number; /** Bearing of the sunrise about +Y, radians — the class faces it. Default 0 (+Z). */ sunrise?: number; } /** One mat's stand-here, in world space. Index 0 is the instructor. */ interface MatSpot { x: number; z: number; /** World yaw to stand at. Students face the sunrise; the instructor faces them. */ facing: number; } interface Shala extends Prop { era: ShalaEra; /** One spot per mat, world-space, converted at call time. Instructor first. */ matSpots(): MatSpot[]; /** A point above the instructor's mat — aim the class's gaze here. */ focus: Object3D; /** Height of the deck surface above the prop's origin. */ deckTop: number; } declare function createShala(options?: ShalaOptions): Shala; /** * The singing bowl — the woofer's calm opposite. * * The woofer publishes the music as an `AudioPulse` and a floor full of * dancers answers it. The bowl publishes **breath**: a `BreathPulse` at a * tenth the frequency, with no beat edge at all — breath has turning * points, not kicks. Strike the bowl and the chime is the cue to breathe * in: the breath clock restarts at the inhale, the ring blooms and then * takes its long time dying away, and anything listening — a class, the * incense, the lanterns — settles onto the bowl's time. * * ```ts * const bowl = createSingingBowl({ seed: 4 }); * scene.add(bowl.object); * window.addEventListener('pointerdown', () => bowl.strike()); * * game.onUpdate((t) => { * bowl.update(t.delta); * const breath = bowl.pulse(); // { phase, inhale, rate, ring } * cls.instructor.slaveTo(breath.phase); // ANIMA: the class keeps bowl time * incense.setRate(0.25 + 0.6 * (breath.inhale ? 0.2 : 1)); // SCENA: ambience * }); * ``` * * ## The ring is long on purpose * * A struck bowl sings for tens of seconds — the decay IS the instrument. * `ringing` falls exponentially (about twelve seconds to a third), the rim * shivers visibly while it lasts, and the bowl's bronze warms with a faint * emissive glow that cools as the note dies. In a browser the strike also * *sounds*: two detuned partials synthesized on a lazily-created * AudioContext (created inside the strike, which is a user gesture, so * autoplay policy is satisfied by construction). Headless and in tests * there is no AudioContext and the bowl simply rings silently — the same * honest degradation as the woofer's bed. * * The pulse's `ring` field carries the envelope, so ambience can answer * the chime as well as the breath — a lantern that flares softly at the * strike and settles as the note does. */ /** What the breath says this frame. ANIMA-compatible by shape, as ever. */ interface BreathPulse { /** 0..1 — inhale over the first half, exhale the second. */ phase: number; inhale: boolean; /** Breaths per minute the clock is pacing. */ rate: number; /** The chime's envelope, 1 at the strike, 0 at silence. */ ring: number; } interface SingingBowlOptions { seed?: number; /** The pace the bowl keeps. Default 6 breaths a minute. */ breathsPerMinute?: number; /** Fundamental pitch, Hz. Default seeded 200–320 (a mid-size bowl). */ frequency?: number; /** Never synthesize audio, even in a browser. */ mute?: boolean; } interface SingingBowl extends Prop { /** Ring the bowl. The breath restarts at the inhale — the chime is the cue. */ strike(velocity?: number): void; update(dt: number): void; /** The whole coupling: read it once a frame and hand it to anything. */ pulse(): BreathPulse; /** The chime envelope, 0..1. */ readonly ringing: number; /** This bowl's fundamental, Hz. */ readonly frequency: number; onStrike(cb: () => void): () => void; /** The paced breath's turning points: 'inhale' | 'exhale'. */ onBreath(cb: (side: 'inhale' | 'exhale') => void): () => void; } declare function createSingingBowl(options?: SingingBowlOptions): SingingBowl; /** * The beach — the line where the water was a moment ago. * * Dry sand is terrain and the sea already ships; what makes a beach READ * is the strip between them. This prop owns that strip: the **swash** — * the tongue of water that runs up the sand and drains back — and the * memory it leaves. Sand where the water has just been is mirror-wet; it * dries through dark, damp and dry over half a minute; each retreat * strands a lace of foam at its high point; and the whole record is * queryable as **`wetAt(x, z)`** — the fifth spatial field, after * `depthAt`, `heatAt`, `chillAt` and `smokeAt`. * * ```ts * const beach = createBeach({ seed: 7, width: 46 }); * scene.add(beach.object); * const ocean = createOcean({ shore: beach.heightAt, wind }); // compose * game.onUpdate((t) => beach.update(t.delta)); * ``` * * ## One water, two directions * * The beach does not own the sea — it *asks* it. Pass any * `water(x, z, time)` (structurally `Ocean.heightAt`) and the swash runs * on the real swell; pass nothing and a seeded built-in swell drives it — * progressive along the shore (tongues run diagonally, as they do) and * modulated into **sets**, because waves arrive in families. Either way * the beach hands its `heightAt` back to the ocean's `shore` option, so * the two agree about where the land is. Neither imports the other. * * ## The sand remembers * * `stamp(x, z)` presses a mark into the sand — a footprint, a paw, a * dropped coconut. **Only wet sand takes a print** (try stamping dry dune * and it simply doesn't), and the next tongue that crosses a print wipes * it. Wire ANIMA's `loco.onFootstep` to `stamp` and characters write * their path along the beach while the sea edits it — that one coupling * is worth more than any ten props. * * `wrackLine()` reports the session's high-water mark per shore segment — * where the tide leaves its shells and kelp, and where 0.82's scatter * will put them. * * Local frame: X runs along the shore, +Z is seaward, the still-water * line sits at `z0` (about a sixth of the depth seaward of centre). All * public queries are **world-space** and ride the prop's transform. */ interface BeachOptions { seed?: number; /** Metres of shoreline (local X). Default 40. */ width?: number; /** Cross-shore extent (local Z). Default 24. */ depth?: number; /** Still-water level, world Y. Default 0. */ level?: number; /** Height of the back dune above sea level. Default 1.6. */ duneHeight?: number; /** Seconds fully-wet sand takes to dry. Default 30. */ dryTime?: number; /** * Water height at a world point and time — structurally `Ocean.heightAt`. * Default: a seeded built-in swell with along-shore progression and sets. */ water?: (x: number, z: number, time: number) => number; } interface Beach extends Prop { /** Sand height at a world point — hand this to `createOcean({ shore })`. */ heightAt(x: number, z: number): number; /** How wet the sand is at a world point, 0..1. The fifth field. */ wetAt(x: number, z: number): number; /** The water edge's current position at shore coordinate x, as a world point. */ reachAt(x: number): { x: number; z: number; }; /** * Press a mark into the sand. Only wet sand (wetAt > 0.15) takes a * print; returns whether it took. The next tongue over it wipes it. */ stamp(x: number, z: number, r?: number): boolean; /** Prints currently in the sand. */ readonly stamps: number; /** Foam scraps currently stranded. */ readonly foam: number; /** The session's high-water mark, one world point per shore segment. */ wrackLine(): Array<{ x: number; z: number; }>; update(dt: number): void; } declare function createBeach(options?: BeachOptions): Beach; /** The pitch: 22 yards stump to stump, 10 feet wide. */ declare const PITCH_LENGTH: number; declare const PITCH_WIDTH = 3.05; /** Stumps: 28 inches tall, 9 inches across all three. */ declare const STUMP_HEIGHT: number; declare const STUMP_SPREAD: number; /** The popping crease is 4 feet in front of the stumps. */ declare const CREASE_FRONT: number; interface CricketGroundOptions { seed?: number; /** Boundary radius in metres. Default 62 — a real, big field. */ boundary?: number; /** Grass colour. Default a mown green. */ grass?: number; /** Width of the mower's stripes, metres. Default 7. */ stripe?: number; } interface CricketGround extends Prop { /** Where the batter on strike stands (world space, on the crease). */ readonly strikerEnd: Vector3; /** Where the non-striker / bowler's end is. */ readonly bowlerEnd: Vector3; /** Boundary radius, metres. */ readonly boundary: number; /** * The base of the stumps at one end: `-1` is the striker's (the end * being bowled AT), `+1` the bowler's. */ stumpsAt(end: -1 | 1): Vector3; /** True when a point has crossed the rope. */ isBoundary(x: number, z: number): boolean; /** Knock the bails off the striker's stumps — a wicket, visibly. */ breakWicket(end?: -1 | 1): void; /** Put them back for the next batter. */ resetWicket(): void; update(dt: number): void; } declare function createCricketGround(options?: CricketGroundOptions): CricketGround; interface BatOptions { seed?: number; /** Blade length, metres. Default 0.58 (a full-size bat is ~0.85 overall). */ blade?: number; } /** A bat: willow blade, shoulders, splice and a bound handle. */ declare function createBat(options?: BatOptions): Prop; interface CricketBallProp extends Prop { /** The ball's own marker so a game can parent effects to it. */ readonly marker: Object3D; } /** A cricket ball: 72 mm, red, with a proud stitched seam. */ declare function createCricketBall(options?: { seed?: number; color?: number; }): CricketBallProp; /** * Tropical trees whose leaves are CLOTH. * * A palm frond is, mechanically, a flag pinned at the stem: fixed at one * edge, free at the fly, rippled by the air, drooping under its own * weight. So these trees borrow the banner machinery wholesale — every * leaf is a tapered plane driven by the shared cloth-wave shader, with a * seeded phase of its own so no two leaves flutter in step. Rigid-leaf * palms read as plastic; fabric reads as alive, which is the whole trick. * * ```ts * const palm = createPalm({ seed: 7, height: 6 }); * scene.add(palm.object); * game.onUpdate((t) => palm.update(t.delta)); * ``` * * Two species: * - **`createPalm`** — a coconut palm: curved trunk (the lean toward the * water is the whole silhouette), a crown of long serrated-feel fronds, * coconuts at the throat. * - **`createBananaTree`** — a banana plant: green pseudostem, huge * paddle leaves that arch up and over — each leaf built as THREE cloth * strips side by side, because banana leaves split along their veins * and the strips fluttering out of phase with each other IS that split. */ interface TropicalTree extends Prop { /** Advance the leaves' cloth. */ update(dt: number): void; /** World-ish height of the crown/stem top, for dressing. */ crownY: number; } interface PalmOptions { seed?: number; /** Trunk height to the crown. Default seeded 4.5–6.5. */ height?: number; /** Sideways lean of the whole trunk, radians. Default seeded 0.1–0.3. */ lean?: number; /** Fronds in the crown. Default 9. */ fronds?: number; /** Coconuts. Default seeded 2–4. */ coconuts?: number; } interface BananaOptions { seed?: number; /** Pseudostem height. Default seeded 1.6–2.4. */ height?: number; /** Leaves. Default 6. */ leaves?: number; /** Hang a bunch of bananas. Default seeded (about half of them). */ fruiting?: boolean; } declare function createPalm(options?: PalmOptions): TropicalTree; declare function createBananaTree(options?: BananaOptions): TropicalTree; /** * The lagoon — the turquoise pool the postcard is actually of. * * Not surf: a big, calm, SWIMMABLE basin of clear water — the sheltered * pool behind the reef, with the open ocean out on the horizon where it * belongs. The build is three honest layers: a sandy **bowl** (visible * through the water, because clear water IS its bottom), a **surface** * whose colour runs pale at the rim to deep turquoise over the middle, * gently rippling, and **fish** — small, colourful, and busy, each on a * seeded circuit of its own, wiggling as it goes. * * ```ts * const lagoon = createLagoon({ seed: 7, radius: 9 }); * scene.add(lagoon.object); * game.onUpdate((t) => lagoon.update(t.delta)); * ``` * * ## Swimmers drop straight in * * The lagoon is structurally ANIMA's `WaterBody` — `surfaceY`, * `depthAt(x, z)`, `disturb()` — so a `Swimming` character needs no * adapter at all: hand them the lagoon and they swim in it. `depthAt` is * world-space and rides the prop's transform, like every SCENA field. * * The outline is organic — a seeded radial wobble, never a circle — and * the bowl is deepest a little off-centre, the way real lagoons are. */ interface LagoonOptions { seed?: number; /** Mean radius of the pool, metres. Default 9. */ radius?: number; /** Depth at the deep point, metres. Default 1.8. */ depth?: number; /** Water level, local Y. Default 0. */ level?: number; /** Fish in the water. Default 14. */ fish?: number; } interface Lagoon extends Prop { /** ANIMA `WaterBody`, structurally: the water's local surface height. */ surfaceY: number; /** Water depth at a world point, metres; 0 outside the pool. */ depthAt(x: number, z: number): number; /** A ripple hook (a swimmer's kick). Accepted, gently ignored for now. */ disturb(x: number, z: number, strength?: number): void; update(dt: number): void; } declare function createLagoon(options?: LagoonOptions): Lagoon; /** * The beach kit — Miami. * * The props that turn sand into a BEACH: the art-deco lifeguard stand, * the striped umbrella, the lounger. Miami Beach's lifeguard towers are * the reason this file has a palette: they are pastel geometric huts on * stilts, no two the same colour, and a beach with a row of them is * unmistakably that beach. * * ```ts * const tower = createLifeguardTower({ seed: 3 }); * const shade = createBeachUmbrella({ seed: 4 }); * const chair = createLounger({ seed: 5, recline: 'reading' }); * ``` * * Cloth wherever cloth belongs: the tower's pennant and the umbrella's * valance are driven by the shared cloth-wave shader (the same one * behind the flags and the palm fronds), so the beach moves even when * nothing is happening. */ /** The Ocean Drive palette: pastels that only look right in that light. */ declare const MIAMI_COLORS: number[]; interface LifeguardTowerOptions { seed?: number; /** Deck height above the sand. Default seeded 1.5–2.2. */ height?: number; /** Body colour. Default: a seeded Miami pastel. */ color?: number; /** Trim/roof colour. Default: a contrasting seeded pastel. */ trim?: number; /** Fly a surf pennant. Default true. */ pennant?: boolean; } interface BeachUmbrellaOptions { seed?: number; /** Canopy radius. Default seeded 1.1–1.5. */ radius?: number; /** Pole height to the hub. Default seeded 2.0–2.4. */ height?: number; /** The two stripe colours. Default: seeded Miami pair. */ colors?: [number, number]; /** Lean off vertical, radians. Default seeded ±0.14. */ tilt?: number; } type LoungerRecline = 'flat' | 'reading' | 'upright'; interface LoungerOptions { seed?: number; /** Back angle preset. Default 'reading'. */ recline?: LoungerRecline; /** Frame colour. Default seeded pastel. */ color?: number; /** Towel over the bed. Default seeded (about half). */ towel?: boolean; } interface BeachProp extends Prop { update(dt: number): void; } declare function createLifeguardTower(options?: LifeguardTowerOptions): BeachProp; declare function createBeachUmbrella(options?: BeachUmbrellaOptions): BeachProp; declare function createLounger(options?: LoungerOptions): BeachProp; /** * Seamarks — the first thing in this library whose entire purpose is **to be * seen from somewhere else.** * * Everything up to here has been a thing that *is*: a hull that floats, a * boiler that makes steam, a net that comes fast. A light is none of those. It * does nothing where it stands. Its whole function happens fifteen miles away * in somebody else's eye, and every number on it is really a number about the * observer. * * ## The curvature of the earth decides it, and the lamp does not * * This is the truth the module exists for, and it is the least intuitive one * in the whole trilogy. A light has two ranges and you get the SMALLER: * * - the **geographic** range, where it drops below the horizon — a function of * how high the light is and how high your eye is, and of nothing else at all; * - the **luminous** range, where it gets too faint to see — a function of the * lamp and the visibility. * * ```ts * const light = createSeamark({ kind: 'flashing' }); // 40 m, 200 000 cd * light.sightedFrom(x, z, 12).range; // 18.5 nm — the lamp is the limit * ``` * * …so make the lamp bigger. Double it, and again, and again: * * ```ts * 200 000 cd -> seen at 18.5 nm * 400 000 cd -> seen at 20.2 nm * 800 000 cd -> seen at 20.4 nm * 1 600 000 cd -> seen at 20.4 nm * 20 000 000 cd -> seen at 20.4 nm ← a hundredfold lamp. Under two miles. * ``` * * The horizon does not negotiate. Past that point the only thing that buys * range is **height** — of the tower, or of the eye looking for it, and that is * why lighthouses are on cliffs and why the answer to "we cannot see it" was * never a bigger lamp. * * ## The same light, the same night, and two boats that see differently * * `heightOfEye` is not a detail. A man standing in an open boat has his eye * about 1.5 m up; the officer on a ship's bridge has his at 12 m. They are * looking at the same lamp: * * ```ts * light.sightedFrom(x, z, 1.5).range; // 15.7 nm, and HORIZON-limited * light.sightedFrom(x, z, 12).range; // 18.5 nm, and LAMP-limited * ``` * * Not only a different range — a different *reason*. And it inverts: with a * feeble light, raising your eye buys nothing whatever, because you were never * near the horizon to begin with. * * ## What tells you it is that light and not another one * * The era axis, and it is about **identity** rather than power: * * | kind | how you know which light it is | * | --- | --- | * | `bonfire` | you do not. It is a fire on a headland, and so is a burning house. | * | `harbour` | by where it is. A fixed light is a fixed light, and ships were lost mistaking one for another. | * | `flashing` | by its **character** — `Fl(3) 15s` is a name you can look up. | * | `sectored` | it tells you where **you** are: white in the fairway, red over the rocks. | * * The last one is the inversion at the end of the axis, the same shape as a * gyro stabiliser that needs no way from you and a self-righting boat that * needs no crew: a sectored light does the navigating instead of you. You do * not take a bearing off it. You look at its colour. * * ```ts * const s = light.sightedFrom(x, z, 4); * s.sector; // 'red' * s.safe; // false — and there is nothing else to work out * ``` */ type MarkKind = 'bonfire' | 'harbour' | 'flashing' | 'sectored'; declare const MARK_KINDS: MarkKind[]; /** * What she has of it, as she comes up on it. * * This is the real sequence, and it is four-stated like everything else here — * `'loom'` is the one people forget. A light is seen in the sky for miles * before it is seen at all: the beam lights the haze above the horizon while * the lamp itself is still under it. * * `'raising'` is the narrow band where the lamp sits ON the horizon, and it is * not a curiosity — standing up brings it in sight and crouching puts it out, * and that gives a distance. It is the one navigational fix in this library * that costs nothing but knowing how tall you are. */ type SightState = 'dark' | 'loom' | 'raising' | 'showing'; interface Sector { name: string; /** * Bearings **outward from the light** — the direction you are, seen from * the tower. * * Charts quote sector limits the other way round, as bearings *from * seaward*, and the two differ by 180°. Take one for the other and the red * sector lands squarely over the fairway, which is a way of putting a ship * on the rocks with entirely correct arithmetic. */ from: number; to: number; colour: 'white' | 'red' | 'green'; /** Fraction of the lamp that gets through the glass. */ transmission: number; /** True where this is the water you want to be in. */ safe: boolean; } /** Coloured glass eats light, and this is how much of it. */ declare const SECTOR_TRANSMISSION: Record<'white' | 'red' | 'green', number>; interface Sighting { /** * Close enough to see it — whether or not it happens to be lit this instant. * * This is the question charts and passage plans ask. `visible` is the other * one, and for a flashing light the two disagree most of the time: `Fl(3) * 15s` is DARK for eleven and a half seconds out of every fifteen, so a * caller testing `visible` once a frame sees a light that is mostly not * there. Both are true statements about the same lamp. */ inRange: boolean; /** In range AND lit at this instant. */ visible: boolean; state: SightState; /** Metres from the observer to the tower. */ distance: number; /** Radians, from the observer to the light, clockwise from north (−z). */ bearing: number; /** How far she could see it from there, m — the SMALLER of the two. */ range: number; /** Where it drops below the horizon, m. The lamp has no say in this. */ geographic: number; /** Where it gets too faint, m. The horizon has no say in this. */ luminous: number; /** Which of the two is doing the limiting — and it changes with her eye. */ limitedBy: 'horizon' | 'lamp'; /** Which sector she is in, or `null` on a light that has none. */ sector: Sector | null; /** In the water the light says is good. `null` where it does not say. */ safe: boolean | null; /** Is the lamp lit this instant? A flashing light is dark most of the time. */ showing: boolean; } interface Seamark extends Prop { kind: MarkKind; /** Focal plane above sea level, m. THE number for geographic range. */ readonly height: number; /** Candela on the white bearing. */ readonly intensity: number; /** `'Fl(3) 15s'`, `'Oc 8s'`, `'F'` — the name of its rhythm. */ readonly character: string; /** Seconds for one complete character. `Infinity` for a fixed light. */ readonly period: number; /** Lit this instant. */ readonly showing: boolean; /** 0–1 through the character. */ readonly phase: number; /** * Can she tell it from another light? * * False for a bonfire, which is a fire like any other fire, and false for a * plain fixed light, which is why characters were invented at all. */ readonly identifiable: boolean; /** On a chart at all, with a name and a daymark. A bonfire is not. */ readonly charted: boolean; /** What she has of it from there. THE method. */ sightedFrom(x: number, z: number, heightOfEye?: number): Sighting; /** * The range at which a given eye raises it, m. * * Also a position line: see the lamp sitting on the horizon and you know how * far off you are, to within the accuracy of knowing your own height. */ dips(heightOfEye: number): number; /** Where it becomes too faint, m, in the visibility she is in. */ luminousRange(intensity?: number): number; /** Meteorological visibility, nautical miles. Straight out of the weather. */ setVisibility(nauticalMiles: number): void; readonly visibility: number; /** Add a sector, in bearings OUTWARD from the light. */ sector(name: string, from: number, to: number, colour: 'white' | 'red' | 'green'): void; sectorAt(bearing: number): Sector | null; readonly sectors: Sector[]; /** * Draw the sectors on the water. * * Off by default, and it is a **chart made visible**: nothing at sea looks * remotely like this. The point of the picture is that the boat can see one * colour at a time and the chart can see all of them. * * `scale` is there because a sector arc is thirteen nautical miles long and * the tower it comes out of is twenty-five metres tall. No frame holds both, * and that is not a limitation of the renderer — it is the subject. Drawn to * a fraction the RATIOS survive, which is what carries the one claim the * picture is for: the red arc is shorter than the white one. */ showSectors(on: boolean, scale?: number): void; /** When you cannot see it at all. */ readonly fogSignal: number; readonly sounding: boolean; /** * Audible out to here, m — and it is the least trustworthy number on the * object. Sound goes over the top of you, round headlands, and into silent * sectors close under the station itself. */ readonly audibleRange: number; station: PropSlot; slots: PropSlot[]; update(dt: number): void; } interface SeamarkOptions { kind?: MarkKind; /** Focal plane above the sea, m. */ height?: number; /** Candela. */ intensity?: number; /** Starting meteorological visibility, nautical miles. */ visibility?: number; seed?: number; palette?: Palette; } /** Metres in a nautical mile. */ declare const NM = 1852; /** * Geographic range in METRES: how far a light of height `H` is visible to an * eye of height `h`, both in metres. * * `2.08(√H + √h)` nautical miles. The 2.08 rather than a bare geometric 1.93 is * terrestrial refraction — the atmosphere bends light down around the curve and * hands you about eight per cent more range than the geometry alone allows. */ declare function geographicRange(height: number, heightOfEye: number): number; /** * Luminous range in METRES, by Allard's law, solved for the distance at which * a light of `intensity` candela falls to the night threshold in `visibility` * nautical miles of air. * * `E = I·e^(−σd) / d²`. There is no closed form for `d`, so it is bisected — * eighty halvings, which is exact to the width of an atom and costs nothing. */ declare function luminousRange(intensity: number, visibility: number): number; declare function createSeamark(options?: SeamarkOptions): Seamark; /** * Small craft — the only vessel in this library whose stability walks around, * and the only one that can be lost and come back. * * Everything else in the boat arc is a machine that survives things. A liner * takes a gale because she is a hundred and eighty metres long; a steamer * takes it because she has a thousand tonnes of cargo holding her down. A * small boat has neither, and what happens to her in the next thirty seconds * is decided by half a metre of freeboard and by where four people are * sitting. * * ## She is not lost to stability. She is lost to freeboard. * * This is the finding, and it is not what anybody expects — including the * first draft of this module, which took the free-surface sum straight out of * `createHold` and got a negative metacentric height out of **two buckets of * water**. That formula is a full-beam slab, derived for a ballast tank six * metres wide with a metre of water standing in it. Water in the bottom of a * boat lies in the *narrow* part of her section, and the width that matters is * the width at that depth. Taken seriously, she keeps a positive GM all the * way to the gunwale. * * What actually happens is a **runaway**: * * ```text * water aboard → less freeboard → more water aboard * ``` * * Nothing else in this library does that. Every other model here settles: a * boiler finds a pressure, a sea finds a height, a hull finds a list. This one * has a tipping point, and on the wrong side of it there is nothing to find. * * ```ts * boat.meet(0.8); boat.swampsIn(); // Infinity — she is dry all day * boat.meet(1.0); boat.swampsIn(); // 79 s * boat.meet(1.5); boat.swampsIn(); // 23 s * boat.bail(2); boat.swampsIn(); // 23 s. A man with a bucket is not in it. * ``` * * ## What happens after she fills * * The era axis is **where in that loop you intervene**, and every one of the * four intervenes somewhere different: * * | fit | what it does about the runaway | * | --- | --- | * | `open` | nothing. You bail, and you lose, and then she goes under. | * | `buoyant` | cannot stop it — puts a FLOOR under it. She floods to awash and stays there. | * | `selfDraining` | breaks it. Water out faster than water in, so the freeboard never falls. | * | `selfRighting` | lets it finish and comes back anyway, with nobody doing anything. | * * `buoyant` is the interesting one, and the numbers say something sharper than * the usual claim for it: **it buys no seconds at all.** She fills marginally * SOONER than an open boat, because the tanks take up room the water would * have had. What changes is what is still floating when she is full — and * turning drowning into swimming is the biggest single step on this list even * though it does not buy one second of it. * * And `selfRighting` is the inversion at the end of the axis, the same shape as * a gyro stabiliser that needs no way and a derrick that cannot let go: it * makes the crew's position **stop mattering**. Every other fit here is a boat * you have to be good in. * * ## A breaker does not care what her GM is * * A sea steeper than one in seven is breaking, and a breaking sea taller than * about six tenths of her beam rolls her over regardless of stability, because * it is not a heeling moment — it is a wall of water with momentum in it. It * is the only failure in this library that no number on the vessel answers. * * ```ts * boat.meet(1.4, 9); // 1.4 m at 9 m long: steepness 1 in 6.4, and breaking * boat.breaking; // true * boat.capsized; // true, and her GM was 3.6 * ``` */ type CraftFit = 'open' | 'buoyant' | 'selfDraining' | 'selfRighting'; declare const CRAFT_FITS: CraftFit[]; /** dry / taking it / full / foundered, on the same four-state shape as the rest. */ type CraftState = 'dry' | 'wet' | 'awash' | 'gone'; /** Somebody aboard, and on a boat this size they are a third of her. */ interface Hand { name: string; /** Kilograms. */ kg: number; /** Where along her, −1 hard aft to +1 hard forward. */ along: number; /** Where across her, −1 on the port gunwale to +1 on the starboard one. */ side: number; /** On their feet — which puts their weight most of a metre higher up. */ standing: boolean; /** Out over the side, 0–1. It multiplies whichever arm they already have, * and which side that is decides whether it saves her. */ out: number; } interface SmallCraft extends Prop { fit: CraftFit; readonly length: number; readonly beam: number; /** Keel to gunwale, m. Freeboard is this minus her draught. */ readonly depth: number; /** * Put somebody aboard. `along` is −1 aft to +1 forward, `side` is −1 to +1 * across her. * * Their weight is a third of her displacement, so this moves her draught, * her trim, her list and her metacentric height all at once — and they can * do it again next second, which is what makes a small boat a boat you have * to be good in. */ seat(name: string, kg: number, along?: number, side?: number): void; /** Move somebody who is already aboard. */ move(name: string, along: number, side: number): void; /** On their feet. Their centre of mass goes up most of a metre. */ stand(name: string, up: boolean): void; /** * Out over the gunwale, 0–1. * * It multiplies the arm they already have, and it does not care which way * that arm points: hiking out on the high side is the only time the crew is * stability rather than a problem, and hiking out on the low side puts her * over twice as fast. The sign is the skill. */ hike(name: string, out: number): void; leave(name: string): void; readonly hands: Hand[]; readonly crew: number; readonly crewMass: number; /** * The sea she is in: height in metres, and the wavelength if you have it. * * `SeaState`'s trains go straight in — `boat.meet(sea.windSea.height, * sea.windSea.length)` — with nothing imported either way. Given a length * she works out for herself whether it is **breaking**, which is the one * thing that can roll her whatever her stability is. */ meet(height: number, length?: number): void; readonly sea: number; /** Steeper than one in seven, and it is a wall rather than a slope. */ readonly breaking: boolean; /** Kilograms of water in her. */ readonly water: number; /** Kilograms she would hold to the gunwale. */ readonly capacity: number; /** Coming aboard right now, kg/s. It GROWS as her freeboard falls. */ readonly boarding: number; /** Bail at this many kg/s — a bucket is about 2, a hand pump about 1.5. */ bail(kgPerSecond: number): void; readonly bailing: number; /** Going out again by itself, kg/s. Zero unless she is self-draining. */ readonly draining: number; /** More coming in than everything she has can put out. The runaway has * started, and on the wrong side of this it does not stop. */ readonly swamping: boolean; /** * Seconds until she is FULL, in the sea she is in now — or `Infinity` if she * can live in it. * * Full is not the same as lost, and the difference is the whole era axis: * read `state` when this expires. A buoyant boat fills marginally SOONER * than an open one, because her tanks take up room the water would have * had — buoyancy buys no seconds whatever. What it changes is what is * floating there at the end of them. * * The same idiom as `SeaState.fallsTo` and `SteamPlant.reach`: run the model * forward coarsely and say when, rather than making the caller integrate it * themselves to find out. */ swampsIn(): number; /** Empty her. */ dry(): void; readonly displacement: number; readonly draught: number; /** * Metres of side above the sea AT HER LOWEST RAIL — which is not the same * as amidships on the centreline once she is trimmed or listed. Four people * sitting in the stern cost her half of it, and that is how a boat is * swamped from astern. */ readonly freeboard: number; readonly gm: number; readonly solidGm: number; readonly freeSurface: number; readonly rollPeriod: number; readonly state: CraftState; onState?: (state: CraftState) => void; readonly capsized: boolean; capsize(): void; /** * Get her back up. * * On an `open` boat that leaves you with a boat full of water. On a * `buoyant` one, a boat floating awash. On a `selfDraining` one she empties * herself afterwards. And on a `selfRighting` one you never call this at * all, because she has already done it. */ right(): void; /** The AK handshake, unchanged: trim, list, sink, stiffness. Hand it to * anything that takes a `ShipInput`. */ readonly loading: Loading; deckAt(x: number, z: number): number | null; normalAt(x: number, z: number): Vector3; ride(position: Vector3): Vector3; /** Bind the water: `ocean.heightAt`. */ float(heightAt: (x: number, z: number) => number): void; helm: PropSlot; slots: PropSlot[]; update(dt: number): void; } interface SmallCraftOptions { fit?: CraftFit; /** Overall length, m. */ length?: number; beam?: number; /** Keel to gunwale, m. */ depth?: number; /** Hull mass with nothing in her, kg. */ light?: number; seed?: number; palette?: Palette; } declare function createSmallCraft(options?: SmallCraftOptions): SmallCraft; /** * The sea a boat of this freeboard can live in, metres. * * Half her freeboard is the whole criterion, and it is worth having on its own * because it is the number that decides whether a passage is a passage or a * drowning — and because it does not mention her length, her engine, her crew * or her stability, none of which come into it. */ declare function livesIn(freeboard: number): number; /** Is a sea of this height and length breaking? Steeper than one in seven. */ declare function isBreaking(height: number, length: number): boolean; /** * Working gear — and the first load in this library that **pulls back.** * * Every other force in the boat arc acts through her centreline. A sail's * drive, an oar's thrust, a screw's push: all of them push her along the way * she is pointing, and none of them can put her on her beam ends. A working * load does not. It acts at a point on her deck, at the end of a wire, and the * further outboard and the higher that point is, the more of your own engine * goes into laying her over instead of moving her. * * `object` stands on her WORKING DECK: y = 0 is the planking, not the * waterline. Hang it off the deck it belongs to — * `gear.object.position.y = deck.y` — the same way a funnel is hung. Left at * the hull's own origin every gallows, hook and boom is a freeboard too low, * which is to say inside her, and the load and the wire are under the sea. * * ```ts * const gear = createGear({ kind: 'tow', beam: ship.beam, length: ship.length }); * const deck = ship.decks.find((d) => d.name === 'waist')!; * gear.object.position.y = deck.y; * ship.object.add(gear.object); * * gear.shoot(); * game.onUpdate((t) => { * gear.setWay(plant.way); * gear.setAngle(towAngle); // …and this is the one that kills you * gear.update(t.delta); * hold.heel('gear', gear.moment); // straight into the same arithmetic * ship.update(t.delta, { speed: plant.way - gear.drag, loading: hold.loading }); * }); * ``` * * ## The wire comes abeam and the boat is gone * * A tug tows from a hook near her own centre of turning, as low as she can get * it, and she is still lost if the line comes across her. It is called * **girting**: the tow's weight comes on the quarter, the pull is behind her * pivot so her rudder cannot bring her back, and she goes over. Every tug ever * built has a way of letting the wire go *instantly*, and that is the only * reason there is a `slip()` on this object. * * ```ts * gear.girting; // true, and you have seconds * gear.slip(); // a tow hook lets go NOW. A derrick cannot let go at all. * ``` * * ## How fast you can get rid of it is not a modern invention * * | kind | the load | how it kills you | letting go | * | --- | --- | --- | --- | * | `pots` | a string of pots on the rail | weight outboard, hauled by hand | drop it | * | `trawl` | a net towed astern | it comes fast on the bottom | knock out the block | * | `tow` | another vessel | it comes abeam — girting | INSTANT, by design | * | `derrick` | a weight in the air | it acts at the boom head the instant it lifts | you cannot | * * The axis is **how fast you can be rid of it**, and it is not monotone with * era: the most capable gear here is the one with no way out. A derrick's load * has to be put down somewhere, and putting it down takes as long as it takes. */ type GearKind = 'pots' | 'trawl' | 'tow' | 'derrick'; declare const GEAR_KINDS: GearKind[]; /** rest / transitioning-toward / at-target / drifting-back, on the gear. */ type GearState = 'stowed' | 'shooting' | 'working' | 'fast'; interface Gear extends Prop { kind: GearKind; /** Put it over the side. */ shoot(): void; /** Get it back aboard. */ haul(): void; /** * LET GO. * * The one verb on this object that exists because of a way of dying. A tow * hook does it in an instant; a trawl takes a few seconds to knock the block * out; a derrick cannot do it at all and `slip` is a no-op — the load has to * be **lowered**, and that no-op is the era axis. */ slip(): void; /** 0 all inboard, 1 all the way out. It travels. */ readonly out: number; readonly state: GearState; onState?: (state: GearState) => void; /** Her way through the water, m/s. The load pulls back harder the harder * you drive her. */ setWay(speed: number): void; readonly way: number; /** Where the wire lies, radians from dead astern. Positive to starboard. */ setAngle(radians: number): void; readonly angle: number; /** For a derrick: how far outboard the boom head is swung, m. */ setOutreach(metres: number): void; readonly outreach: number; /** For a derrick: the weight on the hook, tonnes. */ setLoad(tonnes: number): void; /** Tension in the wire, tonnes. */ readonly strain: number; /** What she can pull before the wire is simply dragging her backwards. */ readonly bollardPull: number; /** HEELING MOMENT, tonne·metres, positive to starboard. Hand it straight to * `Hold.heel`. */ readonly moment: number; /** Speed the gear is costing her, m/s. */ readonly drag: number; /** Where the wire leaves her, in vessel metres. THE lever arm, and the * whole design of a tug is about getting it low and near her middle. */ readonly lead: Object3D; /** * THE OTHER END PULLS. A tow sheers, a net snags a wreck, a load swings off * a barge in a swell — and for a few seconds the wire carries several times * anything she could put on it herself. * * Nothing else in this module can capsize a properly built boat. This can, * and it is the reason a tow hook opens. */ snatch(tonnes: number): void; /** What the other end is adding, tonnes. Decays over a few seconds. */ readonly surge: number; /** She is being pulled over by her own gear. */ readonly girting: boolean; /** The load has come fast — foul of the bottom, or a tow that will not * come. Strain goes to everything she has and stays there. */ readonly fast: boolean; comeFast(): void; clear(): void; station: PropSlot; slots: PropSlot[]; update(dt: number): void; } interface GearOptions { kind?: GearKind; /** Her beam, m — the lead points are placed off it. */ beam?: number; /** Her length, m. */ length?: number; /** Height of her working deck above the water, m. */ freeboard?: number; /** What she can pull, tonnes. Sizes everything else. */ bollardPull?: number; /** Start with it over the side. */ shot?: boolean; seed?: number; palette?: Palette; } declare function createGear(options?: GearOptions): Gear; /** * The list a heeling moment gives a vessel, radians — for a boat with no hold * to hand it to. * * `asin(M / (Δ · GM))`, and it returns the angle of vanishing stability if the * moment is more than she can answer, because past that there is no * equilibrium at all. The same sum `createHold` does, published for the small * craft that do not carry a cargo model around with them. */ declare function listFor(moment: number, displacement: number, gm: number, vanishing?: number): number; /** * Vessels you can stand on — the deck as ground that moves. * * `createBoat` and `createShip` already exist and are hulls that **bob**: * bind a water sampler, ride the waves, seat somebody at the helm. This is * the other half, and it is a different problem. Past about ten metres a * ship stops being a vehicle and becomes a **place** — somewhere with work * and rooms and other people, that happens to be moving. * * Every character controller in the trilogy assumes the floor is the world. * `terrain.heightAt` never moves. A deck pitches, rolls **and translates**, * so the fact that breaks everything is this: * * > A character standing perfectly still on a moving deck has to change * > world position anyway. * * Nothing in ANIMA or GAMA does that, and no amount of walking code fixes * it, because the character is not walking. So the handshake is a pair — * one query that mirrors `terrain.heightAt`, and one that has no equivalent * anywhere in the library: * * ```ts * deckAt(x, z): number | null // walkable height in WORLD space * ride(position): Vector3 // and where that point goes next * ``` * * `ride` is the whole track. It is one matrix multiply: the inverse of the * vessel's transform last frame, times its transform this frame. Feed a * standing character through it and they come along; do not, and they walk * out through the stern at whatever speed the ship is making. * * ```ts * const ship = createVessel({ era: 'carrack' }); * ship.float((x, z) => ocean.heightAt(x, z)); * game.onUpdate((t) => { * ship.update(t.delta, { speed: 4 }); * ship.ride(sailor.position); // carried by the deck * sailor.position.y = ship.deckAt(sailor.position.x, sailor.position.z) ?? 0; * }); * ``` */ type ShipEra = /** An open oared galley: one low deck, no rail, and it moves like a leaf. */ 'galley' /** A carrack: a waist between a raised fo'c'sle and poop, ladders between. */ | 'carrack' /** A steamer: flush deck, rails, a superstructure amidships. */ | 'steamer' /** A liner: several decks, high freeboard, and a motion you barely feel. */ | 'liner'; /** * Ground that moves, in **world** coordinates. * * The fifth spatial handshake, after `depthAt`, `heatAt`, `chillAt` and * `smokeAt` — and the first one that is not a *reading* but a *frame*. The * others answer "what is it like here". This one answers "where is here * going". */ interface DeckField { /** * Walkable height at a world (x, z), or **null** if that point is not over * a deck at all — which is how you test whether somebody is aboard, with * no separate `contains`. * * `near` picks between stacked decks: the walkable surface nearest below * it, so a sailor in the hold does not get teleported to the poop. */ deckAt(x: number, z: number, near?: number): number | null; /** The deck's up vector at a world point — it is not (0,1,0) at sea. */ normalAt(x: number, z: number): Vector3; /** * Carry a world point along with the vessel's own motion, in place. * * Call it every frame on anything standing on the deck, **after** * `update`. Returns the same vector for chaining. */ ride(position: Vector3): Vector3; } /** One walkable level. */ interface DeckLevel { /** Free label: 'waist', 'poop', 'promenade', 'hold'. */ name: string; /** Height above the vessel's origin, in vessel space. */ y: number; /** Extent along the vessel's z (fore–aft) and x (beam). */ length: number; beam: number; /** Centre along z, in vessel space. */ z: number; } /** A way up: structurally ANIMA's `Climbable`, like the pool ladder. */ interface Companionway { bottom: Object3D; top: Object3D; rungSpacing: number; } interface ShipInput { /** Way through the water, m/s. */ speed?: number; /** Rate of turn, radians/s. */ turn?: number; /** * Motion that is NOT along her own heading, m/s in world x and z. * * A vessel making way goes where she is pointing; a vessel being set by a * tide, blown down onto a wall, or held off it by her own mooring lines * does not, and there is no value of `speed` and `turn` that says so. It * is applied inside `update` rather than by the caller writing `position` * afterwards, because everything `ride` does depends on the frame delta * being taken across ALL of a frame's movement — a ship warped sideways * after her own update leaves her crew standing where she used to be. */ drift?: { x: number; z: number; }; /** * How she is loaded — trim, list, sinkage, and how hard she snaps back. * * Structurally `Hold.loading`, and duck-typed rather than imported so the * hull knows nothing about cargo. It is the first thing in this channel * that is a **state of the vessel** rather than a force on her: a drift * stops when the tide slackens, and a list does not stop. * * It is a BIAS on the sea-driven attitude and not a target for it. The * hull eases toward the waves because she has mass; she does not ease * toward her own trim, because her trim is not somewhere she is going. */ /** * How much of her ROLL something is taking out, 0 (nothing) to 1 (all of it). * * Structurally `Stabilisers.damping`, duck-typed like everything else in * this channel. It touches the roll and NOTHING ELSE — fins are wings that * work athwartships, and a stabilised ship in a head sea pitches exactly as * hard as an unstabilised one. That is not a simplification; it is the * commonest complaint about them. */ damping?: number; loading?: { /** Positive is DOWN BY THE HEAD, the way it is said aboard. */ trim?: number; /** Positive is a list to STARBOARD. */ list?: number; sink?: number; stiffness?: number; }; } interface DeckedShip extends Prop, DeckField { era: ShipEra; length: number; beam: number; /** Height of her main rail above the waterline — where lines are led. */ freeboard: number; /** * How deep she is DRAWN, m: keel to waterline, as her hull mesh was built. * * Published because anything that makes her float higher or lower has to * measure from the same datum she was drawn to. A hold that measures its * sinkage from its own load line instead lifts the whole ship clear of the * sea by the difference, and every number about her stays correct. */ draft: number; decks: DeckLevel[]; ladders: Companionway[]; /** Where somebody steers. */ helm: PropSlot; /** Bind the sea: `ocean.heightAt`, or a flat level. */ float(heightAt: (x: number, z: number) => number): void; /** Live attitude, radians. */ readonly pitch: number; readonly roll: number; /** * How hard it is to stand up right now, 0 (alongside) to 1 (hang on). * * Derived from the RATE of change of attitude and the heave, not from the * attitude itself. A vessel heeled steadily at ten degrees under sail is * easy to walk on; the same ten degrees arriving twice a second is not, * and a number taken off the angle cannot tell those apart. */ readonly motion: number; /** * How hard it is to stand up AT A POINT on her, 0–1. A FIELD — the fourth * in the trilogy, after `heightAt`, `depthAt` and the rest. * * `motion` is one number for the whole ship. This one is not, and the * difference is the entire layout of a liner. Her pitch throws the bow and * the stern up and down and leaves amidships almost alone; her roll throws * the high decks and the wings of the bridge about and leaves the * centreline low down almost alone. So the quietest place aboard is * **amidships and low**, and that falls out of two lever arms rather than * out of a price list — which is nonetheless exactly what a price list for * cabins looks like. * * In vessel-local metres: `x` athwartships, `z` fore-and-aft, `y` above * the waterline. */ motionAt(x: number, z: number, y?: number): number; /** Vertical speed of the deck under a point, m/s. What `motionAt` is made * of, published because a number in m/s is a thing you can reason about * and a number in 0–1 is not. */ heaveAt(x: number, z: number, y?: number): number; update(dt: number, input?: ShipInput): void; } interface DeckedShipOptions { era?: ShipEra; seed?: number; palette?: Palette; color?: number; } /** * A vessel with decks you can stand on. * * The origin is at the waterline, amidships, with **+z forward** — matching * the existing watercraft, so a hull and a vessel are interchangeable to * anything that only wants to float something. */ declare function createDeckedShip(options?: DeckedShipOptions): DeckedShip; declare const SHIP_ERAS: ShipEra[]; /** * Cold storage: the larder-to-freezer axis. * * The mirror of the heat track, and the differences are the interesting * part. Heat is a **surface** you put a pot on top of; cold is a **volume** * you put food inside, and the whole of it leaks the moment you open the * door. So the handshake gains a y: * * ```ts * chillAt(x, y, z): number // °C at a world point, ambient outside * ``` * * alongside `heatAt(x, z)` and `depthAt(x, z)`. It reports **°C**, not a * 0–1 dial, because unlike a fire's output there is a real scale here and * the entire game is played against thresholds on it: four degrees keeps * milk, minus eighteen keeps it for a year, and the line between them is a * phase change rather than "a bit colder". * * The era is not a finish. It is *what you have to do to keep the cold in*: * * - a **larder** has no mechanism at all and simply sits a few degrees under * the room, so on a hot day it does nothing and that is the point; * - an **icebox** spends a block of ice, faster the more heat it absorbs — * the fuel loop from the hearth, running backwards; * - a **fridge** holds a setpoint, cycling a compressor you cannot see; * - a **freezer** holds one far below zero and slowly ices itself up until * somebody defrosts it. * * ```ts * const fridge = createColdStore({ era: 'fridge' }); * fridge.door.toggle(); // it leaks while it is open * game.onUpdate((t) => fridge.update(t.delta)); * fridge.keepAt(milk.x, milk.y, milk.z); // how fast the milk is going off * ``` */ type ColdEra = /** A cool stone cupboard with a marble slab and a mesh door. No mechanism. */ 'larder' /** An oak cabinet chilled by a block of ice that melts and must be replaced. */ | 'icebox' /** A domestic refrigerator: a setpoint, a cycling compressor, a door light. */ | 'fridge' /** A freezer, well below zero, that frosts up until it is defrosted. */ | 'freezer'; /** The same four-state shape as the stove and the shower, running the other way. */ type ColdState = 'warm' | 'chilling' | 'cold' | 'warming'; /** * How cold it is somewhere, in **world** coordinates. * * The preserving mirror of `HeatField`. Note that the neutral value is * **ambient**, not zero: `depthAt` and `heatAt` can return 0 for "nothing * here" because no water and no fire are genuinely nothing, but there is no * such thing as a place with no temperature. Outside the cabinet you get * the room. */ interface ChillField { /** Temperature at a world point in °C. The ambient room anywhere outside. */ chillAt(x: number, y: number, z: number): number; /** * How fast food spoils at a world point, **relative to the open bench**. * * 1 is sitting out at 20 °C; a fridge is about 0.3; a freezer is under * 0.01. Multiply a perishable's clock by it and the whole track becomes * one line of gameplay code. */ keepAt(x: number, y: number, z: number): number; } /** The door — structurally a `Manipulable`, like every other hinged thing. */ interface ColdDoor { readonly state: number; readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; update(dt: number): void; onChange?: (open: boolean) => void; object: Object3D; } interface ColdStore extends Prop, ChillField { era: ColdEra; readonly state: ColdState; /** Interior air temperature, °C. */ readonly temperature: number; /** What it is trying to hold, °C. On a larder this floats with the room. */ readonly setpoint: number; /** The room outside, °C. Writable: hand it your season or your weather. */ ambient: number; door: ColdDoor; /** How long the door has been open, in seconds. 0 while it is shut. */ readonly ajar: number; /** * Is the mechanism drawing power right now? * * A fridge does not run continuously — it cycles. This is the one reading * with no visual to go with it, deliberately: you *hear* a fridge. Wire it * to a hum and a power meter. */ readonly running: boolean; /** Ice left, 0–1. Always 1 on the eras that are wired or need none. */ readonly ice: number; /** Put a fresh block in. A no-op on anything but an icebox. */ restock(amount?: number): void; /** Frost on the coils, 0–1. It chokes the cooling as it builds. */ readonly frost: number; /** Scrape it out. */ defrost(): void; /** Interior shelves, also published as `surfaces` so `dress` can fill them. */ shelves: PropSurface[]; /** The bulb, on the eras that have one. Comes on with the door. */ light: PointLight | null; /** Where somebody stands to open it. */ slot: PropSlot; onState?: (state: ColdState) => void; /** The door has been open too long. Fires once per opening. */ onAlarm?: () => void; update(dt: number): void; } interface ColdOptions { era?: ColdEra; /** The room, °C. Default 20. */ ambient?: number; /** Start already down at temperature. Default true — a fridge in a kitchen is cold. */ cold?: boolean; /** Start with a full block of ice. Default true. */ iced?: boolean; /** Shelf count override. */ shelves?: number; seed?: number; palette?: Palette; } /** * Spoilage rate at `t` °C, relative to a bench at 20 °C. * * Q10: bacteria and the chemistry roughly halve for every ten degrees you * take off. That alone would make a freezer only twelve times better than a * worktop, which is nonsense — so freezing is modelled as what it actually * is, a **phase change** rather than more of the same. Once the water in the * food is solid nothing moves through it and the rate falls off a cliff. */ declare function spoilRate(t: number): number; /** * A larder, icebox, fridge or freezer. * * The origin is on the floor at the centre of the front face, facing +z out * into the room — the same convention as the stove, so the two stand side by * side without anybody doing arithmetic. */ declare function createColdStore(options?: ColdOptions): ColdStore; /** A cool cupboard with a marble slab: no mechanism, and it shows. */ declare function createLarder(options?: Omit): ColdStore; /** An oak cabinet cooled by a block of ice you have to keep replacing. */ declare function createIcebox(options?: Omit): ColdStore; /** A refrigerator — `fridge` holds 4 °C, `freezer` holds −18 °C and ices up. */ declare function createFridge(options?: Omit & { era?: 'fridge' | 'freezer'; }): ColdStore; declare const COLD_ERAS: ColdEra[]; /** * Ingredients — the things the rest of the kitchen is for. * * This is the prop that closes the loop. The stove published a `HeatField`, * the cold store published a `ChillField`, the prep bench yields and the * sink consumes; none of it had anything to act *on*. An ingredient is the * subject, and it reads those fields itself, exactly the way `Cookware` * does: * * ```ts * onion.update(t.delta, fridge); // samples keepAt at its own position * ``` * * There are **two independent axes**, and keeping them apart is the whole * design: * * - **form** — `whole` → `prepped`. What you did to it. A one-way step, and * it is the yield of a prep station. * - **freshness** — 1 down to 0. What time did to it, at a rate the cold * store decides. * * They interact in exactly one place, and it is the rule worth having: * **prepping something makes it spoil far faster.** A whole onion keeps for * weeks and a chopped one keeps for a day, so `prep()` is a commitment * rather than a free upgrade — which is the only thing that makes a cook * plan the order of anything. * * ```ts * const onion = createIngredient({ kind: 'onion' }); * onion.prep(); // now on a clock * onion.update(t.delta, larder); // …that the larder can slow down * onion.spoiled; // and eventually lose * ``` */ type IngredientKind = 'onion' | 'carrot' | 'potato' | 'cabbage' | 'meat' | 'fish' | 'bread' | 'cheese' | 'herbs' | 'egg'; /** What has been done to it. One way only — you cannot un-chop an onion. */ type IngredientForm = 'whole' | 'prepped'; /** How it is doing. `spoiled` is terminal. */ type IngredientState = 'fresh' | 'tired' | 'spoiled'; interface IngredientOptions { kind?: IngredientKind; /** Start already cut. Default false. */ form?: IngredientForm; /** Start at less than perfect. 0–1, default 1. */ freshness?: number; seed?: number; palette?: Palette; } interface Ingredient extends Carryable { kind: IngredientKind; readonly form: IngredientForm; readonly state: IngredientState; /** 1 (just picked) down to 0 (gone). */ readonly freshness: number; /** Nothing you can do about it. */ readonly spoiled: boolean; /** * Seconds of life left **at the rate it is currently going off**, or * `Infinity` in a freezer. What a HUD wants and what a planner needs. */ readonly shelfLife: number; /** Cut it up. Returns false if it was already prepped, or already gone. */ prep(): boolean; /** Advance the clock. Pass the cold store it is sitting in, or a rate. */ update(dt: number, chill?: ChillField | number): void; /** Fired once, when it crosses into `tired` or `spoiled`. */ onState?: (state: IngredientState) => void; } /** * One ingredient. * * The origin is at its base, like every other carryable, so it sits on a * shelf or a board without arithmetic. */ declare function createIngredient(options?: IngredientOptions): Ingredient; /** * How long this kind keeps at bench temperature, in seconds — whole, and * once it is cut. * * Exported so a planner can decide what to prepare last without having to * build one and watch it rot. */ declare function keepsFor(kind: IngredientKind, form?: IngredientForm): number; declare const INGREDIENT_KINDS: IngredientKind[]; /** * Dressers, racks and rails — storage that **shows what is in it**. * * Every storage prop the library has so far is a box that opens: a chest, a * drawer, a cupboard, a `Manipulable` whose whole job is to hide its * contents until somebody operates it. A kitchen is the opposite. The room * is arranged so that the things you use most are visible and within reach, * and a dresser with nothing on it is a bookcase. * * So this track's contribution is the vertical counterpart to `dress`: * * ```ts * dress(table.surfaces[0], things); // puts things DOWN on a surface * stock(dresser, things); // puts things AWAY — on shelves, * // in grooves, on hooks * ``` * * `dress` only knows about horizontal surfaces. Half of a kitchen's storage * is neither: plates stand **on edge** in the grooves of a rack, pans **hang** * from a rail, and a wall cupboard's shelves are **behind a door** and * therefore not on display at all until it is opened. A `StorageSpace` * carries all three of those distinctions, and `hidden` is live — shut the * cupboard and what is in it stops counting as shown. * * ```ts * const dresser = createDresser({ kind: 'welsh' }); * stock(dresser, createKitchenware({ count: 14 }), { seed: 3 }); * dresser.shown; // how much of it a person can actually see * ``` */ type DresserKind = /** A Welsh dresser: a cupboard base, and open plate shelves above. */ 'welsh' /** A wall-hung plate rack — grooves, and nothing else. */ | 'plateRack' /** A hanging rail of S-hooks for pans and tools. */ | 'potRail' /** A run of modern wall cabinets. Doors, so nothing is on show. */ | 'wallUnit' /** A tall larder cupboard for dry goods. Deep shelves, behind doors. */ | 'pantry'; type SpaceKind = /** Sits on it, the way it was built. */ 'shelf' /** Stands ON EDGE in it — a plate rack, a tray slot. */ | 'groove' /** Hangs from it, by its top. */ | 'hook' /** Sits in it, and is hidden whether or not there is a door. */ | 'drawer'; /** A cupboard door or a drawer front — structurally a `Manipulable`. */ interface DresserDoor { readonly state: number; readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; update(dt: number): void; onChange?: (open: boolean) => void; object: Object3D; } /** Somewhere one thing goes. */ interface StorageSpace { kind: SpaceKind; /** Where the thing sits, stands or hangs from. A child of the prop. */ anchor: Object3D; /** Clear height above a shelf, or the drop below a hook, in metres. */ clear: number; /** Clear width along the anchor's x, in metres. */ width: number; /** What is in it, or null. */ held: Object3D | null; /** * Is it out of sight? * * **Live**, not a constant: shut the cupboard door and everything on its * shelves stops being on display. This is the distinction the whole track * turns on, because it is the difference between a dresser and a cupboard. */ readonly hidden: boolean; } interface Storage extends Prop { kind: DresserKind; spaces: StorageSpace[]; /** Doors, where it has any. Operate them like any other `Manipulable`. */ doors: DresserDoor[]; /** Worktops and shelf tops you can also `dress`. */ surfaces: PropSurface[]; /** Where somebody stands to reach it. */ slot: PropSlot; readonly used: number; readonly free: number; /** How many of the things in it are actually visible right now. */ readonly shown: number; /** * Put something away. Picks the first free space it fits, optionally of a * given kind. Returns the space, or null if nothing would take it. */ put(item: Prop | Object3D, kind?: SpaceKind): StorageSpace | null; /** Take it back out. Returns what was there, unparented. */ take(space: StorageSpace): Object3D | null; update(dt: number): void; } interface DresserOptions { kind?: DresserKind; seed?: number; palette?: Palette; /** Paint/timber colour. Defaults per kind. */ color?: number; } /** * A dresser, rack, rail or cupboard. * * The origin is on the floor at the centre of the front face, facing +z into * the room — the same as the stove, the cold store and the sink, **including * the wall-hung kinds**. A plate rack's origin is on the floor below it, not * at the bracket, so a kitchen wall is a row of these at the same y with no * arithmetic. */ declare function createDresser(options?: DresserOptions): Storage; interface StockOptions { /** Fill at most this share of the spaces, 0–1. Default 0.72. */ density?: number; /** Restrict to these kinds of space. */ only?: SpaceKind[]; seed?: number; } /** * Put things away — the vertical counterpart to `dress`. * * `dress` arranges items on a horizontal surface. Half of what a kitchen * holds is not on one: plates stand on edge, pans hang, jars go behind a * door. `stock` walks the spaces instead, tries each item into the first one * that will take it, and **deliberately leaves gaps** — a dresser with * something in every single space is a shop, and the density default is * there for the same reason `dress` has one. * * Returns what it actually placed. Anything that would not fit is left * unparented and simply missing from the result. */ declare function stock(storage: Storage, items: Array, options?: StockOptions): Object3D[]; type UtensilStyle = 'ladle' | 'skimmer' | 'spoon' | 'knife' | 'board' | 'sieve'; interface UtensilOptions { style?: UtensilStyle; seed?: number; palette?: Palette; } /** * A kitchen tool, built **handle up**. * * That is the one decision in here. A ladle hangs from its handle with the * bowl below, and it also stands in a jar the same way up — so a single * model works both hung and put down, and `stock` needs no per-prop hook * point to hang it by. It just uses the top of the bounding box. */ declare function createUtensil(options?: UtensilOptions): Carryable; interface CrockeryOptions { /** A single plate, a stack of them, or a stack of bowls. */ style?: 'plate' | 'stack' | 'bowls'; /** How many in a stack. Default 4. */ count?: number; seed?: number; palette?: Palette; } /** * Plates and bowls. * * A single `plate` is what goes in a rack groove; a `stack` is what goes on * a shelf. They are the same object at different counts, which is exactly * why a rack and a shelf are different kinds of space rather than the same * one with a flag. */ declare function createCrockery(options?: CrockeryOptions): Carryable; interface KitchenwareOptions { /** How many pieces. Default 10. */ count?: number; seed?: number; palette?: Palette; } /** * The kitchen dress kit: a mixed set of things a kitchen actually holds. * * Weighted rather than uniform, because a real kitchen is mostly crockery * with a few tools in it, and an even draw across six utensil styles and * three crockery styles gives you a hardware display. */ declare function createKitchenware(options?: KitchenwareOptions): Carryable[]; declare const DRESSER_KINDS: DresserKind[]; declare const UTENSIL_STYLES: UtensilStyle[]; /** * The sink, and the washing-up. * * `createBasin` already exists and this is deliberately not it. A basin is * about **water**: taps, a level, a plug. A sink is about the **pile of * dishes** — the water is only the thing that makes the pile go down, and * how good the water is decides how fast. * * So this is a `WorkStation` like the chopping block and the prep bench, but * with one difference that turns it into a track of its own: **the cycle * time is not a constant.** Every other work loop in the library grinds at a * fixed rate forever. Here the rate is a product of what is in the bowl: * * ``` * rate = (water > 0) × (1 − soil × 0.75) × (0.5 + hot × 0.5) * ``` * * No water and nothing happens at all. Fresh hot water is four times faster * than a cold grey bowlful, and every plate you wash makes the water a * little worse — so at some point you stop, pull the plug, and run another * lot. That decision is the game, and none of it is a re-skin of a basin. * * The era axis is the same shape as the stove's and the cold store's, and it * ends the same way: the modern one **takes the loop away**. A dishwasher is * not a faster sink, it is a door, a capacity and a wait. * * ```ts * const sink = createWashUp({ era: 'sink' }); * sink.load(10); * sink.taps[0].set(true); * sink.onYield = (n) => console.log('washed', n); * game.onUpdate((t) => sink.update(t.delta, cook.atSink)); * ``` */ type SinkEra = /** A stone trough. No tap and no drain: water is carried in and baled out. */ 'trough' /** A deep butler sink with one cold tap. Hot water arrives in a kettle. */ | 'scullery' /** A double bowl, a mixer, a draining board. Hot water on demand. */ | 'sink' /** A machine. Load it, shut it, start it, walk away. */ | 'dishwasher'; /** * A pile of things to wash and a pile of things that are washed. * * Deliberately a **count**, not a list of objects. What a game wants from * the sink is "are the dishes done", and making the caller hand over twelve * `Carryable`s to get twelve back is ceremony around a number. */ interface WashQueue { /** Waiting to be washed. */ readonly dirty: number; /** Washed, and still sitting there until somebody puts them away. */ readonly clean: number; /** How many it holds at once. */ readonly capacity: number; /** Put dirty things in. Returns how many it actually took. */ load(count?: number): number; /** Take the clean ones away. Returns how many it actually gave. */ collect(count?: number): number; } /** The machine door — structurally a `Manipulable`, like every other one. */ interface SinkDoor { readonly state: number; readonly open: boolean; toggle(): boolean; set(target: number | boolean): void; update(dt: number): void; onChange?: (open: boolean) => void; object: Object3D; } interface WashUp extends Prop, WorkStation, WashQueue { era: SinkEra; /** Water in the bowl, 0–1. */ readonly water: number; /** How grey it is, 0 (fresh) to 1 (finished). Every plate adds to it. */ readonly soil: number; /** Heat left in it, 0–1. It goes cold on its own, fast in a stone trough. */ readonly hot: number; /** * Run water in. `hot` is what came out of it — 1 from a plumbed hot tap or * a kettle off the stove, 0 from a bucket. New water **dilutes** what is * already there rather than replacing it, so topping up a filthy bowl * helps a bit and never as much as emptying it. */ fill(amount: number, hot?: number): void; /** Pull the plug. Takes the heat and the dirt with it. */ empty(): void; /** Taps to operate. **Empty on `trough`** — that is the point of it. */ taps: Tap[]; /** The draining board clean things stack on. Also published in `surfaces`. */ board: PropSurface | null; /** The machine's door. Null on everything you wash by hand. */ door: SinkDoor | null; /** * Start a cycle. Returns false if it will not go: door open, nothing in * it, or already running. A no-op on the eras you wash by hand. */ start(): boolean; readonly running: boolean; /** Cycle progress, 0–1. Always 0 on the hand eras. */ readonly cycle: number; /** Steam off hot water, or out of a machine that has just finished. */ steam: Steam; /** Where somebody stands. */ slot: PropSlot; /** Fired when a machine cycle finishes. */ onDone?: () => void; /** Advance it. `working` gates the scrubbing, not the machine. */ update(dt: number, working?: boolean): void; } interface SinkOptions { era?: SinkEra; /** Start with dirty things in it. Default 0. */ dirty?: number; /** Start with water in the bowl, 0–1. Default 0. */ water?: number; /** How fast a fully open tap fills it, in levels per second. Default 0.3. */ rate?: number; seed?: number; palette?: Palette; } /** * A sink, trough, scullery bowl or dishwasher. * * The origin is on the floor at the centre of the front face, facing +z out * into the room — the same convention as the stove and the cold store, so a * kitchen wall is a row of these with no arithmetic. */ declare function createWashUp(options?: SinkOptions): WashUp; /** A stone trough: no tap, no plug, and the water arrives in a bucket. */ declare function createTrough(options?: Omit): WashUp; /** * A kitchen sink — `scullery` for the butler sink, `sink` for the double bowl. * * Named in full because `createSink` is already taken by the bathroom * washstand in `stations.ts`, and the two really are different props: that * one is somewhere to wash your hands, this one is somewhere to work. */ declare function createKitchenSink(options?: Omit & { era?: 'scullery' | 'sink'; }): WashUp; /** The machine that takes the loop away. */ declare function createDishwasher(options?: Omit): WashUp; declare const SINK_ERAS: SinkEra[]; /** * Houseplants. * * The trap here is worth stating before the code, because it is the only * thing that matters: **plants read by silhouette, not by colour.** Six * species that are all "green ball on a stalk" in six different greens is * one plant, six times, and no amount of leaf detail rescues it. So the * species below differ *structurally* — upright blades, drooping strands, a * dense low mound, a bare column with pads, a thin trunk under a canopy, a * flat rosette — and the colour is chosen afterwards. * * ```ts * const plant = createPlant({ species: 'trailing', seed: 3 }); * placeOn(shelf.surfaces[0], plant, { along: 0.2 }); * ``` * * The pot is a `createVessel` lathe, which is the whole reason that track * came first: a houseplant in a box would undo it. */ type PlantSpecies = /** Tall stiff blades fanning up from the soil. */ 'snake' /** A low crown with strands hanging over the rim and down. */ | 'trailing' /** A dense low mound of arcing fronds. */ | 'fern' /** A bare column with a pad or two, and no leaves at all. */ | 'cactus' /** A thin trunk under a loose canopy — the corner tree. */ | 'ficus' /** A flat rosette of thick leaves, barely above the soil. */ | 'succulent'; declare const PLANT_SPECIES: PlantSpecies[]; interface PlantOptions { species?: PlantSpecies; /** Overall height including the pot. Defaults to the species' own. */ height?: number; /** Skip the pot — for planting into a trough or a window box. */ pot?: boolean; /** * How far foliage may hang below the soil, in metres. * * A potted plant on a shelf must stop at the base of its own pot, or the * strands go through it and out under the shelf — and every placement * helper then lifts the whole plant to clear them, floating the pot. But an * UNPOTTED one has been planted into something with a rim of its own, and * that host knows how deep it is. Defaults to the pot height when potted; * pass it when planting into a trough or a basket. */ drop?: number; /** Foliage colour. Defaults to a seeded pick from the palette. */ color?: number; seed?: number; palette?: Palette; } interface Plant extends Prop { species: PlantSpecies; /** Total height in metres. */ height: number; /** Where the soil surface is, for planting several in one trough. */ soil: number; } /** A potted houseplant. */ declare function createPlant(options?: PlantOptions): Plant; interface HangingPlantOptions extends PlantOptions { /** Length of the cords above the pot. Default 0.3. */ cord?: number; } /** * A plant in a hanging basket. * * The origin is at the **fixing point**, with everything below it, because a * hanging thing is positioned by where it is hung from — the same argument * as the tapestry's rod. */ declare function createHangingPlant(options?: HangingPlantOptions): Plant; interface WindowBoxOptions { /** Trough length in metres. Default 0.8. */ length?: number; seed?: number; palette?: Palette; } /** A window trough with several plants in it, sharing one bed of soil. */ declare function createWindowBox(options?: WindowBoxOptions): Prop; /** * Paper on walls — posters, pinboards, whiteboards, sticky notes. * * The one hard rule: **no letterforms.** There is no font here, and fake * glyphs are the single most recognisable tell in a procedural scene — at * any distance where you could tell they were letters, you can tell they are * the wrong ones. What goes on these is *type at the density type has when * you see it across a room*: ruled bands with ragged right edges, heavy * blocks where a headline sits, nothing glyph-shaped. (`createSign` is the * exception and earns it, because a signpost is read deliberately and has a * real vector font behind it.) * * Everything here follows the wall-art convention: origin at the wall face, * facing +z, so `hangOn` places it. */ interface PosterOptions { /** Width in metres. Default 0.5. */ width?: number; /** Height. Defaults to a poster proportion. */ height?: number; /** `poster` (colour field) or `notice` (printed sheet). Default 'poster'. */ style?: Extract; /** Fix it with tape at the corners rather than pins. Default false. */ taped?: boolean; seed?: number; palette?: Palette; } /** A sheet stuck straight to the wall — no frame, no glass. */ declare function createPoster(options?: PosterOptions): Prop; interface PinboardOptions { /** Board width in metres. Default 0.8. */ width?: number; /** Board height. Default 0.6. */ height?: number; /** How many things are pinned to it. Default 7. */ count?: number; seed?: number; palette?: Palette; } /** * A cork pinboard with things overlapping on it. * * The **overlap** is the prop. A board of neatly spaced non-touching notes is * a spreadsheet; a real one has a photo half over a flyer with a corner of a * receipt under both, and everything at a slightly different angle. */ declare function createPinboard(options?: PinboardOptions): Prop; interface WhiteboardOptions { /** Board width in metres. Default 1.2. */ width?: number; /** Board height. Default 0.8. */ height?: number; /** How covered it is, 0–1. Default 0.6. */ fill?: number; seed?: number; } /** * A whiteboard with something on it. * * Handwriting is drawn as strokes rather than as anything readable, for the * same reason as everything else in this file. What sells a whiteboard is * the **layout**: a boxed diagram somewhere, a couple of lines of scrawl, an * arrow — and a big blank patch, because nobody ever fills one. */ declare function createWhiteboard(options?: WhiteboardOptions): Prop; interface StickyNotesOptions { /** How many. Default 5. */ count?: number; /** Note edge in metres. Default 0.075. */ size?: number; /** Spread them over this area (metres). Default 0.4 × 0.3. */ width?: number; height?: number; seed?: number; } /** * A cluster of sticky notes. * * Origin at the wall face, facing +z, so this goes straight onto a wall, the * edge of a monitor, or a whiteboard. They cluster and they overlap, because * nobody spaces them out. */ declare function createStickyNotes(options?: StickyNotesOptions): Prop; /** * Vessels — everything round, from one generator. * * This kit is otherwise made of boxes, and it shows: a room of props built * from `BoxGeometry` has no curves in it anywhere, which reads as a *style* * right up until you put a bowl of fruit on the table and discover there is * no bowl. * * A **surface of revolution** fixes that with almost no code. Sample a * seeded radius profile up the height, spin it, and the same twenty lines * produce a vase, an urn, a bottle, a goblet, a bowl and a candlestick — * shapes that would each be a separate hand-modelled prop otherwise. The * style is nothing but the list of control points. * * ```ts * const vase = createVessel({ style: 'vase', seed: 4 }); * dress(table.surfaces[0], [vase, ...], { seed: 4 }); * ``` */ type VesselStyle = 'vase' | 'urn' | 'bottle' | 'jug' | 'goblet' | 'bowl' | 'pot' | 'candlestick'; interface VesselOptions { style?: VesselStyle; /** Overall height in metres. Defaults to something sensible per style. */ height?: number; /** Base colour. Defaults to the surface's own. */ color?: number; /** Override the finish. */ surface?: SurfaceKind; seed?: number; palette?: Palette; } interface Vessel extends Prop { /** Height in metres, after any seeded variation. */ height: number; /** Widest radius, in metres. */ radius: number; style: VesselStyle; } /** A round thing: vase, urn, bottle, jug, goblet, bowl, pot or candlestick. */ declare function createVessel(options?: VesselOptions): Vessel; declare const VESSEL_STYLES: VesselStyle[]; /** * Clutter — the small stuff that sits on things. * * The kit already had carryables, and every one of them is a *carryable*: a * basket is 48 cm across, so three of them is a full table and a tabletop * dressed from that set says nothing except "somebody left the shopping * out". What was missing is the 5–25 cm layer — books, papers, folded cloth, * a couple of pieces of fruit — which is what actually makes a shelf look * like a shelf rather than a shelf-shaped object. * * ```ts * dress(shelf.surfaces[0], createClutter({ theme: 'study', count: 7, seed: 2 })); * ``` * * Everything here is deliberately cheap: a book is one box, a stack is five. * At the size these occupy on screen that is already more detail than * survives, and the budget belongs to having *more different things* rather * than better ones. */ interface ClutterOptions { seed?: number; palette?: Palette; } type BookStyle = /** Lying flat, largest at the bottom. */ 'stack' /** Standing in a row, shoulder to shoulder. */ | 'row' /** A short row with the last one leaning on it. */ | 'leaning' /** One book, open, face down. */ | 'open'; interface BooksOptions extends ClutterOptions { style?: BookStyle; /** How many. Default 4. */ count?: number; } /** * Books. * * A row of spines with varied heights, a few leaning, one stack lying flat is * most of what a bookshelf is, and none of it needs more than a box each. */ declare function createBooks(options?: BooksOptions): Prop; interface PapersOptions extends ClutterOptions { /** How many sheets. Default 6. */ count?: number; /** Sheet long edge in metres. Default 0.24. */ size?: number; } /** A slew of loose sheets: a stack, never square, with one clear of the pile. */ declare function createPapers(options?: PapersOptions): Prop; interface FoldedOptions extends ClutterOptions { /** Folded width in metres. Default 0.2. */ width?: number; /** Cloth colour. Defaults to a seeded pick from the palette. */ color?: number; } /** Folded cloth: a towel, a napkin pile, a blanket on the end of a bed. */ declare function createFolded(options?: FoldedOptions): Prop; interface TrinketOptions extends ClutterOptions { /** Long edge in metres. Default 0.1. */ size?: number; } /** A small lidded box — the filler that reads as "something of theirs". */ declare function createTrinket(options?: TrinketOptions): Prop; interface FruitBowlOptions extends ClutterOptions { /** How many pieces. Default 5. */ count?: number; } /** * A bowl with fruit in it — the one piece here that composes the two tracks, * since the bowl is a lathe and the fruit are not. */ declare function createFruitBowl(options?: FruitBowlOptions): Prop; type ClutterTheme = 'domestic' | 'kitchen' | 'study' | 'workshop'; interface ClutterKitOptions extends ClutterOptions { theme?: ClutterTheme; /** How many pieces to make. Default 6. */ count?: number; } /** * A mixed set of small things, ready to hand straight to `dress`. * * The pool is drawn from **without replacement until it runs out**, so a set * of six is six different things rather than the same vase six times — which * is what picking at random gives you, and which is exactly as obviously * generated as an even spread. */ declare function createClutter(options?: ClutterKitOptions): Prop[]; declare const CLUTTER_THEMES: ClutterTheme[]; /** * Rolling stock: locomotives, carriages, and goods wagons — and the coupling * that makes a list of them into a train. * * ```ts * const train = createConsist(track, [ * createLocomotive({ seed: 1 }), * createCarriage({ seed: 2 }), * createCarriage({ seed: 3 }), * ]); * scene.add(train.object); * train.place(120); // the whole train, 120 m along the line * ``` * * ## Why a consist is not just N props in a row * * A vehicle on a curve does not face the way the track faces at its centre. It * is a rigid body resting on two bogies, and it faces along the CHORD between * them — which on a bend is measurably different, and is the difference * between a train that looks like a train and a string of boxes shrink-wrapped * to a spline. * * So `place` samples the track twice per vehicle, at its bogie centres, and * puts the body on the midpoint facing the chord. Two extra samples per * carriage per frame, and it is the whole trick. */ interface RollingStockOptions { seed?: number; palette?: Palette; /** Body colour. Seeded from a livery set when omitted. */ color?: number; /** Length over couplings, metres. Sensible defaults per kind. */ length?: number; } /** A vehicle that can be coupled into a consist. */ interface RollingStock extends Prop { /** Length over couplings, metres — what the consist spaces by. */ length: number; /** Distance between bogie centres. The chord `place` faces along. */ bogieSpacing: number; /** * Door centres as offsets from the vehicle's own centre, in metres. * * Empty on a goods wagon. This is what a platform aligns to, and what makes * "the doors stopped 40 cm past their markers" a number rather than a * complaint. */ doors: number[]; /** The bogies, so a caller can spin the wheels at the right rate. */ wheels: Object3D[]; /** Wheel radius, so that rate is derivable rather than guessed. */ wheelRadius: number; } /** A passenger carriage: body, window band, roof, and doors that matter. */ declare function createCarriage(options?: RollingStockOptions): RollingStock; /** A locomotive: the same chassis, a cab you can stand in, and a nose. */ declare function createLocomotive(options?: RollingStockOptions): RollingStock; interface WagonOptions extends RollingStockOptions { /** `open` for mineral/coal, `van` for a closed box, `flat` for a bed. */ kind?: 'open' | 'van' | 'flat'; } /** A goods wagon. No doors to align, no seats — freight, not people. */ declare function createWagon(options?: WagonOptions): RollingStock; interface ConsistOptions { /** Gap between coupled vehicles, metres. Default 0.6. */ coupling?: number; /** Roll the wheels as the train moves. Default true. */ rollWheels?: boolean; } interface Consist { object: Group; vehicles: RollingStock[]; /** Length over the whole train, including couplings. */ length: number; /** * Put the train's FRONT at `distance` along the track. * * The front, not the centre, because a station stop is expressed as "the * front of the train at the stopping mark" — that is what a driver aims at * and what a platform is measured from. */ place(distance: number): void; /** Where `vehicles[v]`'s door `d` is in world space, after the last `place`. */ doorPosition(vehicle: number, door: number, out?: Vector3): Vector3; /** Every door on the train, in order. Convenience over `doorPosition`. */ doorPositions(): Vector3[]; } /** * Couple vehicles onto a track. * * `track` is taken structurally — anything with `length` and `at()` — so this * works with a `RailTrack`, a test double, or whatever a game lays its own * lines with. */ declare function createConsist(track: Pick, vehicles: RollingStock[], options?: ConsistOptions): Consist; /** * A station platform, laid alongside a track. * * ```ts * const platform = createPlatform(track, { * from: 400, to: 520, name: 'HAVENBROOK', * }); * scene.add(platform.object); * platform.stopMark; // where a train's FRONT should come to rest * platform.doorMarks; // where its doors are expected to land * ``` * * ## The marks are the point * * A platform is easy to build and easy to build wrong, and the wrongness is * invisible in a screenshot: a train stops, the doors open, and they are two * metres past the gap in the fence. So the platform publishes where it expects * a train to stop and where it expects the doors to be, and those are numbers * a test can hold to a few centimetres. * * `doorMarks` is derived from the consist you intend to run, not guessed — * pass the door offsets and the platform puts a marking on the paving at each * one. If the train changes length, the markings move, which is exactly what * happens on a real railway when the timetable changes. */ interface StationPlatformOptions { /** Distance along the track where the platform starts, metres. */ from: number; /** Distance along the track where it ends. */ to: number; /** Which side of the track, looking along it. Default 'left'. */ side?: 'left' | 'right'; /** Platform width, metres. Default 6. */ width?: number; /** Height above rail level. Default 0.9 — a step up into a carriage. */ height?: number; /** Station name, carved into the running-in board. Omit for no board. */ name?: string; /** * Door offsets of the train that stops here, from `RollingStock.doors` * mapped through the consist. Each gets a marking on the paving. */ doorOffsets?: number[]; /** Where the train's FRONT stops. Defaults to the far end minus a margin. */ stopAt?: number; /** Canopy over part of the platform. Default true. */ canopy?: boolean; seed?: number; palette?: Palette; } interface StationPlatform { object: Group; obstacleRadius: number; slots: PropSlot[]; /** Distance along the track a train's front should stop at. */ stopMark: number; /** World positions the doors are expected to land on. */ doorMarks: Vector3[]; /** The platform edge, for a crowd to queue behind. */ edge: { from: Vector3; to: Vector3; }; from: number; to: number; dispose(): void; } /** A station platform beside a track, with the marks a train aligns to. */ declare function createStationPlatform(track: Pick, options: StationPlatformOptions): StationPlatform; /** * Ammunition — the whole supply chain, not a shelf of models. * * A round is never just a round. The same cartridge is a thing in a crate, a * thing in a magazine, a thing in a hand and a case on the floor, and a game * that wants ammunition wants all four or none of them. So this module is * organised by STATE rather than by object: * * stored `createAmmoBox` — sealed or open, rounds visible inside * ready `createMagazine` `createBelt` `createQuiver` `createRack` * carried `createRound` — one, `Holdable`, in a hand * spent `createCasing` — brass on the ground, links, an empty box * * and every one of them is DERIVED from a single measured spec per kind. The * magazine is as long as its rounds are, the belt's link pitch is the case * head diameter, the crate's stack count falls out of the crate's inside * dimensions divided by the round. Author forty models by hand and forty * models drift; author one table and a 12.7 mm belt is visibly heavier than a * 5.56 mm one because it *is*. * * ## The handshake that matters * * `ballisticsOf(kind)` returns exactly what GAMA's `Projectiles` and * `Missiles` want — muzzle velocity, drop, tracer size and colour. The same * table that decides how long the cartridge model is decides how fast it * flies and how far it falls. That is the point of putting them together: * a game cannot make the prop and the projectile disagree, because there is * only one number. * * ```ts * const b = ballisticsOf('rifle'); * const shots = new Projectiles({ gravity: b.gravity, size: b.size, color: b.color }); * shots.fire(muzzle, aim.multiplyScalar(b.speed)); * mag.setCount(mag.count - 1); // the belt/magazine visibly empties * ``` * * ## Instancing is not an optimisation here, it is the feature * * A 200-link belt is 200 rounds. Built as meshes that is 400 draw calls for * one prop and the geometry gate refuses it — correctly. Every container * renders its rounds as ONE `InstancedMesh` per part, and `setCount` rewrites * instance matrices rather than adding or removing anything. A magazine that * empties therefore costs the same as a full one, which is what lets a game * put a belt on every gunner in a firefight. */ /** A quantity of one kind of ammunition, in the state a game finds it in. */ type AmmoKind = 'pistol' | 'rifle' | 'shotgun' | 'heavy-mg' | 'autocannon' | 'tank' | 'artillery' | 'mortar' | 'rocket' | 'missile' | 'bomb' | 'torpedo' | 'depth-charge' | 'grenade' | 'rifle-grenade' | 'canister' | 'grapeshot' | 'arrow' | 'bolt' | 'sling' | 'cannonball' | 'ballista'; declare const AMMO_KINDS: AmmoKind[]; /** * How a round is put together, which decides how it is drawn. * * `case` is the material story and `head` is the silhouette, and they are * separate because they vary independently: a tank round is a brass case with * a fin-stabilised dart in it, an artillery shell is a bagged charge with no * case at all, and a crossbow bolt is neither. */ type CaseKind = 'brass' | 'steel' | 'plastic' | 'bagged' | 'none'; type HeadKind = 'spitzer' | 'ball' | 'shot' | 'dart' | 'shell' | 'finned' | 'sphere' | 'shaft' | 'tin' | 'stand'; /** Everything about one kind, measured once. */ interface AmmoSpec { /** Projectile diameter, metres. The number everything else is scaled from. */ calibre: number; /** Overall length of the complete round, metres. */ length: number; /** Mass of the complete round, kilograms. */ mass: number; /** * Muzzle velocity, m/s. Zero for anything not launched from a barrel — a * bomb is dropped, a grenade is thrown, a depth charge is rolled. A game * reading zero here is being told "you supply the launch", which is the * honest answer rather than a made-up number. */ muzzle: number; case: CaseKind; head: HeadKind; /** How many fit in one standard container of the kind below. */ perContainer: number; /** Which ready-state container this kind actually ships in. */ container: 'magazine' | 'belt' | 'quiver' | 'rack' | 'box'; /** Tracer / body colour, for both the model and the projectile. */ color: number; label: string; } /** * The table. * * Real calibres and real masses, because the whole value of deriving the * containers is lost if the source numbers are invented: a 12.7 mm belt is * supposed to look punishing next to a 5.56 mm one, and it only does if the * two are actually 12.7 and 5.56. * * Muzzle velocities are the honest ones too — an APFSDS dart really does * leave a tank gun at 1750 m/s, and a game that gives it 300 because that * looked nice in the editor has thrown away the only reason to have a table. */ declare const AMMO: Record; /** * What a projectile system needs, from the same table that shaped the model. * * Structurally what GAMA's `Projectiles` options and `fire()` want, and * deliberately not an import of them — the trilogy composes on shapes, not * packages. A game that never draws a single round can still use this to make * its shots behave like the calibre it claims they are. */ interface Ballistics { /** Muzzle velocity, m/s. Zero means this is not launched from a barrel. */ speed: number; /** Downward pull to fly it under, m/s². */ gravity: number; /** A sensible tracer radius: visible, and proportional to the calibre. */ size: number; color: number; mass: number; /** Rounds in one full standard container. */ perContainer: number; } /** * Ballistics for a kind. * * `gravity` is the interesting one. Everything unpowered gets 9.81 — a bullet * drops exactly as hard as a cannonball does, and pretending otherwise is the * single most common lie in game ballistics. What differs is TIME OF FLIGHT, * and that falls out of `speed` on its own. Powered rounds are the exception * and get a reduced figure, because a rocket under thrust genuinely does not * fall like a stone; a missile with its own guidance gets zero, since whatever * flies it owns its path. */ declare function ballisticsOf(kind: AmmoKind, options?: { /** * Charge increments loaded, for separate-loading kinds. Omit for a full * charge. Ignored by anything that is not bag-loaded, because a rifle * round's propellant is not a decision anybody makes at the gun. */ increments?: number; /** Increments in a full charge. Default 7. */ chargeCapacity?: number; }): Ballistics; /** A one-line description, for editors, tooltips and debug overlays. */ declare function describeAmmo(kind: AmmoKind): string; /** Triangles in one round, for anyone budgeting a container. */ declare function roundTriangles(kind: AmmoKind): number; interface RoundOptions { seed?: number; /** Scale the whole round. Real calibres are small; a HUD wants them bigger. */ scale?: number; } /** * One round, `Holdable` and `Prop`. * * Laid out along +Z with its base at the origin, so a game can point it the * way it is going without an offset, and ANIMA's `Carry` can hold it. */ interface Round extends Prop { kind: AmmoKind; /** ANIMA's `Holdable` carry style — small rounds go in one hand. */ carry: 'side' | 'crate'; /** Length along +Z, after scale. */ length: number; ballistics: Ballistics; } declare function createRound(kind: AmmoKind, options?: RoundOptions): Round; /** * Anything holding a countable number of rounds. * * A magazine, a belt, a quiver and a shell rack are the same object as far as * a game is concerned: they hold N of something, N goes down, and the model * has to show it. One interface, so a HUD or a reload routine written against * a rifle magazine works on a howitzer's ready rack without knowing. */ interface Countable extends Prop { kind: AmmoKind; readonly capacity: number; readonly count: number; /** Show `n` rounds. Clamped to `[0, capacity]`. Returns the count set. */ setCount(n: number): number; /** Take one. Returns whether there was one to take. */ consume(): boolean; } interface ContainerOptions { seed?: number; /** How many rounds to start with. Default: full. */ count?: number; /** Override the container's capacity. Default: the kind's `perContainer`. */ capacity?: number; scale?: number; } /** * A box magazine — the small-arms one, rounds staggered in a column. * * The body is sized from the rounds rather than the other way round: a * 30-round 5.56 magazine and an 8-round 12-gauge tube come out visibly * different because their contents are, and neither was drawn by hand. */ declare function createMagazine(kind: AmmoKind, options?: ContainerOptions): Countable; interface BeltOptions extends ContainerOptions { /** Curve the belt into a hanging catenary. Default true. */ drape?: boolean; } /** * A linked belt — machine-gun and autocannon feed. * * The link pitch is the case head diameter, so a 12.7 mm belt is genuinely * 40% coarser than a 5.56 one. `setCount` feeds it: rounds disappear from the * front, which is the direction a belt actually empties. */ declare function createBelt(kind: AmmoKind, options?: BeltOptions): Countable; /** * A quiver — arrows, bolts, ballista shafts, nocks up. * * The only container whose rounds stand vertically, and the only one where * the count is read at a glance from outside, which is why archers count them * and riflemen do not. */ declare function createQuiver(kind: AmmoKind, options?: ContainerOptions): Countable; interface RackOptions extends ContainerOptions { /** Rounds per row. Default: the square-ish arrangement. */ perRow?: number; } /** * A ready rack — artillery shells stood on end, bombs on a trolley, torpedoes * in a cradle, round shot in a pyramid frame. * * The heavy end of the set, and the one a game actually walks past. Rounds * stand or lie according to what the real thing does: a 155 mm shell stands, * a torpedo does not, and the rule is the round's own aspect ratio rather * than a per-kind flag. */ declare function createRack(kind: AmmoKind, options?: RackOptions): Countable; interface AmmoBoxOptions extends ContainerOptions { /** Lid open, rounds visible. Default false. */ open?: boolean; } /** * The stored state — a crate with its lid on, or off and full of rounds. * * Closed it is a box and costs almost nothing; open it is the same box with * its contents instanced inside. `open` is a build-time choice rather than a * method because a sealed crate should not pay for geometry nobody can see, * and a level has a hundred of them. */ declare function createAmmoBox(kind: AmmoKind, options?: AmmoBoxOptions): Countable; interface CasingOptions { seed?: number; /** How many to scatter. Default 24. */ count?: number; /** Radius of the scatter, metres. Default 0.6. */ spread?: number; scale?: number; } /** * The spent state — a litter of empty cases where somebody stood. * * A whole scatter rather than one case, because one spent case is invisible * and a hundred of them is a story: this is where the gunner was. Ejected * brass lands on its side, in a loose cone off to the shooter's right, which * is what the scatter is shaped like. * * Kinds with no case — a mortar bomb, an arrow, a grenade — have nothing to * eject, and this returns an empty prop for them rather than inventing litter. */ declare function createCasing(kind: AmmoKind, options?: CasingOptions): Prop; /** * The right ready-container for a kind, without the caller knowing which. * * `AMMO[kind].container` already says whether a kind belts, magazines, * quivers, racks or boxes, so a level that just wants "some ready ammunition * for this weapon" should not have to switch on it. This is that switch, * written once. */ declare function createReady(kind: AmmoKind, options?: ContainerOptions): Countable; type LoaderStyle = /** A spine with rounds in a row, thumbed down into a magazine. */ 'stripper' /** A ring of rounds with a knob, dropped into a revolver cylinder. */ | 'speedloader' /** A cage that goes INTO the rifle with the rounds and ejects after. */ | 'en-bloc'; interface LoaderOptions extends ContainerOptions { style?: LoaderStyle; } /** * A stripper clip, a speedloader or an en-bloc clip. * * The three differ in one thing that matters and it is not their shape: a * stripper clip stays in the hand, a speedloader stays in the hand, and an * en-bloc clip **goes into the rifle** and is ejected when the last round * fires. That is why `en-bloc` is a kind of loader and not a kind of magazine, * and why a game reloading a Garand ejects something and one reloading a * Mauser does not. * * Capacity defaults are the real ones — 5 for a stripper, 6 for a speedloader, * 8 for an en-bloc — rather than the kind's magazine capacity, because a clip * holds what a clip holds regardless of what the magazine under it takes. */ declare function createLoader(kind: AmmoKind, options?: LoaderOptions): Countable & { style: LoaderStyle; }; interface BandolierOptions extends ContainerOptions { /** Loops along the strap. Default 20. */ loops?: number; /** How far the strap sags across the chest, metres. Default 0.16. */ sag?: number; } /** * A bandolier — rounds in loops on a strap, worn across the body. * * The only container in the set that is WORN rather than held or set down, and * the difference shows in the handshake: it publishes `socket`, the name ANIMA * uses for the attachment point, and the caller parents it there. SCENA does * not know what a shoulder is; it knows what a strap that has to hang across * one looks like. * * The strap is a catenary, like the belt, for the same reason: a straight one * is the tell that this was drawn rather than laid out. It is authored in the * plane a torso presents, so parenting it to a chest socket needs no rotation. */ declare function createBandolier(kind: AmmoKind, options?: BandolierOptions): Countable & { socket: string; }; interface ChargeOptions extends ContainerOptions { /** * Increments loaded, 1..`capacity`. This is the gunner's actual decision on * a separate-loading piece: more bags, more velocity, more range, more wear. */ increments?: number; } /** * Bagged propellant — the other half of a separate-loading round. * * A 155 mm shell is not a cartridge. The shell goes in, then a number of cloth * charge bags behind it, and how many is a decision made per shot. Modelling * the shell without the charge is modelling half the round, and it is the half * a gun crew spends its time on. * * `count` is the number of bags SHOWN; `chargeVelocity` says what that many * are worth. Only kinds whose case is `bagged` have these — asking for a * charge for a rifle round is asking for something that does not exist, and * this returns an empty prop rather than inventing one. */ declare function createCharge(kind: AmmoKind, options?: ChargeOptions): Countable; /** * What `increments` bags of propellant are worth, in m/s. * * Muzzle energy is proportional to the propellant burnt and velocity goes as * its square root, so a half charge is **71%** of full velocity rather than * 50%. Getting that linear is the difference between a gunnery mechanic that * behaves like artillery and one that behaves like a slider. * * The full charge is the kind's own `muzzle`, so this and `ballisticsOf` can * never drift apart. */ declare function chargeVelocity(kind: AmmoKind, increments: number, capacity?: number): number; interface KegOptions { seed?: number; scale?: number; /** Lid off, powder visible. Default false. */ open?: boolean; } /** * A powder keg — the bulk propellant that everything before the cartridge ran * on, and the most explosive thing on any pre-modern map. * * Not a `Countable`: a keg holds a mass, not a number of rounds, and giving it * a `count` would be inventing a unit nobody uses. */ declare function createPowderKeg(options?: KegOptions): Prop; interface DumpOptions { seed?: number; /** Pallets of crates. Default 6. */ pallets?: number; /** Crates per pallet. Default 6. */ perPallet?: number; /** Fraction of pallets with the top crate open. Default 0.3. */ open?: number; scale?: number; } interface AmmoDump extends Prop { kind: AmmoKind; /** Crates in the dump, of whatever this kind's crate holds. */ crates: number; /** Rounds the whole dump represents. */ rounds: number; } /** * An ammunition dump — pallet scale. * * The state above `stored`: not a crate, a supply point. Crates stacked on * pallets in a loose grid, a few of them open, the rest sealed, with kegs or * charge bags alongside for the kinds that need them. * * The reason this is worth its own function rather than a loop in a level is * that a naive loop is a performance trap: thirty-six wooden crates is * thirty-six draws before anything is in them, and a sealed crate is exactly * the same box every time. The crates here are ONE instanced mesh, the pallets * another, and only the open ones pay for contents. */ declare function createAmmoDump(kind: AmmoKind, options?: DumpOptions): AmmoDump; /** * Breaking boards — *tameshiwari*, and a number SCENA is willing to be wrong * about in public. * * Everything else in this file exists to support one function. `boardStrength` * says what it takes to break a board, and it says so from published constants * and the board's own dimensions — nothing else: * * MODULUS OF RUPTURE the bending stress timber fails at (Wood Handbook) * YOUNG'S MODULUS how far it bends on the way there (same source) * STRENGTH RATIO the knock-down for knots and grain (ASTM D245) * three-point bending F = 2·σ·b·d² / 3L, the standard relation for a * simply supported beam loaded in the middle * * SCENA does not know what a punch is, has never heard of ANIMA, and imports * nothing from it. It declares what a board takes and stops. * * ## It has been checked against the world * * Feld, McNair and Wilk measured a hand going through a 30 × 15 × 2.5 cm pine * board in Scientific American in 1979 and put the breaking force at about * 3.1 kN. The formulae above, handed that board's dimensions and nothing else, * say 3.62 kN. * * That is a 17% error from four published numbers and no fitting, which is the * point of deriving rather than choosing: the number can be WRONG, out loud, * against somebody else's measurement. */ type Timber = 'pine' | 'poplar' | 'cedar' | 'oak' | 'pineWet'; interface TimberSpec { label: string; /** * Modulus of rupture, pascals — the bending stress at which it snaps. * Wood Handbook (USDA FPL) values for clear, kiln-dried, 12% moisture. */ rupture: number; /** Young's modulus in bending, pascals. Same source. */ stiffness: number; /** Density, kg/m³ — for the mass of the halves once it is in two. */ density: number; } /** * Five timbers, and not one of these numbers was chosen to make a demo work. * * Pine at 40 MPa is the standard tameshiwari board and the reason a beginner * can break one. Oak at 100 MPa is two and a half times as hard and is the * reason nobody uses it. `pineWet` is the same pine at 20% moisture rather * than 12%, which is a real and well-documented 25% loss — and is why boards * are kept in a dry room and why a demonstration in the rain goes wrong. */ declare const TIMBERS: Record; declare const TIMBER_NAMES: Timber[]; interface BoardShape { timber?: Timber; /** Across the grain, metres. A competition board is 0.30. */ width?: number; /** Along the grain, metres. Also 0.30 — boards are square. */ length?: number; /** The one that matters, metres. A competition board is 0.019 (¾"). */ thickness?: number; /** * Distance between the two supports, metres. Defaults to 85% of the * length, which is where hands or blocks actually sit. */ span?: number; } interface BoardStrength { timber: Timber; /** Peak force the board takes before it snaps, newtons. */ force: number; /** How far the middle has moved by then, metres. */ deflection: number; /** * The work done bending it to failure, joules — the area under a linear * force-against-deflection curve, so half of force times deflection. * * Reported because it is derivable and because it settles an argument: a * pine board needs 1.9 J and ANIMA independently puts a hammerfist at 113 J, * sixty times more. ENERGY IS NOT WHAT LIMITS BOARD BREAKING. The force is, * and `force` is the number to compare against. */ energy: number; /** Mass of the board, kg. */ mass: number; } /** * How hard a board is, from what it is made of and how thick it is. * * Three-point bending, which is what a board across two supports with a fist * in the middle is: * * I = b·d³/12 second moment of area of a rectangle * F = 2·σ·b·d² / (3·L) the load at which the outer fibre reaches σ * δ = F·L³ / (48·E·I) how far the middle has gone by then * U = ½·F·δ the work done getting there * * Thickness is squared in the force and cubed in the stiffness. The obvious * conclusion — that doubling it takes eight times as much — is wrong, and was * written here that way first: the `d³` is in the STIFFNESS, and a stiffer * beam reaches its failure stress sooner, so the deflection at failure falls * as `1/d` and the energy comes out linear. See `stackStrength`. * * The FORCE really is quadratic, and that is the one a person runs out of. */ declare function boardStrength(shape?: BoardShape): BoardStrength; /** * A stack of them, spaced against glued — and the answer is not the one you * would guess. * * The force to break a beam goes as `d²` and its stiffness as `d³`, so the * deflection at failure goes as `1/d` and the ENERGY — half force times * deflection — comes out LINEAR in thickness. Six boards glued into one thick * beam take exactly the same energy as six separate ones, to the joule. * * That was written here as "216 times harder" first, on the strength of the * `d³`, and it is simply wrong: the `d³` is in the stiffness, and stiffness * makes a beam break SOONER, not later. The algebra says `U ∝ σ²bdL/E`. * * The difference between spaced and glued is entirely in the FORCE, and it is * enormous: six spaced boards need 3.6 kN each, one at a time, and the same * six glued need 130 kN all at once — which no person can produce. That is * what the spacers are for, and nothing about it is about energy. */ declare function stackStrength(count: number, shape?: BoardShape): { /** Joules for the whole stack, spaced. */ spaced: number; /** ...and glued into one beam. The same number, which is the point. */ solid: number; /** Newtons needed for ONE spaced board. */ spacedForce: number; /** ...and for the glued beam. This is where the difference lives. */ solidForce: number; }; type BoardState = 'intact' | 'broken'; interface BoardOptions extends BoardShape { seed?: number; /** How many boards, held apart by spacers. Default 1. */ count?: number; /** Height of the supports off the ground, metres. Default 0.9. */ height?: number; } interface BoardStack { group: Group; trigger: Obstacle; /** What one board of this stack takes, in joules. */ readonly strength: BoardStrength; /** How many are still whole. */ readonly standing: number; readonly state: BoardState; /** * Hit it with this much FORCE, in newtons. * * Force rather than energy, because that is what breaks a beam: the outer * fibre reaches its rupture stress or it does not, and how much kinetic * energy happened to be behind it is a separate question. Returns how many * boards broke. * * Nothing here knows or cares where the newtons came from — a fist, a * hammer, a falling rock. It is a force against a threshold computed from * the timber, and both sides of that comparison can be derived independently * by people who have never heard of each other. */ strike(newtons: number): number; reset(): void; update(dt: number): void; } /** * Boards on two blocks, and a `strike` that takes joules. * * The halves fly with whatever energy was left after breaking them, which is * the honest thing to do with it: a strike that only just breaks the board * drops the pieces, and one with a lot to spare throws them. */ declare function createBoard(options?: BoardOptions): BoardStack; /** * Armour — what a plate takes, and the second half of a handshake. * * SCENA has never heard of an arrow. This file declares what it costs to push * a hard point through a sheet of metal, from the metal's yield strength and a * ruler, and stops. * * ## The mechanism is indentation, not punching * * The obvious model is shearing a plug: force equals perimeter times thickness * times shear strength, which is what every press-tool handbook uses for * punching holes. Handed a 9 mm bodkin and 2 mm of wrought iron it says * **19.6 joules**, and the measured figure is nearly ten times that. * * It is the wrong mechanism. A sharp point does not shear a plug out — it * OPENS A HOLE, pushing metal aside radially, and the pressure that takes is * the metal's INDENTATION pressure. Tabor measured that in 1951 and it is * about three times the yield stress: * * p ≈ 3·σ_y Tabor's relation. It is also what a hardness * test measures, which is why hardness numbers * and yield strengths sit in that ratio * F = p · π·d²/4 over the point's own frontal area * E = F · t through the thickness of the plate * * Same 9 mm bodkin, same 2 mm plate: **114 joules**, against a measured 175. * 35% out from two published numbers and a ruler. * * ## What it is wrong against * * Alan Williams (*The Knight and the Blast Furnace*, 2003) measured energies to * defeat armour and put 2 mm of wrought iron plate at about **175 J**, and mail * over padding at around **120 J**. English war-bow arrows carry 80-120 J. * * Those are system figures — they include dishing the plate over a hand's * breadth, the arrow bending, and whatever is underneath. This file models the * hole and nothing else, so it should and does come out UNDER them. * * ## And the part this file deliberately hands off * * `mailStrength` says what one riveted ring takes, and the answer is almost * nothing: a couple of joules. Mail is not what stops the arrow. **The padding * under it is**, and the padding is textile — which SCENA has no business * knowing the fracture toughness of. That number lives in ANIMA, in a module * about cutting people, and neither package imports the other. */ type Alloy = 'wroughtIron' | 'mildSteel' | 'mediumCarbon' | 'hardened' | 'bronze' | 'aluminium'; interface AlloySpec { label: string; /** Yield strength, pascals. Ordinary published values. */ yield: number; /** Ultimate tensile strength, pascals. */ ultimate: number; /** kg/m³. */ density: number; } /** * Tabor's relation: the indentation pressure of a ductile metal is about three * times its yield stress. * * Measured, in *The Hardness of Metals* (1951), and it is the reason a Vickers * number and a yield strength sit in that ratio. It is the single number that * turns "how strong is this steel" into "what does it cost to push a spike * through it", and there is no fitting anywhere near it. */ declare const TABOR = 3; /** * Six metals. Wrought iron is the one that matters, because it is what most * surviving armour is and it is nothing like modern steel. */ declare const ALLOYS: Record; declare const ALLOY_NAMES: Alloy[]; interface PlateShape { alloy?: Alloy; /** Metres. Munition plate is 1.5-2 mm; a jousting breastplate is 4 mm. */ thickness?: number; /** The hole that has to be made, metres — the point's widest diameter. */ hole?: number; /** Metres, for the mass. */ width?: number; height?: number; } interface PlateStrength { alloy: Alloy; /** Pa — the indentation pressure, 3σ_y. */ pressure: number; /** Newtons to keep the point moving. */ force: number; /** Joules to open a hole all the way through. */ energy: number; /** kg of the panel. */ mass: number; /** * What the WRONG model says, joules — shearing a plug out instead of opening * a hole. * * Kept and reported because it is the model everybody reaches for first, it * is off by nearly ten times, and a number that is only ever right is a * number nobody has checked against the alternative. */ punchingEnergy: number; } /** * What it costs to put a hard point through a plate. * * Indentation, not shearing: the point opens a hole of its own diameter * against the metal's indentation pressure, all the way through. */ declare function plateStrength(shape?: PlateShape): PlateStrength; interface MailShape { alloy?: Alloy; /** Wire diameter, metres. Surviving mail is 1.0-1.6 mm. */ wire?: number; /** Ring inner diameter, metres. Typically 8-10 mm. */ ring?: number; } interface MailStrength { alloy: Alloy; /** Newtons to burst one riveted ring — two wire sections in tension. */ force: number; /** Joules, over the distance the point has to open the ring. */ energy: number; /** kg/m² of the fabric. */ areal: number; } /** * What one riveted ring takes, and it is not much. * * A point entering a ring loads it in tension across two sections of wire. The * wire is a millimetre and a bit, so the force is a few hundred newtons and the * energy is a couple of joules — against the hundred-odd joules an arrow * carries. * * That is not a defect in the model. It is the reason mail was never worn on * its own. What stops the arrow is the padding, and the padding is textile. */ declare function mailStrength(shape?: MailShape): MailStrength; interface ArmourOptions extends PlateShape { seed?: number; /** How many strikes it takes before the panel is holed. */ hits?: number; } interface ArmourProp { group: Group; strength: PlateStrength; /** Holes made so far. */ holes: number; /** * Strike it with an energy in JOULES. * * Joules and not newtons, and that is the opposite of `createBoard`: a board * fails when the outer fibre reaches its rupture stress, so what runs out is * force. A plate fails when a hole has been opened all the way through, so * what runs out is WORK — force through the thickness. The two props take * different units because they fail by different mechanisms, and pretending * otherwise would be tidier and wrong. */ strike(joules: number): boolean; reset(): void; } declare function createArmour(options?: ArmourOptions): ArmourProp; export { AIR_ABSORPTION, ALLOYS, ALLOY_NAMES, AMMO, AMMO_KINDS, A_WEIGHTING, type AircraftInput, type AircraftProp, type Alloy, type AlloySpec, type AmmoBoxOptions, type AmmoDump, type AmmoKind, type AmmoSpec, type ArmourOptions, type ArmourProp, type AudioPulse, BAND_HZ, BARRIER_CAP, BASIN_ERAS, BERTH_ERAS, type Ballistics, type BananaOptions, type Band, type BandLevels, type BandolierOptions, type BannerOptions, type BannerPattern, type BannerStyle, type BarrierSpec, type Basin, type BasinEra, type BasinOptions, type BatOptions, type BathroomOptions, type Beach, type BeachOptions, type BeachProp, type BeachUmbrellaOptions, type Beacon, type BeaconOptions, type BedOptions, type BedSize, type BeltOptions, type Berth, type BerthEra, type BerthOptions, type BoardGame, type BoardOptions, type BoardShape, type BoardStack, type BoardState, type BoardStrength, type Bollard, type BookStyle, type BooksOptions, type BouncePad, type BouncePadOptions, type Breakable, type BreakableKind, type BreakableOptions, type BreakableState, type BreathPulse, type BuntingOptions, type BushOptions, CLUTTER_THEMES, COLD_ERAS, COOKWARE_KINDS, CRAFT_FITS, CREASE_FRONT, type CampCircleOptions, type CandleOptions, type CandleStyle, type Carrier, type CarryableOptions, type CartCargo, type CartOptions, type CartStyle, type CaseKind, type CasingOptions, type ChargeOptions, type Checkpoint, type CheckpointOptions, type CheckpointState, type ChestOptions, type ChillField, type CladdingOptions, type CladdingStyle, type ClutterKitOptions, type ClutterOptions, type ClutterTheme, type ColdDoor, type ColdEra, type ColdOptions, type ColdState, type ColdStore, type Companionway, type Compartment, type Consist, type ConsistOptions, type ContainerOptions, type Conveyor, type ConveyorOptions, type CookState, type Cookware, type CookwareKind, type CookwareOptions, type Countable, type CraftFit, type CraftInput, type CraftOptions, type CraftProp, type CraftState, type CrateOptions, type CricketBallProp, type CricketGround, type CricketGroundOptions, type CrockeryOptions, type CrumbleOptions, type CrumbleState, type CrumblingPlatform, type CurtainStyle, type Curtains, type CurtainsOptions, type CushionOptions, DRESSER_KINDS, type DanceTiles, type DanceTilesOptions, type DeckField, type DeckLevel, type DeckedShip, type DeckedShipOptions, type DeskSet, type DiningTableOptions, type Direction, type DoorOptions, type Draw, type DrawerOptions, type DresserDoor, type DresserKind, type DresserOptions, type DumpOptions, EXTRACTOR_ERAS, type EchoReading, type EchoState, type EwerOptions, type Extractor, type ExtractorEra, type ExtractorFan, type ExtractorOptions, type FenceOptions, type FieldTrigger, type FighterInput, type FighterOptions, type FighterProp, type Fill, type FillOptions, type FinishGate, type FinishGateOptions, type FireOptions, type Fixture, type FixtureOptions, type FixtureStyle, type FoldedOptions, type FountainOptions, type FrameStyle, type FramedPhotoOptions, type FruitBowlOptions, GEAR_KINDS, type GameTableOptions, type Gangway, type GangwayOptions, type GateOptions, type GateProp, type GateStyle, type GatheringOptions, type Gear, type GearKind, type GearOptions, type GearState, type GrassOptions, type GuitarOptions, HARMFUL, HOLD_KINDS, type Hand, type HangarOptions, type HangingPlantOptions, type HatchOptions, type HeadKind, type HeatControl, type HeatEra, type HeatField, type HeatOptions, type HeatSource, type HeatState, type HeatZone, type HelicopterInput, type HelicopterOptions, type HelicopterProp, type HelipadOptions, type Hold, type HoldKind, type HoldOptions, type HouseOptions, INGREDIENT_KINDS, type ImpostorOptions, type ImpostorProfile, type Ingredient, type IngredientForm, type IngredientKind, type IngredientOptions, type IngredientState, type Jacuzzi, type JacuzziOptions, type KegOptions, type KitchenwareOptions, type Ladder, type LadderOptions, type LadderStyle, type Lagoon, type LagoonOptions, type LampOptions, type LanternLightOptions, type LanternOptions, type LaptopOptions, type LeverOptions, type LifeguardTowerOptions, type LoaderOptions, type LoaderStyle, type Loading, type LongBenchOptions, type LoudnessState, type LoungerOptions, type LoungerRecline, type Luminous, type LuminousClaim, MARK_KINDS, MIAMI_COLORS, type MailShape, type MailStrength, type Manipulable, type MarkKind, type MatSpot, type MechanismOptions, type MirrorOptions, type ModernWindowOptions, type ModernWindowProp, type ModernWindowStyle, type Moorable, type Mooring, type MooringLine, type MooringOptions, type MovingPlatform, NM, type NeonSign, type NeonSignOptions, OAR_GRIP, OAR_KINDS, type Oar, type OarBank, type OarBankOptions, type OarKind, type OutletKind, type OutletOptions, type PAEra, type PAOptions, PITCH_LENGTH, PITCH_WIDTH, PLANT_SPECIES, PREP_KINDS, type PaintingOptions, type PalmOptions, type PapersOptions, type Pendulum, type PendulumOptions, type PergolaOptions, type Photocell, type PhotocellOptions, type Pickup, type PickupField, type PickupFieldOptions, type PickupKind, type PickupOptions, type PickupState, type PicnicTableOptions, type PinboardOptions, type PlaneOptions, type Plant, type PlantOptions, type PlantSpecies, type PlanterOptions, type PlateShape, type PlateStrength, type PlatformMotion, type PlatformOptions, type Plumbing, type PlumbingOptions, type Pool, type PoolLadder, type PoolOptions, type PoolStyle, type PortcullisOptions, type PosterOptions, type PrepKind, type PrepOptions, type PrepStation, type PressureGauge, type PressureGaugeOptions, type PressurePlate, type PressurePlateOptions, type PublicAddress, QUIET, RADIO_STATIONS, RIG_KINDS, type RackOptions, type RadioMedia, type RadioStation, type RailingOptions, type RailingStyle, type RevolvingBeaconOptions, type RigKind, type RockOptions, type RollingStock, type RollingStockOptions, type RoofStyle, type Round, type RoundOptions, type RugOptions, type RugShape, type RuinOptions, type Runway, type RunwayOptions, SCALD, SECTOR_TRANSMISSION, SHALA_ERAS, SHIP_ERAS, SHOUTING, SHOUT_AT_1M, SINK_ERAS, SMOKE_STYLES, SPEED_OF_SOUND, STABILISER_KINDS, STEAM_KINDS, STUMP_HEIGHT, STUMP_SPREAD, SUPPLY_KINDS, type SailOptions, type SailRig, type Scoreboard, type ScoreboardOptions, type ScreenCarryable, type ScreenLight, type ScreenLightOptions, type ScreenProp, type ScreenPropOptions, type Seamark, type SeamarkOptions, type SeatOptions, type SeatStyle, type Sector, type Shala, type ShalaEra, type ShalaOptions, type ShelfOptions, type ShelfStock, type ShipEra, type ShipInput, type Shower, type ShowerOptions, type ShowerState, type ShowerStyle, type SightState, type Sighting, type SignKind, type SignOptions, type SingingBowl, type SingingBowlOptions, type SinkDoor, type SinkEra, type SinkOptions, type SmallCraft, type SmallCraftOptions, type SmokeField, type SmokeLayer, type SmokeLayerOptions, type SmokeOptions, type SmokeSource, type SmokeStyle, type SoundArrival, type SoundField, type SourceSpec, type SpaceKind, type SpikeTrap, type SpikeTrapOptions, type Spray, type SprayOptions, type StabiliserKind, type StabiliserOptions, type Stabilisers, type StallGoods, type StallOptions, type StationPlatform, type StationPlatformOptions, type StatueFigure, type StatueMaterial, type StatueOptions, type Steam, type SteamControl, type SteamKind, type SteamOptions, type SteamPlant, type SteamPlantOptions, type SteamState, type StickyNotesOptions, type StockOptions, type Storage, type StorageSpace, type Stream, type StreamOptions, type StreetLightOptions, type StringLightsOptions, type Stumps, type StumpsOptions, type SupplyKind, type SupplyState, TABOR, TIMBERS, TIMBER_NAMES, type TableOptions, type TableStyle, type TackOptions, type TackStyle, type Tap, type TapOptions, type TapStyle, type TapestryOptions, type TargetDummy, type TargetDummyOptions, type TelevisionOptions, type Terminal, type TerminalOptions, type TerminalStyle, type ThrowOptions, type Timber, type TimberSpec, type TowerOptions, type TreadmillOptions, type TreadmillProp, type TreeLODOptions, TreeOptions, TreeSpecies, type TrimState, type TrinketOptions, type TropicalTree, type Tub, type TubOptions, type TubStyle, UTENSIL_STYLES, type UtensilOptions, type UtensilStyle, VESSEL_STYLES, type ValveOptions, type VehicleInput, type VehicleOptions, type VehicleProp, type Vessel, type VesselOptions, type VesselStyle, type WagonOptions, type WallArt, type WallClock, type WallClockOptions, type WallStyle, type WashQueue, type WashUp, type WellOptions, type WhiteboardOptions, type WindSource, type WindowBoxOptions, type Windsock, type WindsockOptions, type Woofer, type WooferOptions, type WooferState, type WorkStation, type WorkStationOptions, type WorkshopOptions, type Zone, type ZoneOptions, ballisticsOf, barrierLoss, bedPulse, boardStrength, chargeVelocity, createAmmoBox, createAmmoDump, createArmour, createBananaTree, createBandolier, createBanner, createBarrel, createBasin, createBasket, createBat, createBathtub, createBeach, createBeachUmbrella, createBeacon, createBed, createBelt, createBerth, createBike, createBoard, createBoat, createBooks, createBouncePad, createBrazier, createBreakable, createBridle, createBunting, createBush, createCampCircle, createCampfire, createCandle, createCar, createCarriage, createCart, createCasing, createCharge, createCheckpoint, createChest, createChoppingBlock, createCladding, createClutter, createColdStore, createConsist, createConveyor, createCookpot, createCookware, createCounter, createCrate, createCricketBall, createCricketGround, createCrockery, createCrumblingPlatform, createCurtains, createCushion, createDanceTiles, createDeckedShip, createDeskSet, createDiningTable, createDishwasher, createDoor, createDrawer, createDresser, createEwer, createExtractor, createFence, createFighterJet, createFill, createFinishGate, createFixture, createFolded, createForge, createFountain, createFramedPhoto, createFridge, createFruitBowl, createGameTable, createGangway, createGate, createGear, createGrassTuft, createGuitar, createHangar, createHangingPlant, createHatch, createHearth, createHeatSource, createHelicopter, createHelipad, createHob, createHold, createHouse, createIcebox, createImpostor, createIngredient, createJacuzzi, createKitchenSink, createKitchenware, createLadder, createLagoon, createLamp, createLantern, createLanternLight, createLaptop, createLarder, createLever, createLifeguardTower, createLoader, createLocomotive, createLongBench, createLoom, createLounger, createMagazine, createMirror, createModernWindow, createMonitor, createNeonSign, createOarBank, createOreVein, createOven, createPA, createPainting, createPalm, createPapers, createPendulum, createPergola, createPhone, createPhotocell, createPickup, createPickupField, createPicnicTable, createPinboard, createPlane, createPlant, createPlanter, createPlatform, createPlumbing, createPool, createPortcullis, createPoster, createPowderKeg, createPrepStation, createPressureGauge, createPressurePlate, createQuiver, createRack, createRailing, createRange, createReady, createRevolvingBeacon, createRock, createRound, createRug, createRuin, createRunway, createSack, createSaddle, createSailRig, createSawhorse, createScoreboard, createScreenLight, createSeamark, createSeat, createShala, createShelf, createShip, createShower, createSign, createSingingBowl, createSink, createSmallCraft, createSmartDisplay, createSmartwatch, createSmoke, createSmokeLayer, createSpikeTrap, createSpray, createStabilisers, createStall, createStationPlatform, createStatue, createSteam, createSteamPlant, createStickyNotes, createStream, createStreetLight, createStringLights, createStumps, createTable, createTablet, createTap, createTapestry, createTargetDummy, createTelevision, createTerminal, createThrow, createToilet, createTower, createTractor, createTreadmill, createTrinket, createTrough, createTruck, createTub, createUtensil, createValve, createVessel, createWagon, createWallClock, createWashUp, createWell, createWhiteboard, createWindowBox, createWindsock, createWoofer, createZone, dampingAt, describeAmmo, earshot, expansionRatio, exposureLimit, firesVisibleFrom, freeSurfaceCost, geographicRange, headPressure, holdPoint, isBreaking, keepsFor, listFor, livesIn, loudnessState, luminousRange, mailStrength, mixFor, mixedAt, moor, noGoDegrees, oarGripAt, orificeFlow, plateStrength, pressureFor, roundTriangles, spoilRate, spreadingLoss, stackStrength, steamPerWork, stock, sumDecibels, tempFor, treeLOD };