# Runek docs (full) Every page of https://runek.nullorder.org/docs as one Markdown file: guides first, then every component. The index is https://runek.nullorder.org/llms.txt; the changelog is https://runek.nullorder.org/docs/changelog.md. # What is Runek? Source: https://runek.nullorder.org/docs/what-is-runek > A source registry of procedural 3D components for React Three Fiber — shadcn for 3D worlds. Runek is a **source registry** of procedural 3D components for [React Three Fiber](https://r3f.docs.pmnd.rs/). You pull a component's source into your project with one command and own it — no black box, no version lock. Every component — a bookshelf, a lake, a whole house — generates its own geometry from **props and a `seed`**. There are no `.glb` files, no textures, no CDN. A whole world is just data: diffable, forkable, and version-controlled like any repo. ## The five principles (the moat) 1. **Procedural-first** — geometry from props + `seed`; no binary assets. 2. **A world is data** — every component is a pure, deterministic function of its props. 3. **Seeded determinism** — same seed → same result, everywhere. 4. **Parametric LOD** — detail scales with props and distance. 5. **Local-first** — no backend; a world deploys as a static site. This very documentation is a Runek world: a walkable library built from the components it documents. Read the [getting started guide](https://runek.nullorder.org/docs/getting-started.md) to compose your first scene. --- # Getting started Source: https://runek.nullorder.org/docs/getting-started > Pull components into your project with the CLI and compose your first walkable world. Runek ships as a source registry. The `runek` CLI copies editable component source into your project and installs what it needs. ## Install components ```bash npx @runek/cli init # writes runek.config.json + the install dir npx @runek/cli add player terrain bookshelf # pulls source + installs deps npx @runek/cli list # browse the catalog ``` `add` resolves dependencies for you: it installs `@runek/core` (the `` provider, seeded `rng`, and contract types) from npm, and `house` pulls the walls, floor, roof, door, and window it composes from as source. Working with a coding agent? Point it at [Runek for agents](https://runek.nullorder.org/docs/for-agents.md) (or [llms.txt](https://runek.nullorder.org/llms.txt)). ## Compose a world The CLI copies components into `src/runek/` by default; they import the runtime from `@runek/core`, which it installs for you: ```tsx import { World } from '@runek/core' import { Bookshelf } from './runek/Bookshelf' import { Player } from './runek/Player' import { Terrain } from './runek/Terrain' export function FirstWorld() { return ( ) } ``` `` sets up the canvas, lighting, physics, and keyboard controls. `` is a first-person controller by default — drop in and walk: WASD moves, the arrow keys or a mouse-drag steer the camera. The world's [`avatar` and `controls` settings](https://runek.nullorder.org/docs/the-world-provider.md#controls) change the view and the bindings. ## Same seed, same world Geometry is a pure function of props and `seed`. The same `seed` produces the same bookshelf — the same books, the same arrangement — on every machine, every render. Change the seed to roll a new variation; commit the number to lock it forever. --- # The workshop Source: https://runek.nullorder.org/docs/the-workshop > Build a world in your browser, tune every prop, edit it as JSON or live TSX, put one component in the lab, and share it all as a link. The [workshop](https://runek.nullorder.org/workshop) is Runek's in-browser workbench: the room next to the library, where the world you build renders live and the tools float over it. Nothing is installed and nothing leaves your browser. The whole session is a `WorldData` value, so a share link is just that value, compressed into the URL. You can also walk there: in the [library](https://runek.nullorder.org/library), the door on the right wall with the green ring leads to it. ## Three modes - **Build** composes a world. The **Outliner** lists the nodes (drag to reorder or nest them under a `Group`), and its **+ Add**, **Templates** and **Snippets** tabs drop things at the point the camera looks at. Click anything in the room to select it, then move or rotate it with the gizmo (`g`, `r`), or edit it in the **Inspector**. - **Lab** puts one component on a pedestal with instruments: bounds in meters, a 1.8 m figure for scale, wireframe, normals, colliders, triangle and draw-call counts, and how long it took to build. The **seed grid** shows 9, 16 or 25 seeds side by side; **A / B** compares two prop sets; **check determinism** builds the component twice and compares geometry hashes. - **Code** opens the code panel: `world.json`, a live `App.tsx`, and the install commands for your world. ## Every prop, generated The inspector is not hand-written. At build time the docs site reads each component's TypeScript (its props interface, the defaults in its signature, the JSDoc) and turns it into a schema. Numbers get sliders, colors get swatches that show the palette slot they fall back to, string unions become segmented choices or dropdowns, `[x, y, z]` gets three scrubbable fields, and nested specs (a `Person`'s `clothes`, a `Wall`'s `openings`) get list editors. A dot marks props you changed, and ⟲ resets one to its default (it disappears from the file). Drag a number's label sideways to scrub it. Callbacks and React children can't live in a world file; the inspector lists them under **Code-only props**, to use from `App.tsx`. **Export → Prop schema** downloads the whole schema as JSON. ## world.json and App.tsx The `world.json` tab is the world itself, two ways: edit the room and the file updates; edit the file and the room follows (after a short pause). Completion knows component names, props, enum values and palette slots; inside a `nodes` array, type `node:` for a node or `snippet:` for a group. If the JSON is broken, the room keeps the last good world and the error is marked in the gutter. **minimal** hides props equal to their defaults. `App.tsx` is a real React file that runs live. It starts generated from your world, either rendering the JSON with `WorldRenderer` (the *data* style) or spelled out component by component (the *JSX* style). `import world from './world.json'` always gives the current world, so the inspector keeps working while your code runs. Export a `registry` with your own components and the outliner accepts them as node types. The **new ▾** menu has examples: a custom seeded component, a `useFrame` animation, interactions and click handlers, and a world generated in a loop. The code runs in a sandboxed iframe with its own origin: it can't read this site's storage, cookies or page, and it can import only React, three.js, React Three Fiber, drei, Rapier, `@runek/core` and `@runek/components`. A loop that never yields is stopped. Links that carry code ask before running it. ## Stages The room around your world is a stage, not part of it, and it is never exported: - **Room**: the Workshop hall, with a 20 × 20 m build plot. - **Plot**: an open plot under the sky, for terrain and islands. - **None**: your world alone, exactly as your app would render it. ## Walk it **▶ Play** (or `p`) hides the tools and drops a first-person `Player` where the camera was looking (or uses your world's own `Player`). `Esc` brings you back. In the Room stage, walking out of the door leads back to the library. ## Take it home - **Share → Copy link** puts the world, the stage and the camera in the URL. There is no server: anyone with the link gets the same world, rebuilt from the same seeds. - **Export** downloads `world.json`, `App.tsx`, a PNG of the view (2×, optionally transparent), or the prop schema. - The **Install** tab lists the exact `npx @runek/cli add …` command for the components your world uses. - Drop a `.world.json` anywhere on the page to open it, or link to one: `/workshop?src=https://…/world.json` loads it from any static host that allows it. Your work autosaves in the browser. **Templates** start you from a reading room, a village square, an island cove, an office, a campsite at dusk, or a line-up of people. ## Keyboard | Keys | | |---|---| | `⌘K` | Command palette: add anything, load templates, jump to a node | | `⌘Z` · `⇧⌘Z` | Undo · redo | | `g` · `r` | Move · rotate | | `f` · `Home` | Frame the selection · frame everything | | `d` · `⌫` | Duplicate · delete | | `⌘C` · `⌘V` | Copy · paste nodes as JSON (paste a whole world too) | | `p` · `Esc` | Play · back | | `Tab` | Hide or show all panels | | `1` `2` `3` | Build, Lab, Code | | `⌘↵` | Run App.tsx | | `?` | All shortcuts | --- # The World provider Source: https://runek.nullorder.org/docs/the-world-provider > sets up the canvas, lighting, physics, and controls — every component lives inside it. `` is the root of every Runek scene. It wires up the React Three Fiber canvas, a default light rig, the Rapier physics world, and keyboard controls, then exposes scene-wide settings to its children through context. ```tsx import { World } from '@runek/core' import { Bookshelf } from './runek/Bookshelf' import { Player } from './runek/Player' ``` ## What it sets up - A `` with shadows and a sensible default camera. - A default **light rig** (ambient + a shadow-casting sun). Turn it off with `lights={false}` to supply your own — e.g. ``. - A Rapier `` world. Every component registers its colliders here. - `KeyboardControls` so `` (and your own input) work out of the box. ## Props | Prop | Type | Default | Description | |---|---|---|---| | `unit` | `number` | `1` | World scale: `1` unit = 1 meter. Read it via `useWorld()`. | | `gravity` | `[x, y, z]` | `[0, -9.81, 0]` | Physics gravity. | | `lights` | `boolean` | `true` | Render the default lights. Set `false` to use your own. | | `palette` | `Partial` | built-in | Override color slots; every component re-themes at once. | | `controls` | `Record` | built-in | Remap input bindings (action → key codes); serializable. | | `fog` | `{ color, near, far }` | off | Linear distance fog; pair the color with the sky's horizon. | | `input` | `boolean` | `true` | Read the keyboard. Set `false` to ignore it, e.g. while a modal is open. | | `paused` | `boolean` | `false` | Stop rendering and physics until set back to `false`. | | `debug` | `boolean` | `false` | Draw Rapier collider wireframes. | ## Controls Input bindings are a world setting, not a hardcode. The defaults move with WASD and steer the camera with the arrow keys (Left/Right turn, Up/Down look — in first *and* third person, composing with mouse-drag). Override any subset; the rest keep their defaults: ```tsx {/* AZERTY-friendly: ZQSD moves too */} ``` Keys are `KeyboardEvent.code` names. Bind an action to `[]` to disable it, or add your own action names — they become bindings any component can read with drei's `useKeyboardControls`. The resolved map is available to components at `useWorld().controls`. For a completely custom drei map there's still the low-level `keyboardMap` prop, which wins verbatim when given. ## Sharing the page A world often sits beside ordinary UI: a chat box, a settings form, a modal. Keys typed into a text field (`input`, `textarea`, `select`, or anything `contenteditable`) are left to the page, so typing "wasd" into a form never walks the avatar. To ignore the keyboard entirely, pass `input={false}`. Keys held at that moment are released, and so are keys held when the window loses focus. Mouse look listens on the canvas only, so anything drawn over the canvas already takes the pointer. When the world isn't on screen (a background tab of your app, a hidden panel), `paused` stops the frame loop and the physics steps so the world costs nothing: ```tsx {/* … */} ``` On resume, animations carry on from where they stopped, and anything driven by the wall clock (a walking `Person`) is already where it should be. `WorldRenderer` and `WorldEditor` take both props too. ## The palette Components default their colors to named slots — `wood`, `wall`, `foliage`, `waterDeep`, and friends — read from context. Override any subset on `` and the whole scene re-themes; explicit color props on a component still win: ```tsx {/* frame picks up the new wood */} {/* leaves pick up the new foliage */} {/* explicit prop beats the palette */} ``` The full slot list is the `WorldPalette` interface in `@runek/core`. ## useWorld() Components read the shared scale from the provider so they size consistently: ```tsx import { useWorld } from '@runek/core' function MyThing() { const { unit, palette } = useWorld() const width = 2 * unit // 2 meters, whatever the world's scale const color = palette.wood // themed with the rest of the world // … } ``` While a `Player` is mounted, `useWorld().player?.current` is its avatar (an `Object3D` at eye height), so a component can react to where the player *is* rather than where the camera is. In third person those differ: the camera trails a few meters behind. Read it inside `useFrame`, and fall back to the camera when it's `null` (no `Player`, e.g. an orbit view). Next: [the component contract](https://runek.nullorder.org/docs/the-component-contract.md) — the rules every component follows. --- # Runek for agents Source: https://runek.nullorder.org/docs/for-agents > The fast path for a coding agent: find components, install them, write a world as JSON, check it, and preview it. Runek is built to be driven by coding agents as much as by people. A world is JSON, every prop is described by a published schema, and the CLI checks your work and prints errors you can act on. This page is the short version of everything else in these docs. ## 1. Discover - **[llms.txt](https://runek.nullorder.org/llms.txt)** indexes every guide and component with a one-line description. **[llms-full.txt](https://runek.nullorder.org/llms-full.txt)** is every page in one file. - Every docs page has a Markdown twin: add `.md` to its URL (`/docs/components/bench.md`). - From a terminal: ```bash npx @runek/cli list --json # the catalog: name, title, category, description npx @runek/cli info bench # one component: props, types, defaults, deps, an example npx @runek/cli info bench --json # the same, as JSON ``` - The machine-readable sources behind all of this: - [`/r/registry.json`](https://runek.nullorder.org/r/registry.json): the catalog. - [`/r/props.json`](https://runek.nullorder.org/r/props.json): every prop of every component (type, default, palette slot, doc), keyed by the world-data `type`. - [`/r/world.schema.json`](https://runek.nullorder.org/r/world.schema.json): a JSON Schema for world files. - `/r/components/.json`: one component's full source plus its dependencies. ## 2. Install ```bash npx @runek/cli init # once: writes runek.config.json, creates src/runek/ npx @runek/cli add player terrain bench sky # copies source into src/runek/, installs npm deps ``` `add` resolves everything a component needs (other components as source, npm packages including `@runek/core`). Pass `--no-install` to print the install command instead of running it. The copied files are yours to edit. ## 3. Compose a world Write the world as data. Start the file with `$schema` so editors (and you) get validation and completion: ```json { "$schema": "https://runek.nullorder.org/r/world.schema.json", "version": 1, "time": "17:30", "nodes": [ { "type": "Sky" }, { "type": "LightRig" }, { "type": "Terrain", "props": { "size": [80, 80], "seed": 3 } }, { "type": "Bench", "anchor": "ground", "props": { "position": [2, 0, -4], "rotation": [0, 1.57, 0] } }, { "type": "Player", "props": { "position": [0, 2, 6] } } ] } ``` Render it with `WorldRenderer` and a registry map of the components you added (keys are the node `type`s): ```tsx import { type ComponentRegistry, parseWorld, WorldRenderer } from '@runek/core' import { Bench } from './runek/Bench' import { LightRig } from './runek/LightRig' import { Player } from './runek/Player' import { Sky } from './runek/Sky' import { Terrain } from './runek/Terrain' import worldJson from './world.json' const registry: ComponentRegistry = { Bench, LightRig, Player, Sky, Terrain } const world = parseWorld(JSON.stringify(worldJson)) export const App = () => ``` Or skip the JSON and write the same world as JSX inside `` (see [Getting started](https://runek.nullorder.org/docs/getting-started.md)). Both are equivalent; JSON is easier to generate, diff, and check. ## 4. Check ```bash npx @runek/cli validate world.json # unknown types, unknown props, wrong value types, with "did you mean" npx @runek/cli check-world world.json # things buried in or floating above the ground npx @runek/cli check-world world.json --fix # write the suggested heights back into the file ``` Both take `--json` for structured output and `--strict` to exit non-zero (for CI). Fix what `validate` reports before you look at the scene: a misspelled prop renders silently with its default. ## 5. Preview ```bash npx @runek/cli preview world.json ``` prints a link to the [workshop](https://runek.nullorder.org/docs/the-workshop.md) with your world loaded: walk it, tune props in the inspector, and copy the edited JSON back. Nothing is uploaded; the world travels in the link itself. ## Conventions - **Units:** 1 unit = 1 meter. Y is up. Rotations are radians, as `[x, y, z]` Euler angles (a quarter turn about the vertical axis is `[0, Math.PI / 2, 0]`, in JSON `[0, 1.5708, 0]`). - **`type`** is the PascalCase component name (`Bookshelf`, not `bookshelf`). `add` takes the lowercase registry name; the node takes the title. `Group` is a built-in container for `children`. - **Props go under `props`.** Node-level keys are only `type`, `id`, `anchor`, `props`, and `children`. - **`seed`** (an integer) picks a variation. The same seed always gives the same geometry; change it to roll a new one, keep it to lock one in. Every component accepts `seed`, but some have nothing to vary: `props.json` marks those `"seeded": false`, so don't spend effort picking seeds for them. - **`anchor: "ground"`** makes `position[1]` a height above the terrain at that spot, so nothing ends up buried. Use `"surface"` to stand on the highest thing there (a dock, a floor). See [Placing things on uneven ground](https://runek.nullorder.org/docs/placing-things-on-uneven-ground.md). - **Colors** are CSS color strings. Leave them unset to follow the world `palette`, which re-themes every component at once. - **Callbacks and React children** (`onEnter`, `onSelect`, ...) can't live in JSON. The props tables mark them "code only"; use them from TSX. - Only ever use props listed in the component's props table or in `props.json`. Defaults are listed too, so leave out anything you don't need to change. ## Common mistakes | Mistake | Fix | |---|---| | `{ "type": "bench" }` | `{ "type": "Bench" }`: the PascalCase title. | | `{ "type": "Bench", "position": [0, 0, 0] }` | Put it under `props`: `{ "type": "Bench", "props": { "position": [0, 0, 0] } }`. | | `"rotation": [0, 90, 0]` | Radians: `[0, 1.5708, 0]`. | | Props float or sink on hilly terrain | Add `"anchor": "ground"` and set `position[1]` to `0`, or run `check-world --fix`. | | A component renders but isn't in your app | It needs `npx @runek/cli add ` and an entry in your registry map. | | No `"version": 1` | Every world file starts with it. | ## The agent skill The same workflow, condensed into a skill file your coding agent loads on its own when you work with Runek: ```bash npx @runek/cli skill # writes .claude/skills/runek/SKILL.md npx @runek/cli skill --out path/to/SKILL.md # anywhere else your agent reads skills from ``` The source is served at [`/r/agents/SKILL.md`](https://runek.nullorder.org/r/agents/SKILL.md). --- # The component contract Source: https://runek.nullorder.org/docs/the-component-contract > Every Runek component is a pure, deterministic function of its props — here are the rules. Runek components all follow the same small contract. Honor it and a component is composable, serializable, and swappable with any other. ## The rules 1. **Accept `position`, `rotation`, `seed`.** These are the shared base props (`WorldComponentProps`). Geometry is generated from them — nothing else. 2. **Generate geometry in `useMemo`, keyed on every geometry-affecting prop** (including `seed`). Same inputs → same mesh, no rebuild on unrelated renders. 3. **Register your own colliders** — a Rapier `RigidBody`. Keep collider count proportional to gameplay surface, not visual detail (one cuboid for a bookshelf, not one per book). 4. **Respect `unit`** from `useWorld()` so the component scales with the world. 5. **No assets.** Geometry and color come from code; no `.glb`, no textures. 6. **Default colors from the palette.** Read `palette` from `useWorld()` and use its slots (`wood`, `wall`, `foliage`, …) as your color defaults; an explicit color prop always wins. One palette swap then re-themes the whole world. 7. **Instance repeated geometry.** Books, branches, blades — render them as one `InstancedMesh`, not one mesh per piece, so worlds stay cheap at scale. 8. **Publish your surface.** If people can stand on your component, attach `Component.surface` (its walkable top, as a pure function in a `surfaces/.ts` module). If it stands on the ground, set `Component.groundSitting = true`. That's what lets worlds anchor to it and `runek check-world` catch buried or floating props. ## A minimal compliant component ```tsx import { RigidBody } from '@react-three/rapier' import { rng, useWorld, type Vec3 } from '@runek/core' import { useMemo } from 'react' export interface CrateProps { position?: Vec3 rotation?: Vec3 size?: number seed?: number } export function Crate({ position = [0, 0, 0], rotation = [0, 0, 0], size = 1, seed = 1 }: CrateProps) { const { unit } = useWorld() const s = size * unit const hue = useMemo(() => rng(seed)() * 0.1 + 0.05, [seed]) return ( ) } ``` Because the contract is uniform, a world is just a list of `{ type, props }` — see [worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md). Contributing a component? The normative spec (with a conformance checklist) is [`CONTRACT.md`](https://github.com/nullorder/runek/blob/main/CONTRACT.md), and the step-by-step workflow is [`CONTRIBUTING.md`](https://github.com/nullorder/runek/blob/main/CONTRIBUTING.md). --- # Seeded determinism Source: https://runek.nullorder.org/docs/seeded-determinism > Same seed → same world, on every machine and every render. The seeded RNG and its helpers. Procedural variety in Runek is **deterministic**: a component's randomness comes from a single `seed`, so the same seed always produces the same result. Commit the number and the world is locked forever; change it to roll a new variation. ## The RNG `rng(seed)` returns a stream — call it for the next value in `[0, 1)`: ```tsx import { rng } from '@runek/core' const next = rng(42) next() // 0.37… — and always the same first value for seed 42 next() // 0.81… ``` ## Helpers ```tsx import { rng, range, int, pick, sub } from '@runek/core' const next = rng(42) range(next, 0, 10) // a float in [0, 10) int(next, 1, 6) // an integer in [1, 6] (inclusive) pick(next, ['oak', 'pine']) // a stable choice from a list ``` ## Stable child seeds When a component composes others (a `Hut` framing its doorway, a composite instance seeding its arrangement), derive child seeds with `sub(seed, n)` so each child is deterministic *and* distinct — without threading the same stream through everything: ```tsx import { sub } from '@runek/core' ``` ## Why it matters - **Reproducible** — the same world renders identically everywhere. - **Diffable & forkable** — a world is data + seeds, so it lives in git like code. - **Cheap variety** — one number yields a whole new arrangement. --- # Units & colliders Source: https://runek.nullorder.org/docs/units-and-colliders > One unit is one meter; colliders track gameplay surface, not visual detail. ## Units - **1 unit = 1 meter**, **Y is up**, rotations are in **radians**. - Components multiply their dimensions by `unit` from `useWorld()`, so a whole world can be rescaled from one place (``). ```tsx const { unit } = useWorld() const tableHeight = 0.75 * unit // 75 cm ``` ## Colliders Every component registers its own physics colliders via a Rapier `RigidBody`. The guiding rule: **collider complexity should match gameplay surface, not visual detail.** A bookshelf is one cuboid you can't walk through — not one collider per book. | Shape | When | Example | |---|---|---| | `cuboid` | Box-like solids | Bookshelf, walls, crates | | Several fixed cuboids | Composed structures | Room (4 walls + floor) | | Convex hull | Faceted irregular solids | Rocks | | Trimesh | Arbitrary surfaces where visual = collision | Terrain | Trimesh is the most expensive — reach for it only when a shape genuinely needs arbitrary geometry (displaced terrain). Prefer cuboids and hulls everywhere else. Turn on `` to see collider wireframes while building. --- # The building kit Source: https://runek.nullorder.org/docs/building-kit > Compose buildings from parts — walls with openings, slabs with stairwells, stackable levels — and ship arrangements as composites. Buildings in Runek are composed, not configured. The coded layer is a small kit of parts; anything larger — a house, a room, a watchtower — is an *arrangement* of those parts, expressed as data. This guide covers the kit's conventions and the composite mechanism that packages arrangements. ## The parts - **`Wall`** — one straight run with rectangular `openings` (doors, windows) cut into it. Solid piers, sills, and lintels are emitted around the holes, all in one fixed collider. - **`Floor`** — a slab whose top surface is its origin, with an optional rectangular `opening` for a stairwell. - **`Level`** — a ring of four walls plus an optional slab: the stackable unit. Sides take per-side configs (`openings`, `color`, `present: false` to leave a side open). - **`Roof`** — flat or gable; a gable caps its own triangular ends so the attic isn't open to the sky. - **`Door`**, **`Window`**, **`Staircase`** — the fittings that live in and around the openings. ## Anchors and stacking Every part anchors so that composition is pure translation: - A `Wall` sits base-down at `y = 0`, centered on its length. - A `Floor`'s **top surface** is its origin (you stand at the component's `y`). - A `Level`'s origin is the base of its walls. - A `Roof`'s base rests at its origin. So a two-level building is arithmetic, no measuring: ```tsx ``` Level 2 sits at `y = 3` (level 1's height); the roof at `y = 3 + 2.6`. The upper slab's `opening` sits over the staircase run, so you can actually walk up through it — the slab splits into strips around the hole and the colliders match the visual exactly. ## Composites: arrangements as data A **composite** is a named arrangement of nodes, held in the registry as JSON instead of code. `House` and `Room` are composites: `runek add house` copies `composites/house.json` plus the source of every part it references. Place one in a world like any component — the renderer expands the arrangement in place, wrapped in a group carrying your transform: ```json { "type": "House", "props": { "position": [12, 0, -4], "seed": 7 } } ``` Two behaviors worth knowing: - **Seeds cascade.** An instance `seed` deterministically re-rolls every child in the arrangement that doesn't pin its own (`sub(seed, index)` under the hood). A street of houses with different seeds doesn't look copy-pasted. - **Unpack to customize.** In the editor, select a composite instance and hit **Unpack**: the instance is replaced by a `Group` node carrying the arrangement as ordinary, editable children. Move the plant, delete a window, stretch a level — it's all plain nodes afterward. (There's no live link back to the definition; an unpacked house is yours.) In JSX (outside worlds-as-data), render an arrangement through the same path with `WorldNodes`: ```tsx import { registry } from './runek/registry' import { WorldNodes } from '@runek/core' ``` ## Authoring your own composite Any world subtree is a composite waiting to happen. Arrange parts (in the editor or by hand), then save the nodes under a `kind: "composite"` wrapper: ```json { "kind": "composite", "name": "Watchtower", "description": "Three stacked levels under a flat roof.", "nodes": [ { "type": "Level", "props": { "size": [4, 4], "height": 3 } }, { "type": "Level", "props": { "position": [0, 3, 0], "size": [4, 4], "height": 3 } }, { "type": "Level", "props": { "position": [0, 6, 0], "size": [4, 4], "height": 3 } }, { "type": "Roof", "props": { "position": [0, 9, 0], "size": [4, 4], "style": "flat" } } ] } ``` Register it under a name and it's placeable everywhere: ```ts import watchtower from './composites/watchtower.json' const registry = { ...parts, Watchtower: watchtower as unknown as CompositeDef } ``` Composites are static arrangements by design — no template variables. The variation knobs are the instance `seed` and unpack-and-edit. (Deferred, by decision: streaming/lazy instantiation for big worlds; the `bounds` field in a definition is reserved for that.) --- # Worlds as data Source: https://runek.nullorder.org/docs/worlds-as-data > Because every component is a pure function of props, a whole world serializes to plain JSON. Every component is a deterministic function of its props, so an entire scene can be described as **data** — a list of `{ type, props }` — then rendered, saved, diffed, and edited like any file. ## The shape ```ts interface WorldData { $schema?: string // editor hint: https://runek.nullorder.org/r/world.schema.json version: 1 meta?: WorldMeta // the world's identity (title, authors, license, source) unit?: number gravity?: [number, number, number] ground?: number // baseline ground level (Y); floor + water default to it time?: string // pinned time-of-day "HH:MM" (reproducible day/night) timezone?: string // OR an IANA zone for a live, clock-driven day/night avatar?: 'first' | 'third' | 'overhead' // default player camera view controls?: Record // input remap: action → KeyboardEvent.codes palette?: Partial // color-slot overrides for the whole world fonts?: Partial // fonts the world ships, by role (display, body) fog?: { color: string; near: number; far: number } nodes: WorldNode[] } interface WorldNode { type: string // a registry key, e.g. "Bookshelf" id?: string // stable identity; the editor fills this in (see below) anchor?: 'ground' | 'surface' // position[1] is an offset above the ground here props?: Record children?: WorldNode[] } ``` A small world: ```json { "version": 1, "palette": { "wood": "#7a5a40" }, "nodes": [ { "type": "Terrain", "props": { "size": [40, 40] } }, { "type": "Bookshelf", "props": { "position": [0, 1, 0], "seed": 42 } }, { "type": "Player" } ] } ``` The look of a world is data too: `palette` re-themes every component at once and `fog` sets the atmosphere — both diff as cleanly as any node. ## Validate it Start a world file with a `$schema` line and editors check it and complete component names, props, and enum values as you type: ```json { "$schema": "https://runek.nullorder.org/r/world.schema.json", "version": 1, "nodes": [] } ``` The [world schema](https://runek.nullorder.org/r/world.schema.json) is generated from the [prop schema](https://runek.nullorder.org/r/props.json): every prop of every component with its type, default, and doc, read from the TypeScript source. `npx @runek/cli validate world.json` runs the same checks from a terminal (see the [CLI reference](https://runek.nullorder.org/docs/cli.md)), and `serializeWorld` keeps the `$schema` line when it saves. ## World settings (the rules) A handful of top-level fields are the world's **rules**: values components read through `useWorld()` to decide how the world looks and plays, not geometry. They resolve onto the world context the same way `unit`, `gravity`, and `palette` do — set once on the world, read everywhere. ```json { "version": 1, "time": "21:30", "avatar": "third", "nodes": [{ "type": "Sky" }, { "type": "LightRig" }, { "type": "Player" }] } ``` - **`time`** pins a fixed time-of-day (`"HH:MM"`, 24-hour), so the world is fully reproducible: the same file lights the same way every time. **`timezone`** (an IANA zone like `"Asia/Kolkata"`) makes the world *live* instead — the day/night state tracks the real clock, an explicit exception to determinism. A pin wins if both are set. Day/night-aware components read the resolved value as `useWorld().time`: `Sky` arcs the sun overhead by day and swaps to a dark, starlit dome at night, and `LightRig` follows it with golden-hour tints and dim moonlight. - **`avatar`** (`"first"`, `"third"`, or `"overhead"`) is the default camera view. `Player` uses it unless its own `view` prop is set — an explicit prop always wins. - **`controls`** remaps input bindings: a partial `action → KeyboardEvent.code[]` map merged over the defaults (WASD moves, arrow keys turn/look — in either view). Declare only what you change (`{ "forward": ["KeyW", "KeyZ"] }` for AZERTY), bind `[]` to disable an action, or add unknown action names as custom bindings your own components read via `useKeyboardControls`. Components see the resolved map at `useWorld().controls`. Controls are orthogonal to `avatar`: bindings behave the same in every view. - **`ground`** is the baseline ground level (a `Y` value, default `0`): the datum that `Floor` and water like `Lake` default their placement to. In a coastal world it is effectively your **sea level**, the terrain rises from it and the water sits at it, so moving `ground` shifts a whole world's datum at once (a sunken basin, a plateau). Open water sits at or below it (a floating lake is a bug); an explicit `position` still wins. - **`fonts`** are the typefaces the world ships, keyed by role (`display` for titles and signage, `body` for labels). They are the one exception to the no-assets moat: components hold no fonts, so the world declares them and `Sign` draws from them. An undeclared role falls back to the pixel font bundled in `@runek/core`, so `Sign` always renders even with no `fonts` set. Every setting is optional with a sensible default (a bright midday, first-person), so a world that declares none renders exactly as before. ## Render it `` maps each node's `type` through a registry of components: ```tsx import { WorldRenderer, parseWorld } from '@runek/core' import { registry } from './runek/registry' const world = parseWorld(await (await fetch('/my.world.json')).text()) ``` Two node types resolve without a component behind them: - **Composites** — a registry entry can be a data arrangement instead of code (`House`, `Room`). The renderer expands the arrangement in place, inside a group carrying the node's `position`/`rotation`; an instance `seed` re-rolls unpinned children deterministically. See [the building kit](https://runek.nullorder.org/docs/building-kit.md). - **`Group`** — a built-in transform container (`position`, `rotation`, `children`), what the editor's **Unpack** leaves behind when you break a composite instance open for per-instance edits. ## Save it `serializeWorld(data)` returns pretty JSON. The runtime editor (``) edits a `WorldData` live and exports it with the same call, so a world round-trips: render, edit, serialize, commit. The editor has its own entry point, so an app that only renders worlds doesn't ship its props panel. It needs `leva` installed alongside `@runek/core`: ```tsx import { WorldEditor } from '@runek/core/editor' ``` ## Identity & contribution A world is a creative work, so its identity travels *in the file* under `meta` (the way `package.json` carries a package's name, author, and `repository`): ```ts interface WorldMeta { title?: string description?: string authors?: { name: string; url?: string }[] license?: string // a world is content, so a CC license often fits better than MIT source?: { url: string; path?: string; branch?: string } // the canonical repo } ``` `meta` is optional, and so is every field within it. When present, both `` (walk) and `` (edit) show a small **ⓘ** that opens an "About this world" panel with the title, authors, license, and a link to the source repo. A world with no `meta` still renders. When `meta.source` points at a GitHub repo, the editor also gains a **Contribute** action with two paths. *Fork this world* opens GitHub's fork page for your own deployable copy. *Suggest changes upstream* downloads the edited JSON plus a PNG snapshot of the view and opens GitHub's edit-file URL, which auto-forks for non-collaborators, so your change becomes a normal pull request with no backend, account, or token. Non-GitHub hosts fall back to opening the repo. ## Anchored nodes On uneven `Terrain`, a fixed Y is only right where the ground happens to be that high. An `anchor` makes `position[1]` an offset above the ground at the node's (x, z) instead: `"ground"` stands on terrain, `"surface"` on whatever is highest there (terrain, a dock, a floor). ```json { "type": "Person", "anchor": "ground", "props": { "position": [3.2, 0, 115.4], "seed": 123 } } ``` The file keeps the offset, so moving or reseeding the terrain re-seats everything anchored to it. See [Placing things on uneven ground](https://runek.nullorder.org/docs/placing-things-on-uneven-ground.md). ## Stable node ids Each `WorldNode` can carry an optional `id`. You don't write these by hand: the editor assigns one to any node missing it on load and preserves the rest. Ids give nodes a durable identity across edits, and `serializeWorld` writes keys in a canonical order, so a pull request shows only what actually changed. --- # Placing things on uneven ground Source: https://runek.nullorder.org/docs/placing-things-on-uneven-ground > Anchor nodes to the terrain, drop them in the editor, and catch buried or floating props with runek check-world. `Terrain` displaces its surface with seeded noise, an island falloff, and a flat `flatRadius` pad. That's deterministic, but a world file only knows absolute numbers, so a prop placed at `y: 0.42` is right on the pad and wrong everywhere else. On a real island world, 16 of 56 people placed that way ended up underground past the pad, one buried to the head, alongside crates, signposts, a tent, and two houses nobody had noticed. Runek makes the ground queryable instead. Every surface-providing component (`Terrain`, `Shore`, `Dock`, `Floor`, `Slab`) publishes its walkable top as a pure function of its props, the same function that builds its mesh. So the height at any (x, z) is known exactly, without running physics or raycasting. ## Anchor a node Give a node an `anchor`, and its `position[1]` becomes an offset above the ground at its (x, z): ```json { "type": "Person", "anchor": "ground", "props": { "position": [3.2, 0, 115.4], "seed": 123 } } ``` - `"ground"` stands on terrain (`Terrain`, `Shore`). - `"surface"` stands on whatever is highest: terrain, or a deck like a `Dock`, `Floor`, or `Slab`. Use it for people on a jetty or crates in a house. `0` stands on the ground wherever the node is; `0.5` hovers half a unit above it. Any node can be anchored, including a `Group` or a composite such as `House`, and anchors resolve through parent transforms. The world file keeps the offset, never the resolved height, so reseeding or moving the terrain re-seats everything anchored to it. `Player` can be anchored too: an anchored spawn at `[x, 1.2, z]` starts 1.2 above whatever is there. Anchors resolve in `WorldRenderer` and `WorldEditor`; the component itself just receives an absolute `position`. Under a parent tilted by more than about a degree, "the ground below" stops being well-defined, so the anchor is treated as absolute and a warning is logged. ## Query the ground in code Inside a data world, `useGround()` returns the query: the highest surface at (x, z), or the world's `ground` baseline when nothing is there. ```tsx import { useGround } from '@runek/core' const groundAt = useGround() const y = groundAt(x, z) // terrain or deck const soil = groundAt(x, z, { kinds: ['terrain'] }) ``` Outside React (scripts, tests, build steps), the same query is pure and imports from the React-free `@runek/core/data` entry: ```ts import { createGroundIndex } from '@runek/core/data' import { dockSurface } from './runek/surfaces/dock' import { terrainSurface } from './runek/surfaces/terrain' const ground = createGroundIndex(world, { Terrain: { surface: terrainSurface }, Dock: { surface: dockSurface }, }) ground.groundAt(12, -40) ``` The surface modules are plain TypeScript with no React or three.js, so they run in Node as-is. ## Drop to ground in the editor With a node selected, the editor toolbar offers: - **Drop to ground** / **Drop to surface**: seat the selection on the terrain (or the highest surface) under it, writing an absolute Y. Undoable. - **Anchor**: switch between none, ground, and surface. The node stays where it is; its Y converts between an absolute height and an offset. Dragging an anchored node with the move gizmo slides it along the ground. Dragging its Y handle changes the offset instead. ## Check a world `runek check-world` flags every ground-standing node that is buried or floating, plus open water that stands above its shore: ```bash npx @runek/cli check-world public/world.json # buried Windmill [14] at (-26, 0.42, 96) is 2.08 below the ground (2.5); set position[1] to 2.498, or anchor it # floating Bench [91] at (-92.3, 17.05, 157) floats 2.02 above the surface (15.03); set position[1] to 15.025, or anchor it ``` `--fix` writes the suggested heights into the file, and `--strict` exits non-zero so you can put it in CI next to your other checks. Only components that declare themselves ground-sitting are checked (people, trees, furniture, buildings), and not birds, clouds, or signs hung in the air. Anchored nodes pass by construction. Two things it can't know: a prop stacked on another prop (a crate on a crate) or standing on something that isn't a surface (a rock, a cliff top) reads as floating. And trees on a slope are often sunk a little on purpose so the downhill side doesn't hover; `--tolerance` widens the allowed gap. --- # Embedding a world in an app Source: https://runek.nullorder.org/docs/embedding-a-world > Host a world beside ordinary UI, in a browser or a desktop webview. Content security policy, lazy loading, input that yields to the page, pausing, and figures driven by app state. A Runek world doesn't have to be the whole page. It can be one panel of a larger app: a map beside a sidebar, a room under a chat composer, a scene in a desktop app's webview. This guide covers what changes when it is. ## Content security policy A world needs three things a strict policy usually blocks. On top of whatever your app already allows: | Directive | Add | Why | |---|---|---| | `script-src` | `'wasm-unsafe-eval'` | Rapier, the physics engine, is WebAssembly. Without it the world fails to start. | | `script-src` | `blob:` | Text (`Sign`, a `Person`'s `label`, interaction prompts) is laid out in a worker that loads its code from `blob:` URLs. | | `connect-src` | `data:` | The default font bundled in `@runek/core` is a `data:` URL, fetched from that worker. Fonts your world declares in `fonts` need their own origins here too. | So a host that otherwise allows only itself ends up with: ``` default-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; connect-src 'self' data:; ``` `worker-src` falls back to `script-src`, so it needs nothing unless your policy sets it, in which case add `blob:` there too. `img-src` and `font-src` need nothing: no component loads an image or a CSS font. Development servers usually want more than this (inline scripts for hot reload), which is between you and your bundler. In Tauri the policy lives in `app.security.csp` in `tauri.conf.json`. ## Desktop webviews Every desktop webview runs WebGL2, which is all a world needs: - **macOS** uses WKWebView (Safari's engine). - **Windows** uses WebView2 (Chromium). - **Linux** uses WebKitGTK, the slowest of the three. A room or a house is fine; for a large world, lean on each component's level of detail and keep `Person` counts modest. ## Load it lazily three.js, Rapier, and drei are a large download. Put the world behind `React.lazy` so the rest of your app starts without them: ```tsx const Room = lazy(() => import('./world/Room')) export function RoomPage() { return ( Loading the room…

}>
) } ``` The editor ships from its own entry, `@runek/core/editor`, and is the only part that imports `leva`. An app that never mounts `WorldEditor` doesn't need `leva` installed. ## Input that yields to the page `World` reads the keyboard from the whole window, but never keys aimed at a text field, a select, or editable text, so typing "wasd" into a composer beside the canvas never walks the avatar. Pointer look listens on the canvas only, so panels drawn over it already take the pointer. Switch the keyboard off entirely while a modal is open: ```tsx … ``` Keys held when input switches off are released, so none stay stuck down. ## Pause when hidden A world in a tab nobody is looking at should cost nothing. `paused` stops the frame loop and physics: no rendering, no steps, no input. ```tsx … ``` Anything driven by the wall clock is simply where it should be on resume. A `Person` on a route was never stepping along it; its position is a function of the time, so pausing for an hour and resuming puts it exactly where an hour of walking would have. ## A view of the whole place An app showing many figures at once usually wants to open on all of them. `view="overhead"` puts the camera high above the avatar at a fixed tilt; it follows as the avatar walks, scroll zooms, and WASD walks relative to the screen. Give the world a `view` control and the player can switch between overhead, third person, and first person: ```tsx ``` ## Figures driven by app state When people in the world stand for something live in your app (agents, players, orders), give each one a trip whenever its state moves it. A trip is a `route` with a start time: ```tsx report(agent.id, trip.to)} /> ``` Before `departAt` the figure waits at the first point; after the last it stays put, facing the way it came, in its `pose`. Because the position is a function of `(route, departAt, now)`, two windows showing the same world agree, and reopening the page puts everyone mid-stride where they should be without replaying anything. `onArrive` fires once per trip, on the first frame the trip is over. That includes a trip that ended while the world was paused or not mounted at all, so the arrival is always reported; dedupe by `departAt` if your app remounts the world. Pathfinding stays in your app: compute the waypoints (a small graph through your doorways is plenty) and hand them over. If a new trip starts before the last one ended, `tripAt(waypoints, departAt, Date.now())` gives where the figure had got to, in the same node-local frame, as the start of the next. ## Actions near a figure Give a figure `actions` and a prompt appears over it when the avatar comes close, showing the key bound to each: ```tsx (id === 'talk' ? openChat(agent) : openSheet(agent))} /> ``` Only the nearest figure in range shows its prompt and takes the key, so two people standing together never both answer `T`. Keys come through the world's keyboard, so they yield to text fields, `input={false}`, and `paused` like everything else. The prompt reads its key names from the world's `controls`, so remapping `talk` relabels it. Anything else can offer actions the same way by wrapping it in `Interactable` (with `use: ['KeyE']` in the world's `controls`): ```tsx ``` --- # World-to-world travel Source: https://runek.nullorder.org/docs/world-to-world-travel > Portal is a sensor gate that fires when the avatar (or a vehicle) walks through it — wire it to swap worlds, change levels, or teleport. A Runek `` is a self-contained scene: its own canvas, its own physics world. To move between scenes — island to island, level to level, room to room — you need an in-world affordance that says "go here," and a host that listens. `Portal` is that affordance. ```tsx import { Portal } from './runek/Portal' // Data-only: walking through navigates the browser. // Or drive an in-app transition yourself: goTo('/foosha.world.json')} /> ``` ## The data and the event `Portal` follows the contract's rule for interactive components ([§1](https://runek.nullorder.org/docs/the-component-contract.md)): the *data* of the interaction stays JSON-serializable, and the callback is optional, so a world still renders and round-trips from data without any code wired up. - **`to`** — a URL or route, plain data. With no `onEnter`, entering the gate does `window.location.href = to` (the same fallback `Bookshelf` uses for a book's `href`). This is all a hand-authored, data-only world needs. - **`onEnter(to)`** — an optional callback. When set, it runs *instead* of navigating, so your app can react however it likes: fade the screen, swap the mounted world JSON, push a route. The destination `to` is passed straight through, so one handler can serve many gates. Because `onEnter` is optional and `to` is serializable, the gate is fully expressible in a `*.world.json` and still drives real navigation out of the box. ## The physics The trigger is a **Rapier sensor** (a collider that reports overlaps without blocking movement), sized a little larger than the visible ring. Two details make it robust: - **It fires for vehicles, not just walkers.** Rapier's default collision events only cover a *dynamic* body (the avatar capsule) against the fixed gate. `Portal` also enables `KINEMATIC_FIXED` active-collision types, so a **kinematic** vehicle — a scripted boat or cart moved with `setNextKinematicTranslation` — trips it too. - **It ignores the scenery.** The handler skips any *fixed* body (`bodyType() === Fixed`), so the gate never fires against the terrain or props it sits among — only against something that moves through it. The sensor is also kept deep along the approach axis so a fast vehicle can't tunnel past it between frames. The gate registers no solid collider — you can always walk through it; only the sensor matters. ## Reacting across the canvas boundary There's a catch worth knowing. `` (and ``) mount their own React Three Fiber ``, which React renders with a **separate reconciler**. React context from your page does **not** cross into the scene, and the scene can't reach your app shell through props. So an `onEnter` fired inside the world can't directly call `setState` in the component that rendered ``. Bridge it with a **module-level store** — a plain singleton both React roots import: ```ts // voyage.ts — shared by the in-world Portal and the app shell let pending: string | null = null const subs = new Set<() => void>() export const requestVoyage = (to: string) => { pending = to subs.forEach((fn) => fn()) } export const getVoyage = () => pending export const clearVoyage = () => { pending = null } export const subscribeVoyage = (fn: () => void) => { subs.add(fn) return () => subs.delete(fn) } ``` ```tsx // in the world: registered so JSON can place it // in the app shell, outside the canvas: const pending = useSyncExternalStore(subscribeVoyage, getVoyage) useEffect(() => { if (!pending) return fadeOut().then(() => { setWorldFile(pending) // swap the mounted world JSON clearVoyage() }) }, [pending]) ``` The store is the seam between the two worlds — the scene writes to it, the page reads from it. ## When to use it - **World-to-world travel** — the driving case: a hub or an open area with gates to other scenes. - **Level / area transitions** and **teleports** within a project. ## When not to For a *single continuous space*, don't teleport — move the camera or the player. A Portal is for crossing a seam between scenes, not for getting around inside one. --- # CLI reference Source: https://runek.nullorder.org/docs/cli > runek init / add / list / info / validate / check-world / preview / skill: pull editable component source into your project and check your worlds. The `runek` CLI copies component **source** from the registry into your project — you own and edit the files. Run it with `npx @runek/cli `. ## init ```bash npx @runek/cli init [options] ``` Writes `runek.config.json` and creates the install directory. Run it once per project. ## add ```bash npx @runek/cli add [options] ``` Pulls one or more components. For each, it: 1. resolves **registry dependencies** recursively (e.g. `house` also pulls its walls, floor, roof, door, and window as source), 2. writes the source into your install directory verbatim (components import `@runek/core` from npm, so there's no import to rewrite), 3. installs the npm **dependencies**, including `@runek/core`, with your package manager (auto-detected from the lockfile). ```bash npx @runek/cli add player terrain bookshelf ``` ## list ```bash npx @runek/cli list [options] ``` Prints the catalog, grouped by category. `--json` prints the registry index items instead (`name`, `title`, `type`, `category`, `description`, `files`). ## info ```bash npx @runek/cli info [options] ``` Shows one component without installing it: its description, the `add` command, the npm and registry dependencies it pulls, every prop with its type, default, and doc (code-only props such as callbacks are marked), the nested types it takes (like `Person`'s `PersonGarment`), and a minimal world node and JSX line to start from. `` is the registry name (`bookshelf`) or the world-data type (`Bookshelf`), in any case; a near miss suggests the closest name. It reads only the registry, so it works before `init`. ```bash npx @runek/cli info bench # Bench (bench, interiors) # ... # Props # length number = 1.6 # Length, in units. # back boolean = true # Include a backrest. # color color = palette.wood # World node # {"type":"Bench","props":{"position":[0,0,0]}} ``` `--json` prints the same card as one object: `name`, `title`, `kind`, `category`, `description`, `doc`, `add`, `dependencies`, `registryDependencies`, `props` (`name`, `type`, `optional`, `default`, `palette`, `computed`, `doc`, `codeOnly`), `types`, and `example` (`node`, `jsx`). ## validate ```bash npx @runek/cli validate [options] ``` Checks a world file against the components' prop schema (served at `/r/props.json`): unknown world fields, node fields and component types, props a component doesn't have, and values of the wrong type or outside an enum, each with a "did you mean" when a name is close. Invalid JSON is reported with its line and column. Components that are not in the registry but have a source file in your install directory (your own `MyThing.tsx`) are accepted as is. ```bash npx @runek/cli validate public/world.json # error avatar Avatar is "first", "third" or "overhead". # warning nodes[1].props.colr Bookshelf has no prop "colr". Did you mean "color"? # warning nodes[2].type Unknown component "Benchh". Did you mean "Bench"? ``` Errors (the world can't load) exit non-zero; warnings (it renders, maybe not as meant) only with `--strict`. `--json` prints `{ valid, errors, warnings, issues: [{ path, pathString, severity, message }] }`, where `path` is the JSON path as an array (`["nodes", 1, "props", "colr"]`). It uses `validateWorld` from your installed `@runek/core` (0.14 or newer). Editors can check the same rules as you type through the world JSON Schema: see [Worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md). ## check-world ```bash npx @runek/cli check-world [options] ``` Finds nodes that are **buried** (below the terrain) or **floating** (above the highest surface), and open water that stands above its shore. It reads the ground from your own component source (the `surfaces/*.ts` modules that ship with `terrain`, `dock`, `floor`, `slab`, `shore`, and `lake`) and the ground math from your installed `@runek/core`, so it checks exactly what your app renders. Only components marked `groundSitting` are checked, and anchored nodes pass by construction. ```bash npx @runek/cli check-world public/world.json # buried Tent [70] at (-28, 0.42, 110) is 1.8 below the ground (2.22); set position[1] to 2.216, or anchor it ``` `--fix` writes each suggested Y into the file; `--strict` exits non-zero when anything is found, so an app can run it in CI. `--json` prints `{ surfaces, issues }` (plus `fixed` with `--fix`), each issue with its `kind`, ground-index `path`, `type`, `id`, `at`, `ground`, and suggested `fix`. Loading `.ts` surfaces needs Node 22.18 or newer. See [Placing things on uneven ground](https://runek.nullorder.org/docs/placing-things-on-uneven-ground.md). ## preview ```bash npx @runek/cli preview [--open] ``` Prints a [workshop](https://runek.nullorder.org/docs/the-workshop.md) link that carries the whole world in its URL, the same as the workshop's own share links, so you (or whoever you send it to) can see a world file rendered without running an app. `--open` opens it in your browser. Very large worlds make long links; past 8,000 characters it warns. With a registry served by a Runek docs site (a URL ending in `/r`), the link points at that site's workshop. ## skill ```bash npx @runek/cli skill [--out ] [--overwrite] ``` Installs the Runek agent skill (served at `/r/agents/SKILL.md`): how to add components, write and check world files, and place things, for coding agents working in your project. It writes `.claude/skills/runek/SKILL.md` by default; `--out` puts it anywhere else, such as another agent's rules folder. An existing file is kept unless you pass `--overwrite`. See [Runek for agents](https://runek.nullorder.org/docs/for-agents.md). ## Options | Option | Commands | Description | |---|---|---| | `--registry ` | all | Registry base. Defaults to the configured one (`https://runek.nullorder.org/r`). A local path works for development. | | `--dir ` | init, add, validate, check-world | Install directory (default `src/runek`). | | `--overwrite` | add, skill | Replace files that already exist (otherwise they're skipped). | | `--no-install` | add | Print the dependency install command instead of running it. | | `--force` | init | Overwrite an existing `runek.config.json`. | | `--strict` | validate, check-world | Exit non-zero on any issue (validate: warnings too). | | `--fix` | check-world | Write the suggested Y values into the world file. | | `--tolerance ` | check-world | Allowed gap before a node is flagged (default `0.08`). | | `--json` | list, info, validate, check-world | Machine-readable JSON on stdout, for scripts and agents. | | `--out ` | skill | Where to write the skill (default `.claude/skills/runek/SKILL.md`). | | `--open` | preview | Open the link in your browser. | | `-v, --version` | — | Print the CLI version (also `runek version`): a pixel banner in a terminal, the bare version when piped. | | `-h, --help` | — | Show help. | ## runek.config.json ```json { "$schema": "https://runek.nullorder.org/registry/config-schema.json", "registry": "https://runek.nullorder.org/r", "dir": "src/runek" } ``` - **registry** — where components are fetched from. - **dir** — where component source is written. The runtime comes from the `@runek/core` npm package, not copied here. ## Registry index The default registry is served at [`https://runek.nullorder.org/r`](https://runek.nullorder.org/r). Its index lists every component (name, type, description); each entry resolves to a self-contained manifest under `components/.json`. Point `--registry` at it (the default) or at a local copy for development. [https://runek.nullorder.org/r/registry.json](https://runek.nullorder.org/r/registry.json): The catalog the CLI reads first: every component name, type, and description. --- # ArcadeCabinet Source: https://runek.nullorder.org/docs/components/arcadecabinet > An upright arcade cabinet cut to the classic profile, with a lit marquee, joystick and buttons, and a screen running a seeded pixel game. ## Add it ```bash npx @runek/cli add arcadecabinet ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { ArcadeCabinet } from './runek/ArcadeCabinet' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "ArcadeCabinet", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `width` | `number` | `0.66` | Cabinet width, in units. | | `color` | `color` | | Body color. Seeded when unset. | | `accent` | `color` | | Side-art stripe and marquee glow. Seeded when unset. | | `on` | `boolean` | `true` | Lit screen showing a seeded game; off, dark glass. | | `players` | `1 \| 2` | | Control sets on the panel (one or two players). Seeded when unset. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/arcadecabinet.json](https://runek.nullorder.org/r/components/arcadecabinet.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add arcadecabinet` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Arch Source: https://runek.nullorder.org/docs/components/arch > Freestanding gateway: two piers and a semicircular arch of voussoirs. Composes with Wall. ## Add it ```bash npx @runek/cli add arch ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Arch } from './runek/Arch' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Arch", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `2.4` | Clear opening width, in units. | | `height` | `number` | `2.6` | Height to the springline (top of the piers), in units. | | `depth` | `number` | `0.6` | Depth, in units. | | `thickness` | `number` | `0.4` | Pier thickness, in units. | | `blocks` | `number` | `9` | Voussoir blocks forming the semicircular arch. | | `color` | `color` | palette `stone` | Defaults to the world palette's `stone` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/arch.json](https://runek.nullorder.org/r/components/arch.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add arch` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Barrel Source: https://runek.nullorder.org/docs/components/barrel > Staved barrel bellied at the middle, banded by metal hoops; convex-hull collider. ## Add it ```bash npx @runek/cli add barrel ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Barrel } from './runek/Barrel' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Barrel", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `radius` | `number` | `0.35` | Radius, in units. | | `height` | `number` | `0.9` | Height, in units. | | `color` | `color` | palette `wood` | Stave color; defaults to the world palette's `wood` slot. | | `hoopColor` | `color` | palette `metal` | Hoop color; defaults to the world palette's `metal` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/barrel.json](https://runek.nullorder.org/r/components/barrel.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add barrel` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Bed Source: https://runek.nullorder.org/docs/components/bed > Bed with a frame, mattress, pillows, and a headboard. ## Add it ```bash npx @runek/cli add bed ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Bed } from './runek/Bed' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Bed", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `1.4` | Width, in units. | | `length` | `number` | `2` | Length, in units. | | `color` | `color` | palette `wood` | Frame color; defaults to the world palette's `wood` slot. | | `beddingColor` | `color` | palette `fabric` | Bedding color; defaults to the world palette's `fabric` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/bed.json](https://runek.nullorder.org/r/components/bed.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add bed` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Bench Source: https://runek.nullorder.org/docs/components/bench > Slatted bench with an optional backrest; indoor or outdoor seating. ## Add it ```bash npx @runek/cli add bench ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Bench } from './runek/Bench' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Bench", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `length` | `number` | `1.6` | Length, in units. | | `depth` | `number` | `0.5` | Depth, in units. | | `seatHeight` | `number` | `0.45` | | | `back` | `boolean` | `true` | Include a backrest. | | `color` | `color` | palette `wood` | Defaults to the world palette's `wood` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/bench.json](https://runek.nullorder.org/r/components/bench.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add bench` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Birds Source: https://runek.nullorder.org/docs/components/birds > A loose flock circling overhead, each bird flapping on its own seeded orbit. ## Add it ```bash npx @runek/cli add birds ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Birds } from './runek/Birds' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Birds", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `count` | `number` | `10` | | | `area` | `number` | `20` | Orbit spread, in units. | | `height` | `number` | `12` | Height, in units. | | `speed` | `number` | `1` | | | `color` | `color` | `"#2a2a30"` | | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/birds.json](https://runek.nullorder.org/r/components/birds.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add birds` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Book Source: https://runek.nullorder.org/docs/components/book > A single procedural book — standing, lying, or open — with a seeded cover and optional click interaction; the standalone sibling of Bookshelf's instanced spines. ## Add it ```bash npx @runek/cli add book ``` Pulls `@react-three/drei@^10.7.7`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Book } from './runek/Book' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Book", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `width` | `number` | `0.22` | Cover width (spine to fore-edge), in units. | | `height` | `number` | `0.3` | Spine length, in units. | | `thickness` | `number` | `0.05` | Closed page-block thickness, in units. | | `pose` | `"standing" \| "lying" \| "open"` | `"lying"` | `lying` rests on its back cover (spine on the -x edge); `standing` stands upright on its bottom edge; `open` lies opened flat at the spine. | | `color` | `color` | | Cover color. Defaults to a seeded cloth-bound color. | | `pageColor` | `color` | `"#efe7d2"` | | | `title` | `string` | | Shown as a hover label when the book is interactive. | | `href` | `string` | | Navigated to on click when no `onSelect` is given. | | `onSelect` | `function` (code only) | | Called on click. Optional, so the book still renders from data. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/book.json](https://runek.nullorder.org/r/components/book.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add book` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Bookshelf Source: https://runek.nullorder.org/docs/components/bookshelf > Procedurally generated bookshelf with seeded books and one cuboid collider. ## Add it ```bash npx @runek/cli add bookshelf ``` Pulls `sign`, `@react-three/drei@^10.7.7`, `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Bookshelf } from './runek/Bookshelf' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Bookshelf", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `width` | `number` | `1.2` | Outer dimensions in units. | | `height` | `number` | `2` | Height, in units. | | `depth` | `number` | `0.3` | Depth, in units. | | `shelves` | `number` | `3` | Number of rows. Defaults to 3; set 1 or 2 for a shorter case. | | `fill` | `number` | `0` | Fraction of shelf space filled with procedural decoration, 0–1. Defaults to `0` (an empty shelf); set it for a decorative, non-interactive shelf. Ignored when `books` is set. | | `color` | `color` | palette `wood` | Frame color. Defaults to the world palette's `wood` slot. | | `backColor` | `color` | palette `woodDark` | Back-panel color. Defaults to the world palette's `woodDark` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | | `books` | `BookSpec[]` | | Explicit, addressable books. When provided, these replace the procedural `fill` and are the only books rendered. Pass `[]` for an empty shelf and append to make books appear. A book is clickable when `onBookSelect` is set or it carries an `href`. | | `onBookSelect` | `function` (code only) | | Called with the clicked book. When omitted, a book's `href` is navigated to. | | `label` | `string` | | Section label rendered above the shelf, e.g. `"Guides"`. Drawn as a `Sign` in the world's `display` face. | | `labelColor` | `color` | palette `accent` | Label color. Defaults to the world palette's `accent` slot. | | `labelSize` | `number` | `0.08` | Label cap height in units. | ### `BookSpec` | Prop | Type | Default | Description | |---|---|---|---| | `id` | `string` | **required** | Stable identity, passed back to `onBookSelect`. | | `title` | `string` | | Shown as a hover label. | | `color` | `color` | | Spine color. Defaults to a seeded color. | | `href` | `string` | | Navigated to on click when no `onBookSelect` is given. | | `shelf` | `number` | | Row to place the book on, counted from the top (0 = top row). Clamped to the shelf's row count. Books without a row auto-pack bottom-up around the placed ones. | ## Registry manifest [https://runek.nullorder.org/r/components/bookshelf.json](https://runek.nullorder.org/r/components/bookshelf.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add bookshelf` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Bridge Source: https://runek.nullorder.org/docs/components/bridge > Plank deck spanning a gap, optionally arched, with railings and slab colliders that follow the deck. ## Add it ```bash npx @runek/cli add bridge ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Bridge } from './runek/Bridge' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Bridge", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `length` | `number` | `6` | Span along local X, in units. | | `width` | `number` | `2` | Walkway width along local Z, in units. | | `arch` | `number` | `0` | Arch rise at the center, in units (0 = flat). | | `rails` | `boolean` | `true` | Side railings. | | `color` | `color` | palette `wood` | Deck color; defaults to the world palette's `wood` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/bridge.json](https://runek.nullorder.org/r/components/bridge.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add bridge` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Bush Source: https://runek.nullorder.org/docs/components/bush > Clustered foliage blobs, seeded; the mid-height density between Grass and Trees. ## Add it ```bash npx @runek/cli add bush ``` Pulls `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Bush } from './runek/Bush' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Bush", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `radius` | `number` | `0.6` | Overall radius, in units. | | `blobs` | `number` | `7` | Number of foliage blobs. | | `color` | `color` | palette `foliage` | Defaults to the world palette's `foliage` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/bush.json](https://runek.nullorder.org/r/components/bush.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add bush` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Campfire Source: https://runek.nullorder.org/docs/components/campfire > Stone ring, a tepee of logs, and an animated flame with a warm flickering light. ## Add it ```bash npx @runek/cli add campfire ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Campfire } from './runek/Campfire' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Campfire", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `radius` | `number` | `0.5` | Radius, in units. | | `logColor` | `color` | palette `wood` | Log color; defaults to the world palette's `wood` slot. | | `flameColor` | `color` | `"#ff7a1a"` | | | `intensity` | `number` | `14` | | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/campfire.json](https://runek.nullorder.org/r/components/campfire.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add campfire` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Chair Source: https://runek.nullorder.org/docs/components/chair > Chair with seat, back, and legs. ## Add it ```bash npx @runek/cli add chair ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Chair } from './runek/Chair' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Chair", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `0.45` | Width, in units. | | `depth` | `number` | `0.45` | Depth, in units. | | `seatHeight` | `number` | `0.45` | | | `backHeight` | `number` | `0.5` | | | `color` | `color` | palette `wood` | Defaults to the world palette's `wood` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/chair.json](https://runek.nullorder.org/r/components/chair.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add chair` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Cliff Source: https://runek.nullorder.org/docs/components/cliff > A rocky promontory: a low-poly, seed-jittered truncated cone with a flattish plateau and a convex-hull collider. ## Add it ```bash npx @runek/cli add cliff ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Cliff } from './runek/Cliff' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Cliff", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `radius` | `number` | `10` | Base radius at the waterline, in units. | | `topRadius` | `number` | `6` | Plateau (top) radius, in units. | | `height` | `number` | `12` | Height from the base to the plateau, in units. | | `segments` | `number` | `8` | Radial facets — fewer reads as blockier rock. | | `rough` | `number` | `0.35` | Surface jitter, as a fraction of a facet. | | `color` | `color` | | Rock color; defaults to the palette's `stone`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/cliff.json](https://runek.nullorder.org/r/components/cliff.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add cliff` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Clock Source: https://runek.nullorder.org/docs/components/clock > Procedural analog wall clock; hands track the system clock or a given IANA timezone. ## Add it ```bash npx @runek/cli add clock ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Clock } from './runek/Clock' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Clock", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `radius` | `number` | `0.55` | Face radius in units. | | `timezone` | `string` | | IANA timezone, e.g. "Asia/Kolkata". Omit to inherit the world's `timezone` (``), then the local system time; if neither resolves, the clock falls back to UTC. | | `frameColor` | `color` | palette `metal` | Rim/frame color. Defaults to the world palette's `metal` slot. | | `faceColor` | `color` | `"#0d1117"` | Dial color. | | `handColor` | `color` | `"#e8eef5"` | Hour/minute hand and tick color. | | `accentColor` | `color` | palette `accent` | Second hand + hub accent. Defaults to the world palette's `accent` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/clock.json](https://runek.nullorder.org/r/components/clock.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add clock` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Clouds Source: https://runek.nullorder.org/docs/components/clouds > Drifting clouds built from clustered soft blobs (no textures); layer above a Sky. ## Add it ```bash npx @runek/cli add clouds ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Clouds } from './runek/Clouds' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Clouds", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `count` | `number` | `8` | Number of clouds. | | `area` | `[number, number]` | `[80, 80]` | Spread `[width, depth]`, in units. | | `height` | `number` | `24` | Height above the origin, in units. | | `drift` | `number` | `0.6` | Drift speed along +X, in units/sec (0 holds still). | | `color` | `color` | `"#eef2f7"` | | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/clouds.json](https://runek.nullorder.org/r/components/clouds.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add clouds` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # CoffeeMachine Source: https://runek.nullorder.org/docs/components/coffeemachine > A countertop coffee machine, espresso or drip, with a cup or carafe and a few puffs of steam. Sits on a Counter. ## Add it ```bash npx @runek/cli add coffeemachine ``` Pulls `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { CoffeeMachine } from './runek/CoffeeMachine' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "CoffeeMachine", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `kind` | `"espresso" \| "drip"` | | Seeded when unset. | | `color` | `color` | | Body color. Seeded from a few appliance finishes when unset. | | `steam` | `boolean` | `true` | A few puffs of steam rising from the cup or carafe. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/coffeemachine.json](https://runek.nullorder.org/r/components/coffeemachine.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add coffeemachine` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Compass Source: https://runek.nullorder.org/docs/components/compass > A screen-fixed HUD compass: a glassy dial in a corner of the canvas whose rose card swings with the camera heading, with a fixed lubber line and an optional wind + bearing readout — works with any traversal that drives the camera (on foot or at a vehicle's chase-cam). ## Add it ```bash npx @runek/cli add compass ``` Pulls `@react-three/drei@^10.7.7`, `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Compass } from './runek/Compass' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Compass", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `corner` | `"top-left" \| "top-right" \| "bottom-left" \| "bottom-right"` | `"bottom-left"` | Screen corner the dial sits in. | | `size` | `number` | `108` | Dial diameter, in CSS px (shrinks ~22% on narrow viewports). | | `inset` | `[number, number]` | `[16, 16]` | Inset from the corner edges, in CSS px `[x, y]`. | | `readout` | `boolean` | `true` | Show the wind + bearing readout pill under the dial. | | `north` | `number` | `0` | World yaw (radians) the dial reads as north, using the same convention as `Player`/`Helm` yaw: 0 faces +Z, so the default makes +Z north (and -X east). | | `accentColor` | `color` | `"#c0392b"` | North needle + north letter color. Compass-north red; no palette slot fits. | | `position` | `[x, y, z]` | | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/compass.json](https://runek.nullorder.org/r/components/compass.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add compass` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Counter Source: https://runek.nullorder.org/docs/components/counter > A service / bar counter: a solid body under a worktop that overhangs the front; one cuboid collider. With hob, a kitchen stove: a glass cooktop and an oven door. ## Add it ```bash npx @runek/cli add counter ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Counter } from './runek/Counter' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Counter", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `length` | `number` | `3` | Length along local X, in units. | | `height` | `number` | `1.1` | Counter height, in units. | | `depth` | `number` | `0.6` | Depth along local Z, in units. | | `color` | `color` | | Body color; defaults to the palette's `wood`. | | `topColor` | `color` | | Worktop color; defaults to the palette's `woodDark`. | | `hob` | `boolean` | `false` | A kitchen stove at the +X end: a glass hob with four burners on the worktop and an oven door in the front below it. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/counter.json](https://runek.nullorder.org/r/components/counter.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add counter` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Crate Source: https://runek.nullorder.org/docs/components/crate > Slatted wooden crate with seeded plank shades; stacks via a cuboid collider. ## Add it ```bash npx @runek/cli add crate ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Crate } from './runek/Crate' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Crate", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `size` | `number` | `0.8` | Edge length, in units. | | `color` | `color` | palette `wood` | Plank color; defaults to the world palette's `wood` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/crate.json](https://runek.nullorder.org/r/components/crate.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add crate` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # CurvedWall Source: https://runek.nullorder.org/docs/components/curvedwall > An arc of wall around the origin — solid concrete or a floor-to-ceiling glass curtain wall with mullions. Compose several arcs to leave door gaps; chords carry cuboid colliders. ## Add it ```bash npx @runek/cli add curvedwall ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { CurvedWall } from './runek/CurvedWall' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "CurvedWall", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `radius` | `number` | `6` | Arc centerline radius, in units. The component origin is the circle's center. | | `arc` | `number` | `1.5707963267948966` | Sweep angle in radians, centered on the local +Z axis. | | `height` | `number` | `3` | Height, in units. | | `thickness` | `number` | `0.15` | Thickness, in units. | | `style` | `"solid" \| "glass"` | `"solid"` | `glass` renders a transparent curtain wall with mullions; `solid` a plain wall. | | `segments` | `number` | | Chord segments; defaults from `radius × arc`, more segments reading as smoother. | | `color` | `color` | palette `wall` | Solid wall color; defaults to the world palette's `wall` slot. | | `glassColor` | `color` | `"#b7d8e8"` | Glass tint (glass style). | | `frameColor` | `color` | palette `metal` | Mullion and rail color (glass style); defaults to the palette's `metal` slot. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/curvedwall.json](https://runek.nullorder.org/r/components/curvedwall.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add curvedwall` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Desk Source: https://runek.nullorder.org/docs/components/desk > A work desk: a top over slim legs, a drawer pedestal, or side panels, with a modesty panel at the back. Base and drawers seeded, each pinnable. One cuboid collider. ## Add it ```bash npx @runek/cli add desk ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Desk } from './runek/Desk' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Desk", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `width` | `number` | `1.4` | Width along local X, in units. | | `depth` | `number` | `0.7` | Depth along local Z, in units. The front, where you sit, faces +Z. | | `height` | `number` | `0.75` | Height of the work surface, in units. | | `base` | `"legs" \| "pedestal" \| "panels"` | | Seeded when unset. | | `drawers` | `number` | | Drawers in the pedestal, 0 to 4. Seeded when unset; only a `pedestal` desk has them. | | `color` | `color` | | Top color; defaults to the palette's `wood`. | | `baseColor` | `color` | | Legs, pedestal, and panels; defaults to the palette's `metal` for legs, else `woodDark`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/desk.json](https://runek.nullorder.org/r/components/desk.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add desk` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Dock Source: https://runek.nullorder.org/docs/components/dock > A plank jetty on pilings reaching out over water; walkable deck colliders, side stringers, and seaward mooring posts. ## Add it ```bash npx @runek/cli add dock ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Dock } from './runek/Dock' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Dock", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `length` | `number` | `10` | How far the jetty reaches out from the shore (local +Z), in units. | | `width` | `number` | `3` | Walkway width (local X), in units. | | `depth` | `number` | `4` | How deep the pilings sink below the deck surface, in units. | | `color` | `color` | | Deck color; defaults to the world palette's `wood`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/dock.json](https://runek.nullorder.org/r/components/dock.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add dock` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Door Source: https://runek.nullorder.org/docs/components/door > Door panel within a frame. ## Add it ```bash npx @runek/cli add door ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Door } from './runek/Door' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Door", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `0.9` | Width, in units. | | `height` | `number` | `2` | Height, in units. | | `thickness` | `number` | `0.05` | Thickness, in units. | | `openAngle` | `number` | `0` | Hinge angle in radians; 0 is closed. | | `color` | `color` | palette `wood` | Defaults to the world palette's `wood` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/door.json](https://runek.nullorder.org/r/components/door.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add door` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Fence Source: https://runek.nullorder.org/docs/components/fence > Posts and rails with seeded weathering; encloses yards and paths. One footprint collider. ## Add it ```bash npx @runek/cli add fence ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Fence } from './runek/Fence' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Fence", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `length` | `number` | `6` | Total length along local X, in units. | | `height` | `number` | `1.1` | Height, in units. | | `postSpacing` | `number` | `1.5` | Spacing between posts, in units. | | `rails` | `number` | `2` | Number of horizontal rails. | | `color` | `color` | palette `wood` | Defaults to the world palette's `wood` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/fence.json](https://runek.nullorder.org/r/components/fence.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add fence` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Flag Source: https://runek.nullorder.org/docs/components/flag > A cloth flag on a pole; the cloth ripples per-frame, pinned at the luff. Thin pole collider. ## Add it ```bash npx @runek/cli add flag ``` Pulls `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Flag } from './runek/Flag' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Flag", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `poleHeight` | `number` | `6` | Pole height, in units. | | `fly` | `number` | `2.4` | Flag width away from the pole (the fly), in units. | | `drop` | `number` | `1.5` | Flag height (the drop), in units. | | `waveSpeed` | `number` | `3` | Ripple speed. | | `waveAmplitude` | `number` | `0.22` | Ripple depth as a fraction of the fly. | | `color` | `color` | | Cloth color; defaults to the world palette's `fabric`. | | `poleColor` | `color` | | Pole color; defaults to the palette's `wood`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/flag.json](https://runek.nullorder.org/r/components/flag.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add flag` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Floor Source: https://runek.nullorder.org/docs/components/floor > Flat floor slab with a fixed collider; optional stairwell opening. ## Add it ```bash npx @runek/cli add floor ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Floor } from './runek/Floor' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Floor", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `size` | `[number, number]` | `[8, 8]` | `[width, depth]` in units. The top surface sits at the component origin. | | `thickness` | `number` | `0.2` | Thickness, in units. | | `opening` | `FloorOpening` | | A hole in the slab (stairwell); the slab splits into strips around it. | | `color` | `color` | palette `floor` | Defaults to the world palette's `floor` slot. | ### `FloorOpening` | Prop | Type | Default | Description | |---|---|---|---| | `x` | `number` | | Hole center offset from the slab center along X, in units. | | `z` | `number` | | Hole center offset from the slab center along Z, in units. | | `width` | `number` | **required** | | | `depth` | `number` | **required** | | ## Registry manifest [https://runek.nullorder.org/r/components/floor.json](https://runek.nullorder.org/r/components/floor.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add floor` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Flowers Source: https://runek.nullorder.org/docs/components/flowers > Instanced seeded wildflowers (stem + colored head) scattered over a patch. ## Add it ```bash npx @runek/cli add flowers ``` Pulls `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Flowers } from './runek/Flowers' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Flowers", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `area` | `[number, number]` | `[6, 6]` | Patch extent `[width, depth]`, in units. | | `count` | `number` | `60` | | | `height` | `number` | `0.4` | Height, in units. | | `stemColor` | `color` | palette `foliage` | Stem color; defaults to the world palette's `foliage` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/flowers.json](https://runek.nullorder.org/r/components/flowers.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add flowers` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Fountain Source: https://runek.nullorder.org/docs/components/fountain > Two-tier stone fountain with gently rippling water (a per-frame bob, no textures). ## Add it ```bash npx @runek/cli add fountain ``` Pulls `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Fountain } from './runek/Fountain' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Fountain", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `radius` | `number` | `1.6` | Radius, in units. | | `color` | `color` | palette `stone` | Stone color; defaults to the world palette's `stone` slot. | | `waterColor` | `color` | palette `waterShallow` | Water color; defaults to the world palette's `waterShallow` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/fountain.json](https://runek.nullorder.org/r/components/fountain.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add fountain` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Fridge Source: https://runek.nullorder.org/docs/components/fridge > A kitchen fridge with a top or bottom freezer or one tall door, bar handles, and a seeded enamel or steel finish. ## Add it ```bash npx @runek/cli add fridge ``` Pulls `@react-three/drei@^10.7.7`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Fridge } from './runek/Fridge' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Fridge", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `width` | `number` | `0.7` | Width, in units. | | `height` | `number` | `1.8` | Height, in units. | | `depth` | `number` | `0.68` | Depth, in units. | | `layout` | `"top" \| "bottom" \| "single"` | | Seeded when unset. | | `color` | `color` | | Body color. Seeded between an enamel white and brushed steel when unset. | | `handleColor` | `color` | | Handles; defaults to the palette's `metal`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/fridge.json](https://runek.nullorder.org/r/components/fridge.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add fridge` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Grass Source: https://runek.nullorder.org/docs/components/grass > Instanced grass blades (seeded scatter). ## Add it ```bash npx @runek/cli add grass ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Grass } from './runek/Grass' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Grass", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `area` | `[number, number]` | `[10, 10]` | Patch extent `[width, depth]`, in units. | | `count` | `number` | `600` | | | `height` | `number` | `0.35` | Height, in units. | | `color` | `color` | palette `foliage` | Defaults to the world palette's `foliage` slot. | | `sway` | `number` | `1` | Wind sway strength; 0 disables the animation. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/grass.json](https://runek.nullorder.org/r/components/grass.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add grass` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Hedge Source: https://runek.nullorder.org/docs/components/hedge > A trimmed green wall with seeded surface tufts; blocks like a Wall. ## Add it ```bash npx @runek/cli add hedge ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Hedge } from './runek/Hedge' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Hedge", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `length` | `number` | `4` | Length along local X, in units. | | `height` | `number` | `1.2` | Height, in units. | | `depth` | `number` | `0.6` | Depth, in units. | | `color` | `color` | palette `foliage` | Defaults to the world palette's `foliage` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/hedge.json](https://runek.nullorder.org/r/components/hedge.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add hedge` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # House Source: https://runek.nullorder.org/docs/components/house > Composite: a two-level dwelling arranged from parts (Levels, Door, Windows, Staircase, gable Roof, Plant). Unpack to customize. **House is a composite**: a data arrangement of parts, not code. `add` copies the arrangement JSON plus the source of every part it references. ## Add it ```bash npx @runek/cli add house ``` Pulls `door`, `level`, `plant`, `roof`, `staircase`, `window`. ## Use it Place it in a world by type — the renderer expands the arrangement in place: ```json { "type": "House", "props": { "position": [0, 0, 0], "seed": 1 } } ``` Register the arrangement in your registry map next to the parts it uses: ```ts import house from './runek/composites/house.json' const registry = { /* …parts… */, House: house as unknown as CompositeDef } ``` An instance `seed` deterministically re-rolls every child that doesn't pin its own. In the editor, **Unpack** replaces an instance with its editable arrangement (a `Group` of ordinary nodes) for per-instance customization. ## Arrangement ```json { "kind": "composite", "name": "House", "description": "A two-level dwelling arranged from parts: wall rings with door and window openings, a staircase through the upper floor, a gable roof, and a plant by the door.", "bounds": [9.6, 7.2, 7.6], "groundSitting": true, "nodes": [ { "type": "Level", "props": { "size": [9, 7], "height": 3, "walls": { "front": { "openings": [ { "width": 1, "height": 2.1 }, { "offset": -3, "width": 1.3, "height": 1.2, "sill": 1 }, { "offset": 3, "width": 1.3, "height": 1.2, "sill": 1 } ] }, "left": { "openings": [{ "width": 1.3, "height": 1.2, "sill": 1 }] }, "right": { "openings": [{ "width": 1.3, "height": 1.2, "sill": 1 }] } } } }, { "type": "Level", "props": { "position": [0, 3, 0], "size": [9, 7], "height": 2.6, "walls": { "front": { "openings": [ { "offset": -3, "width": 1.3, "height": 1.1, "sill": 0.9 }, { "offset": 3, "width": 1.3, "height": 1.1, "sill": 0.9 } ] }, "left": { "openings": [{ "width": 1.3, "height": 1.1, "sill": 0.9 }] }, "right": { "openings": [{ "width": 1.3, "height": 1.1, "sill": 0.9 }] } }, "floor": { "opening": { "x": 3.7, "z": -1.95, "width": 1.4, "depth": 3.1 } } } }, { "type": "Staircase", "props": { "position": [3.7, 0, -3.4], "steps": 10, "totalHeight": 3, "width": 1.3, "depth": 3 } }, { "type": "Door", "props": { "position": [0, 0, 3.5], "width": 1, "height": 2.1, "openAngle": -1.1 } }, { "type": "Window", "props": { "position": [-3, 1, 3.5], "width": 1.3, "height": 1.2 } }, { "type": "Window", "props": { "position": [3, 1, 3.5], "width": 1.3, "height": 1.2 } }, { "type": "Window", "props": { "position": [-4.5, 1, 0], "rotation": [0, 1.5708, 0], "width": 1.3, "height": 1.2 } }, { "type": "Window", "props": { "position": [4.5, 1, 0], "rotation": [0, 1.5708, 0], "width": 1.3, "height": 1.2 } }, { "type": "Window", "props": { "position": [-3, 3.9, 3.5], "width": 1.3, "height": 1.1 } }, { "type": "Window", "props": { "position": [3, 3.9, 3.5], "width": 1.3, "height": 1.1 } }, { "type": "Window", "props": { "position": [-4.5, 3.9, 0], "rotation": [0, 1.5708, 0], "width": 1.3, "height": 1.1 } }, { "type": "Window", "props": { "position": [4.5, 3.9, 0], "rotation": [0, 1.5708, 0], "width": 1.3, "height": 1.1 } }, { "type": "Roof", "props": { "position": [0, 5.6, 0], "size": [9, 7], "style": "gable" } }, { "type": "Plant", "props": { "position": [-3.5, 0, 2.3] } } ] } ``` ## Migrate **v0.11.0 → v0.12.0.** `House` is no longer a coded component with parametric props — the registry name now resolves to a **composite**: a data arrangement of `Level`s, a `Door`, `Window`s, a `Staircase`, a gable `Roof`, and a `Plant`. The default house is a two-level dwelling with a working staircase. **v0.11.0** ```tsx import { House } from './runek/House' ``` **v0.12.0** ```tsx import { WorldNodes } from '@runek/core' import { registry } from './runek/registry' ``` - World files with default props need nothing: `{ "type": "House" }` keeps rendering, now by expanding the composite. - Nodes that relied on the old parametric props (`size`, `height`, `roofStyle`, `wallColor`, `roofColor`) can bake the exact old single-storey look with [`scripts/migrate-buildings.mjs`](https://github.com/nullorder/runek/blob/main/scripts/migrate-buildings.mjs), which replaces each node with the equivalent arrangement of parts. - To customize a house, **Unpack** the instance in the editor, or edit `composites/house.json` after `runek add house`. An instance `seed` re-rolls the arrangement. - The `HouseProps` type no longer exists. ## Registry manifest [https://runek.nullorder.org/r/components/house.json](https://runek.nullorder.org/r/components/house.json): Self-contained JSON: the inlined arrangement plus resolved dependencies, exactly what `runek add house` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Hut Source: https://runek.nullorder.org/docs/components/hut > A round hut: a post-ribbed wall on a stone base, a framed doorway and window, under a shaggy thatch cone; one cylinder collider. ## Add it ```bash npx @runek/cli add hut ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Hut } from './runek/Hut' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Hut", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `radius` | `number` | `3` | Wall radius, in units. | | `wallHeight` | `number` | `2.9` | Wall height at the eaves, in units. | | `roofHeight` | `number` | `1.5` | Conical roof height above the eaves, in units. | | `doorWidth` | `number` | `1.3` | Doorway width at the front (local +Z), in units. | | `wallColor` | `color` | | Wall color; defaults to the palette's `wall`. | | `roofColor` | `color` | | Roof color; defaults to the palette's `roof`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/hut.json](https://runek.nullorder.org/r/components/hut.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add hut` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Interactable Source: https://runek.nullorder.org/docs/components/interactable > Wrap anything to make it something the player can act on: walk within range and a prompt shows its actions with the keys the world binds to them; press one and onAction gets its id. Only the nearest in range takes the key, and keys yield to text fields. ## Add it ```bash npx @runek/cli add interactable ``` Pulls `sign`, `@react-three/drei@^10.7.7`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Interactable } from './runek/Interactable' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Interactable", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `actions` | `InteractionAction[]` | `[]` | What the player can do here. Each names a world `controls` action for its key, so declare it in the world (`controls: { use: ['KeyE'] }`). | | `radius` | `number` | `2` | How close the avatar must come for the prompt to show, in units. | | `promptHeight` | `number` | `1.4` | Height of the prompt above the origin, in units. | | `onAction` | `function` (code only) | | | | `children` | `node` (code only) | | | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ### `InteractionAction` | Prop | Type | Default | Description | |---|---|---|---| | `id` | `string` | **required** | Passed back to `onAction`. | | `label` | `string` | **required** | Shown in the prompt, e.g. `Talk`. | | `control` | `string` | **required** | The world `controls` action that triggers it (declare `talk: ['KeyT']` in the world's `controls`). The prompt shows the key bound to it, so a remap relabels it. | ## Registry manifest [https://runek.nullorder.org/r/components/interactable.json](https://runek.nullorder.org/r/components/interactable.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add interactable` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Lake Source: https://runek.nullorder.org/docs/components/lake > Procedural animated-shader water surface (no textures); place its surface at or below ground level. ## Add it ```bash npx @runek/cli add lake ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Lake } from './runek/Lake' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Lake", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `size` | `[number, number]` | `[20, 20]` | Water surface `[width, depth]`, in units. | | `colorDeep` | `color` | palette `waterDeep` | Defaults to the world palette's `waterDeep` slot. | | `colorShallow` | `color` | palette `waterShallow` | Defaults to the world palette's `waterShallow` slot. | | `sunPosition` | `[x, y, z]` | `[80, 30, 40]` | Direction the sun glint comes from; pair with your Sky's `sunPosition`. | | `waveHeight` | `number` | `0.12` | | | `waveSpeed` | `number` | `1` | | | `segments` | `number` | `64` | | ## Registry manifest [https://runek.nullorder.org/r/components/lake.json](https://runek.nullorder.org/r/components/lake.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add lake` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Lamp Source: https://runek.nullorder.org/docs/components/lamp > Lamp that emits a point light. ## Add it ```bash npx @runek/cli add lamp ``` Pulls `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Lamp } from './runek/Lamp' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Lamp", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `height` | `number` | `1.6` | Height, in units. | | `color` | `color` | palette `metal` | Base + pole color. Defaults to the world palette's `metal` slot. | | `shadeColor` | `color` | `"#e9d8a6"` | | | `lightColor` | `color` | `"#ffe8c2"` | | | `intensity` | `number` | `18` | | | `flicker` | `number` | `0.08` | Candle-like intensity flicker, 0–1; 0 holds the light steady. | ## Registry manifest [https://runek.nullorder.org/r/components/lamp.json](https://runek.nullorder.org/r/components/lamp.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add lamp` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Level Source: https://runek.nullorder.org/docs/components/level > A stackable wall ring plus optional slab: per-side openings, sides that can be omitted, and a stairwell hole — the unit a building is composed from. ## Add it ```bash npx @runek/cli add level ``` Pulls `floor`, `wall`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Level } from './runek/Level' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Level", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `size` | `[number, number]` | `[8, 8]` | Interior footprint `[width, depth]`, in units. | | `height` | `number` | `3` | Height, in units. | | `thickness` | `number` | `0.2` | Thickness, in units. | | `walls` | `LevelWalls` | | Per-side wall configs; an omitted side renders a solid wall. | | `floor` | `boolean \| LevelFloorConfig` | `true` | The slab underfoot; `false` for none, or a config with a stairwell `opening`. | | `color` | `color` | palette `wall` | Defaults to the world palette's `wall` slot. | | `seed` | `number` | | Reserved for procedural variation. | ### `LevelWalls` | Prop | Type | Default | Description | |---|---|---|---| | `front` | `LevelWallConfig` | | | | `back` | `LevelWallConfig` | | | | `left` | `LevelWallConfig` | | | | `right` | `LevelWallConfig` | | | ### `LevelWallConfig` | Prop | Type | Default | Description | |---|---|---|---| | `present` | `boolean` | | Set `false` to leave this side open (porch, lean-to). | | `openings` | `WallOpening[]` | | | | `color` | `color` | palette `wall` | Defaults to the level `color`, then the world palette's `wall` slot. | ### `WallOpening` | Prop | Type | Default | Description | |---|---|---|---| | `offset` | `number` | | Horizontal center offset from the wall center, in units. | | `width` | `number` | **required** | | | `height` | `number` | **required** | | | `sill` | `number` | | Height of the opening's base above the wall base, in units. | ### `LevelFloorConfig` | Prop | Type | Default | Description | |---|---|---|---| | `opening` | `FloorOpening` | | | | `thickness` | `number` | | | | `color` | `color` | palette `floor` | Defaults to the world palette's `floor` slot. | ### `FloorOpening` | Prop | Type | Default | Description | |---|---|---|---| | `x` | `number` | | Hole center offset from the slab center along X, in units. | | `z` | `number` | | Hole center offset from the slab center along Z, in units. | | `width` | `number` | **required** | | | `depth` | `number` | **required** | | ## Registry manifest [https://runek.nullorder.org/r/components/level.json](https://runek.nullorder.org/r/components/level.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add level` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # LightRig Source: https://runek.nullorder.org/docs/components/lightrig > Sun + hemisphere/ground fill with shadow configuration. ## Add it ```bash npx @runek/cli add lightrig ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { LightRig } from './runek/LightRig' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "LightRig", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `sunPosition` | `[x, y, z]` | | | | `sunColor` | `color` | | | | `sunIntensity` | `number` | | | | `ambient` | `number` | | | | `skyColor` | `color` | `"#bcd4ff"` | | | `groundColor` | `color` | `"#4a4030"` | | | `shadows` | `boolean` | `true` | | | `shadowRange` | `number` | `30` | Half-extent of the shadow camera frustum, in units. | ## Registry manifest [https://runek.nullorder.org/r/components/lightrig.json](https://runek.nullorder.org/r/components/lightrig.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add lightrig` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Monitor Source: https://runek.nullorder.org/docs/components/monitor > A desk monitor on a foot, plate, or legs. Switched on, the screen glows in its own color with a few seeded windows and lines of text on it, all geometry. ## Add it ```bash npx @runek/cli add monitor ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Monitor } from './runek/Monitor' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Monitor", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `width` | `number` | `0.6` | Screen width, in units. | | `aspect` | `number` | `1.7777777777777777` | Width over height. | | `stand` | `"legs" \| "foot" \| "plate"` | | Seeded when unset. | | `screen` | `color` | | The screen's glow when on. Seeded from a few calm tints when unset. | | `on` | `boolean` | `true` | Lit, showing a few seeded windows; off, a dark glass. | | `color` | `color` | | Bezel and stand; defaults to the palette's `metal`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/monitor.json](https://runek.nullorder.org/r/components/monitor.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add monitor` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Ocean Source: https://runek.nullorder.org/docs/components/ocean > Camera-following animated-shader ocean that reaches the world fog horizon (no textures); sits at the world ground. ## Add it ```bash npx @runek/cli add ocean ``` Pulls `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Ocean } from './runek/Ocean' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Ocean", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `size` | `[number, number]` | `[400, 400]` | Plane size `[width, depth]`, in units. With `follow` on (the default) this patch tracks the camera, so keep it large enough to reach past the world fog. | | `colorDeep` | `color` | palette `waterDeep` | Defaults to the world palette's `waterDeep` slot. | | `colorShallow` | `color` | palette `waterShallow` | Defaults to the world palette's `waterShallow` slot. | | `sunPosition` | `[x, y, z]` | `[80, 30, 40]` | Direction the sun glint comes from; pair with your Sky's `sunPosition`. | | `waveHeight` | `number` | `0.4` | | | `waveSpeed` | `number` | `0.6` | | | `segments` | `number` | `160` | | | `follow` | `boolean` | `true` | Track the camera horizontally for an endless sea (default true). Set false to pin it. | ## Registry manifest [https://runek.nullorder.org/r/components/ocean.json](https://runek.nullorder.org/r/components/ocean.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add ocean` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # OfficeChair Source: https://runek.nullorder.org/docs/components/officechair > A swivel office chair: five-star base on casters, gas column, padded seat, low or high back, optional arms. Seats a Person in sit or type pose. ## Add it ```bash npx @runek/cli add officechair ``` Pulls `@react-three/drei@^10.7.7`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { OfficeChair } from './runek/OfficeChair' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "OfficeChair", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `seatHeight` | `number` | `0.47` | Top of the seat cushion, in units. | | `back` | `"low" \| "high"` | | Seeded when unset. | | `arms` | `boolean` | | Armrests. Seeded when unset. | | `color` | `color` | | Seat and back; defaults to the palette's `fabric`. | | `frameColor` | `color` | | Base, column, and arms; defaults to the palette's `metal`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/officechair.json](https://runek.nullorder.org/r/components/officechair.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add officechair` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Palm Source: https://runek.nullorder.org/docs/components/palm > A coconut palm: leaning bowed trunk and a crown of drooping instanced fronds, deterministic from seed. ## Add it ```bash npx @runek/cli add palm ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Palm } from './runek/Palm' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Palm", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `height` | `number` | `6` | Trunk height, in units. | | `lean` | `number` | `0.18` | Sideways lean of the crown, as a fraction of height. | | `fronds` | `number` | `11` | Frond count. | | `frondLength` | `number` | `2.2` | Frond length, in units. | | `trunkColor` | `color` | palette `bark` | Defaults to the world palette's `bark` slot. | | `frondColor` | `color` | palette `foliage` | Defaults to the world palette's `foliage` slot. | | `coconuts` | `boolean` | `true` | Grow a coconut cluster under the crown. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/palm.json](https://runek.nullorder.org/r/components/palm.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add palm` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Path Source: https://runek.nullorder.org/docs/components/path > A meandering ribbon trail from seeded waypoints; decorative, laid just over the ground. ## Add it ```bash npx @runek/cli add path ``` Pulls `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Path } from './runek/Path' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Path", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `length` | `number` | `12` | Length along local Z, in units. | | `width` | `number` | `1.4` | Width, in units. | | `meander` | `number` | `1.2` | Lateral meander amplitude, in units. | | `rise` | `number` | `0` | Total height climbed from the near end (local −Z) to the far end (+Z), in units. For a trail that gains height as it winds; the ribbon rises linearly along its length. | | `heights` | `number[]` | | Explicit elevation profile, in units: evenly spaced samples from the near end (local −Z) to the far end (+Z), linearly interpolated along the ribbon. Overrides `rise`. Author it from the terrain the trail crosses so the ribbon hugs the ground it climbs. | | `color` | `color` | palette `ground` | Defaults to the world palette's `ground` slot. | | `segments` | `number` | `48` | | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/path.json](https://runek.nullorder.org/r/components/path.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add path` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Person Source: https://runek.nullorder.org/docs/components/person > A sculpted, clothed, skinned figure generated from a seed: one continuous body with a sculpted face, lidded eyes that follow you, and hands with fingers, in three style templates (stylized by default, realistic, anime). Every part is configurable as plain JSON: the body (build, tone, height), the face (eyes, brows, nose, lips, blush), hair, and clothing as ordered layers (shirts, sweaters, vests, coats, dresses, robes, trousers, jeans, skirts, shoes, boots, belts, scarves, capes) that fit any body. Role presets, swappable cosmetic skins, hats and accessories, poses, idle breathing and blinking. Give it a patrol or a wander radius and it walks: a route that is a pure function of the clock, following the ground, with a walk cycle and a step aside for the player and other walkers. A route with a departure time is a one-way trip (to a desk, to bed) that reports its arrival, and actions put a key prompt over it (Talk, Info) when the player comes near. Meshes build in the background, coarse first, with full detail near the camera. Place one at a spawn point, or use it as the Player's third-person body. ## Add it ```bash npx @runek/cli add person ``` Pulls `interactable`, `sign`, `@react-three/drei@^10.7.7`, `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Person } from './runek/Person' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Person", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `style` | `"stylized" \| "realistic" \| "anime"` | `"stylized"` | Proportion and face template: `stylized` (default) is an animated-film look with a big head and eyes, small features, and slender limbs; `realistic` keeps canonical human proportions; `anime` pushes the eyes and legs further. | | `kind` | `"villager" \| "merchant" \| "guard" \| "sailor" \| "farmer" \| "noble" \| "scholar" \| "traveler"` | `"villager"` | Role preset: fills `outfit`, `hat`, and `accessories` unless you set them. | | `gender` | `"feminine" \| "masculine" \| "neutral"` | | Silhouette cues only; every trait it influences stays overridable. Seeded when unset. | | `age` | `"child" \| "adult" \| "elder"` | `"adult"` | Head-to-body ratio, stature, and (for `elder`) a stooped spine. | | `body` | `PersonBodySpec` | | The body: `build`, skin `tone`, and `height`. The flat props below are shorthands for the same fields; when both are set, this object wins. | | `face` | `PersonFaceSpec` | | The face, part by part: eye color and size, brow color and weight, nose shape, lip color and fullness, blush. | | `clothes` | `PersonGarment[]` | | Clothing layers, inner to outer, each with its own type, color, and options. The order decides the fit: a shirt listed before the jeans is tucked in. Replaces `outfit`; `[]` is the bare base body (a smooth mannequin). | | `height` | `number` | | Standing height, in units. Every other measurement derives from it. | | `build` | `"slim" \| "average" \| "stocky"` | | Limb thickness and torso depth. Seeded when unset. | | `skinTone` | `color` | | Skin color; seeded from a curated set of tones when unset. | | `skin` | `"sailor" \| "hoodie" \| "festival" \| "chef" \| "harvest" \| "winter" \| "voyage" \| "ceremony" \| "atelier" \| "pirate" \| "bartender" \| "office" \| "casual" \| "preppy" \| PersonSkin` | | Cosmetic loadout: a preset name from `PERSON_SKINS` (e.g. `"winter"`) or a custom `PersonSkin` bundle. Individual props win over it. Plain JSON either way, so an editor or an in-world UI can swap a figure's whole look by writing this one prop. | | `hair` | `"short" \| "long" \| "swept" \| "bun" \| "ponytail" \| "braid" \| "cropped" \| "bald" \| PersonHairSpec` | | A style name, or `{ style, color }`. | | `hairColor` | `color` | | | | `facialHair` | `"none" \| "stubble" \| "moustache" \| "beard"` | | | | `outfit` | `"tshirt" \| "shirt" \| "sweater" \| "hoodie" \| "blazer" \| "vest" \| "tunic" \| "coat" \| "dress" \| "robe" \| "apron" \| "uniform"` | | One-word outfit; expands to a `clothes` stack (e.g. `tunic`: trousers, tunic, shoes). | | `topColor` | `color` | | Outermost top's color; defaults to a seeded shade of a palette slot. A garment's own `color` wins over it. | | `bottomColor` | `color` | | Bottoms' color; defaults to a seeded shade of the palette's `woodDark`, `stone`, or `metal`. | | `shoeColor` | `color` | | Footwear color; defaults to the palette's `metal`. | | `hat` | `"none" \| "cap" \| "straw" \| "brim" \| "bandana" \| "hood" \| "chef"` | | | | `accessories` | `"belt" \| "scarf" \| "cape" \| "bag" \| "glasses" \| "staff"[]` | | | | `pose` | `"stand" \| "sit" \| "lean" \| "work" \| "wave" \| "type" \| "play" \| "drink" \| "lie"` | `"stand"` | What the figure is doing when it isn't walking. `sit` and `type` need a seat under them (`type` is seated at a desk, fingers tapping); `work` leans over a counter; `play` works controls at waist height; `drink` holds a mug and sips from it now and then; `lie` lies on its back centered on `position`, head toward local -Z, so it takes a `Bed`'s position and rotation at mattress height. | | `patrol` | `[x, y, z][]` | | Walk these waypoints, relative to `position` and turning with `rotation`, in units. Height follows the ground; a waypoint's y lifts the figure above it. | | `wander` | `number` | | Wander a seeded loop of five points within this radius of `position`, in units. `patrol` wins when both are set. | | `speed` | `number` | | Walking speed, in units per second. Defaults by age: child 1.0, adult 1.3, elder 0.8. | | `pause` | `number` | `1.5` | Seconds held at each waypoint. Walking always stands; the authored `pose` plays here. | | `loop` | `"loop" \| "pingpong"` | `"loop"` | `loop` walks from the last waypoint back to the first; `pingpong` retraces the route. | | `route` | `[x, y, z][]` | | A one-way trip through these waypoints, relative to `position` like `patrol`, setting off at `departAt`. Before then the figure waits at the first point facing its `rotation`; after the last it stays there, facing the way it came, in its `pose`. Wins over `patrol` and `wander`. Give a new trip a new `position` (the old one's end) and `departAt`. | | `departAt` | `number` | `0` | When the `route` trip sets off, in epoch milliseconds (`Date.now()`). Plain data, so every viewer sees the figure at the same point of its trip. Unset, the trip is long over. | | `onArrive` | `function` (code only) | | Called once per trip (`route` + `departAt`) when the figure has arrived, on the first frame it has, so a trip that ended while the world was paused or unmounted still reports. | | `gait` | `number` | | Walk-cycle speed, in units per second, for a figure something else moves (a cart, a script). Unset, a route drives it, and a `Player`'s body walks at the avatar's speed. | | `idle` | `boolean` | `true` | Breathing, weight shift, and blinking. | | `lookAt` | `boolean` | `true` | Turn the head (and eyes) toward the player's avatar (or the camera, when no `Player` is mounted) when it comes within `lookRadius`. | | `lookRadius` | `number` | `9` | How close the avatar must be for `lookAt` to engage, in units. | | `label` | `string` | | Floating name above the head. | | `bubble` | `string` | | A short line in a speech bubble over the head (what the figure is saying or doing right now). Long lines wrap and are cut after a few. | | `emote` | `"sleep" \| "alert" \| "think" \| "happy" \| "coffee"` | | A small animated sign over the head: `sleep` (rising z's), `alert` (a bouncing !), `think` (pulsing dots), `happy` (a heart), `coffee` (a steaming cup). | | `actions` | `InteractionAction[]` | | What the player can do with this figure (`Talk`, `Info`). When the avatar comes within `actionRadius` a prompt shows them over the head, with the key each world `controls` action is bound to; pressing one calls `onAction` with its `id`. Only the nearest figure (or other `Interactable`) in range shows a prompt and takes the key. | | `actionRadius` | `number` | `2` | How close the avatar must come for `actions`, in units. | | `onAction` | `function` (code only) | | | | `collider` | `boolean` | `true` | Capsule collider, so the figure is something you bump into. | | `physics` | `boolean` | `true` | Render as a bare visual with no `RigidBody`: for a parent that owns the physics (e.g. as `Player`'s third-person avatar). When false, `collider` is ignored. | | `detail` | `"low" \| "high" \| "auto"` | `"auto"` | `auto` builds the full-detail mesh only within ~10 units of the camera; `high` always; `low` never. | | `position` | `[x, y, z]` | | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ### `PersonBodySpec` | Prop | Type | Default | Description | |---|---|---|---| | `build` | `"slim" \| "average" \| "stocky"` | | Limb thickness and torso depth. | | `tone` | `color` | | Skin color. | | `height` | `number` | | Standing height, in units. | ### `PersonFaceSpec` | Prop | Type | Default | Description | |---|---|---|---| | `eyes` | `eyes` | | | | `brows` | `brows` | | Brow color defaults to the hair color; `weight` thickens them. | | `nose` | `"button" \| "straight" \| "broad"` | | | | `lips` | `lips` | | | | `blush` | `number` | | Cheek flush, 0 (none) to 1. | ### `Person.eyes` | Prop | Type | Default | Description | |---|---|---|---| | `color` | `color` | | | | `size` | `number` | | | ### `Person.brows` | Prop | Type | Default | Description | |---|---|---|---| | `color` | `color` | | | | `weight` | `number` | | | ### `Person.lips` | Prop | Type | Default | Description | |---|---|---|---| | `color` | `color` | | | | `fullness` | `number` | | | ### `PersonGarment` | Prop | Type | Default | Description | |---|---|---|---| | `type` | `"tshirt" \| "shirt" \| "sweater" \| "hoodie" \| "blazer" \| "vest" \| "tunic" \| "coat" \| "dress" \| "robe" \| "apron" \| "trousers" \| "jeans" \| "shorts" \| "skirt" \| "shoes" \| "boots" \| "belt" \| "scarf" \| "cape"` | **required** | | | `color` | `color` | | | | `sleeves` | `"none" \| "short" \| "long"` | | Tops only. | | `neck` | `"crew" \| "v" \| "collar"` | | Tops only. | | `length` | `number` | | 0 to 1: how far it reaches (a top's hem, a leg's length, a skirt or boot's drop). | ### `PersonSkin` | Prop | Type | Default | Description | |---|---|---|---| | `outfit` | `"tshirt" \| "shirt" \| "sweater" \| "hoodie" \| "blazer" \| "vest" \| "tunic" \| "coat" \| "dress" \| "robe" \| "apron" \| "uniform"` | | | | `clothes` | `PersonGarment[]` | | Full layer stack; wins over `outfit`. | | `topColor` | `color` | | | | `bottomColor` | `color` | | | | `shoeColor` | `color` | | | | `hat` | `"none" \| "cap" \| "straw" \| "brim" \| "bandana" \| "hood" \| "chef"` | | | | `hair` | `"short" \| "long" \| "swept" \| "bun" \| "ponytail" \| "braid" \| "cropped" \| "bald"` | | | | `hairColor` | `color` | | | | `accessories` | `"belt" \| "scarf" \| "cape" \| "bag" \| "glasses" \| "staff"[]` | | | ### `PersonHairSpec` | Prop | Type | Default | Description | |---|---|---|---| | `style` | `"short" \| "long" \| "swept" \| "bun" \| "ponytail" \| "braid" \| "cropped" \| "bald"` | | | | `color` | `color` | | | ### `InteractionAction` | Prop | Type | Default | Description | |---|---|---|---| | `id` | `string` | **required** | Passed back to `onAction`. | | `label` | `string` | **required** | Shown in the prompt, e.g. `Talk`. | | `control` | `string` | **required** | The world `controls` action that triggers it (declare `talk: ['KeyT']` in the world's `controls`). The prompt shows the key bound to it, so a remap relabels it. | ## Migrate **v0.13.0 → v0.14.0.** The figure is rebuilt as one sculpted, skinned body with clothes as layers, and the default `style` is now `stylized`. No prop was removed: every flat prop still works, and the new part objects (`body`, `face`, `hair`, `clothes`) are optional. **v0.13.0** ```tsx ``` **v0.14.0** ```tsx ``` - Nothing to change to keep a world rendering: the flat props still resolve. The part objects are the long form when you want to set each piece yourself. Pass `style="anime"` for the closest match to the old default look. - Any trait set as a flat prop no longer shifts the seeded rolls of the others, so a few figures with explicit colors may roll a different hairstyle than before. - `clothes` lists layers inner to outer; a top listed before the trousers is tucked in. `clothes={[]}` is the bare base body. ## Registry manifest [https://runek.nullorder.org/r/components/person.json](https://runek.nullorder.org/r/components/person.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add person` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Pillar Source: https://runek.nullorder.org/docs/components/pillar > Column with a plinth, a tapered (optionally fluted) shaft, and a capital. ## Add it ```bash npx @runek/cli add pillar ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Pillar } from './runek/Pillar' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Pillar", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `height` | `number` | `3` | Height, in units. | | `radius` | `number` | `0.28` | Shaft radius at the base, in units. | | `flutes` | `number` | `0` | Vertical flutes around the shaft; 0 = smooth. | | `color` | `color` | palette `stone` | Defaults to the world palette's `stone` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/pillar.json](https://runek.nullorder.org/r/components/pillar.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add pillar` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Plant Source: https://runek.nullorder.org/docs/components/plant > Potted plant: a tapered planter and a seeded cluster of foliage. ## Add it ```bash npx @runek/cli add plant ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Plant } from './runek/Plant' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Plant", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `height` | `number` | `0.7` | Height, in units. | | `potColor` | `color` | palette `wood` | Planter color; defaults to the world palette's `wood` slot. | | `foliageColor` | `color` | palette `foliage` | Foliage color; defaults to the world palette's `foliage` slot. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/plant.json](https://runek.nullorder.org/r/components/plant.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add plant` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Player Source: https://runek.nullorder.org/docs/components/player > First/third-person character controller (ecctrl wrapper). WASD moves; mouse-drag or the arrow keys steer the camera in either view. Reads the world's `avatar` setting for its default view and the world's `controls` for key bindings. ## Add it ```bash npx @runek/cli add player ``` Pulls `@react-three/drei@^10.7.7`, `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `ecctrl@^1.0.97`, `three@^0.184.0`. ## Use it ```tsx import { Player } from './runek/Player' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Player", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 3, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `view` | `"first" \| "third" \| "overhead"` | | Camera view: `first` (through the avatar's eyes), `third` (behind it), or `overhead` (high above at a fixed tilt, following it; scroll zooms, WASD walks relative to the screen). Unset defers to the world default (``); falls back to first-person. An explicit value here always wins. A world `view` control (e.g. `controls: { view: ['KeyV'] }`) cycles the three at runtime. | | `yaw` | `number` | `0` | Initial camera yaw in radians (0 faces +z). | | `children` | `node` (code only) | | Custom avatar visual, replacing the default capsule. Size it to the capsule envelope (~1.3 units tall, centered at the character origin); it is hidden in first-person view. In world JSON, nest it as a child node of the Player. | ## Registry manifest [https://runek.nullorder.org/r/components/player.json](https://runek.nullorder.org/r/components/player.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add player` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Pool Source: https://runek.nullorder.org/docs/components/pool > A built swimming pool flush with the deck: coping rim, walkable plastered basin with corner exit steps, and a translucent water surface. ## Add it ```bash npx @runek/cli add pool ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Pool } from './runek/Pool' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Pool", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `size` | `[number, number]` | `[6, 3.5]` | Water surface `[width, depth]`, in units. | | `depth` | `number` | `1.6` | Basin depth below the deck, in units. | | `coping` | `number` | `0.35` | Coping ledge width around the rim, in units. | | `steps` | `boolean` | `true` | Build exit steps into one corner of the basin. | | `copingColor` | `color` | palette `wall` | Coping color; defaults to the world palette's `wall` slot. | | `basinColor` | `color` | `"#bfe0e6"` | Basin plaster color. | | `waterColor` | `color` | palette `waterShallow` | Defaults to the world palette's `waterShallow` slot. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/pool.json](https://runek.nullorder.org/r/components/pool.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add pool` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # PoolTable Source: https://runek.nullorder.org/docs/components/pooltable > A pool table with rails, cushions, six pockets, and sixteen instanced balls, racked or scattered mid-game. ## Add it ```bash npx @runek/cli add pooltable ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { PoolTable } from './runek/PoolTable' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "PoolTable", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `length` | `number` | `2.5` | Outer length along local X, in units. | | `width` | `number` | `1.4` | Outer width along local Z, in units. | | `height` | `number` | `0.8` | Height of the playing surface, in units. | | `balls` | `"none" \| "racked" \| "scattered"` | | Seeded when unset. | | `color` | `color` | | The cloth. Seeded from a few classic colors when unset. | | `frameColor` | `color` | | Rails and legs; defaults to the palette's `wood`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/pooltable.json](https://runek.nullorder.org/r/components/pooltable.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add pooltable` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Portal Source: https://runek.nullorder.org/docs/components/portal > A travel gate: a glowing sensor ring that fires onEnter (or navigates to a destination) when the avatar or a vehicle passes through — for world-to-world travel, level transitions, and teleports. ## Add it ```bash npx @runek/cli add portal ``` Pulls `sign`, `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Portal } from './runek/Portal' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Portal", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `to` | `string` | | Where the portal leads — a URL or app route. Kept JSON-serializable so a world round-trips from data. With no `onEnter`, entering navigates here (`window.location.href = to`); with `onEnter`, this is just passed through to it. | | `onEnter` | `function` (code only) | | Called once when the avatar (or a vehicle) enters the gate, with `to`. Optional, so the component still renders and round-trips from data without it (CONTRACT §1). Use it to drive an in-app transition instead of a full navigation — e.g. swap the mounted world. Note: a host rendered inside `` lives in a separate React reconciler from the page, so bridge this callback out through a module-level store, not React context. | | `label` | `string` | | Floating caption above the gate (e.g. the destination's name). | | `radius` | `number` | `2.2` | Ring radius, in units. | | `color` | `color` | palette `accent` | Glow / ring color; defaults to the world palette's `accent` slot. | | `active` | `boolean` | `true` | Arm the trigger. Set false for a decorative or not-yet-open gate. Default true. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/portal.json](https://runek.nullorder.org/r/components/portal.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add portal` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Road Source: https://runek.nullorder.org/docs/components/road > A paved street: a flat stone deck with low kerbs, draped on the ground like Path; decorative (no collider). ## Add it ```bash npx @runek/cli add road ``` Pulls `@runek/core@^0.13.0`. ## Use it ```tsx import { Road } from './runek/Road' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Road", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `length` | `number` | `12` | Length along local Z, in units. | | `width` | `number` | `3` | Width along local X, in units. | | `color` | `color` | | Deck color; defaults to the palette's `stone`. | | `kerbColor` | `color` | | Kerb color; defaults to the palette's `woodDark`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/road.json](https://runek.nullorder.org/r/components/road.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add road` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Rocks Source: https://runek.nullorder.org/docs/components/rocks > Faceted rocks with convex-hull colliders (seeded scatter). ## Add it ```bash npx @runek/cli add rocks ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Rocks } from './runek/Rocks' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Rocks", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `count` | `number` | `6` | | | `spread` | `number` | `3` | Cluster radius, in units. | | `size` | `number` | `0.6` | Mean rock radius, in units. | | `hue` | `number` | `30` | | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/rocks.json](https://runek.nullorder.org/r/components/rocks.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add rocks` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Roof Source: https://runek.nullorder.org/docs/components/roof > Flat or gable roof. ## Add it ```bash npx @runek/cli add roof ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Roof } from './runek/Roof' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Roof", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `size` | `[number, number]` | `[8, 8]` | `[width, depth]` in units. The roof rests with its base at the component origin. | | `style` | `"flat" \| "gable"` | `"gable"` | | | `peak` | `number` | `1.6` | Ridge height for a gable roof, in units. | | `thickness` | `number` | `0.18` | Thickness, in units. | | `overhang` | `number` | `0.3` | | | `ends` | `boolean` | `true` | Cap the triangular gable ends so the attic isn't open to the outside. | | `color` | `color` | palette `roof` | Defaults to the world palette's `roof` slot. | | `endColor` | `color` | palette `wall` | Gable end caps; defaults to the world palette's `wall` slot. | ## Migrate **v0.11.0 → v0.12.0.** No signature break, but gable roofs now cap their open triangular ends with wall-colored prisms (the attic used to show the sky). The old look is one prop away. **v0.11.0** ```tsx // v0.11.0: gable ends rendered open ``` **v0.12.0** ```tsx // v0.12.0: ends are capped by default // opt back into the open look ``` - The caps default to the palette `wall` slot; override with `endColor`. ## Registry manifest [https://runek.nullorder.org/r/components/roof.json](https://runek.nullorder.org/r/components/roof.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add roof` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Room Source: https://runek.nullorder.org/docs/components/room > Composite: four walls and a floor with a doorway, arranged from one Level. Unpack to customize. **Room is a composite**: a data arrangement of parts, not code. `add` copies the arrangement JSON plus the source of every part it references. ## Add it ```bash npx @runek/cli add room ``` Pulls `level`. ## Use it Place it in a world by type — the renderer expands the arrangement in place: ```json { "type": "Room", "props": { "position": [0, 0, 0], "seed": 1 } } ``` Register the arrangement in your registry map next to the parts it uses: ```ts import room from './runek/composites/room.json' const registry = { /* …parts… */, Room: room as unknown as CompositeDef } ``` An instance `seed` deterministically re-rolls every child that doesn't pin its own. In the editor, **Unpack** replaces an instance with its editable arrangement (a `Group` of ordinary nodes) for per-instance customization. ## Arrangement ```json { "kind": "composite", "name": "Room", "description": "Four walls and a floor with a front doorway — one Level with an opening. Unpack it to add a ceiling, windows, or more openings.", "bounds": [8, 3, 8], "groundSitting": true, "nodes": [ { "type": "Level", "props": { "size": [8, 8], "height": 3, "walls": { "front": { "openings": [{ "width": 1.4, "height": 2 }] } } } } ] } ``` ## Migrate **v0.11.0 → v0.12.0.** `Room` is no longer a coded component — the registry name now resolves to a **composite**: one `Level` with a front doorway. For JSX composition, reach for `Level` directly; it is the same walls-plus-floor ring with per-side openings. **v0.11.0** ```tsx import { Room } from './runek/Room' ``` **v0.12.0** ```tsx import { Level } from './runek/Level' ``` - World files need nothing: `{ "type": "Room" }` keeps rendering via the composite. - The old `roof` ceiling is a `Floor` on top: `` (its origin is its top surface), or unpack the composite and extend it. - [`scripts/migrate-buildings.mjs`](https://github.com/nullorder/runek/blob/main/scripts/migrate-buildings.mjs) bakes old `Room` nodes with non-default props into the equivalent parts. - The `RoomProps` type no longer exists. ## Registry manifest [https://runek.nullorder.org/r/components/room.json](https://runek.nullorder.org/r/components/room.json): Self-contained JSON: the inlined arrangement plus resolved dependencies, exactly what `runek add room` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Rug Source: https://runek.nullorder.org/docs/components/rug > Procedural striped rug (seeded stripes, no textures). ## Add it ```bash npx @runek/cli add rug ``` Pulls `@runek/core@^0.13.0`. ## Use it ```tsx import { Rug } from './runek/Rug' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Rug", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `size` | `[number, number]` | `[3, 2]` | `[width, depth]` in units. | | `baseColor` | `color` | palette `fabric` | Defaults to the world palette's `fabric` slot. | | `borderColor` | `color` | palette `accent` | Defaults to the world palette's `accent` slot. | | `accentColor` | `color` | `"#9c5252"` | | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/rug.json](https://runek.nullorder.org/r/components/rug.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add rug` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Sailboat Source: https://runek.nullorder.org/docs/components/sailboat > A small procedural sailboat: a station-built low-poly hull, mast, boom, and mainsail, floating at the waterline with a gentle moored bob. ## Add it ```bash npx @runek/cli add sailboat ``` Pulls `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Sailboat } from './runek/Sailboat' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Sailboat", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `length` | `number` | `5.5` | Hull length along local Z (bow at +Z), in units. | | `beam` | `number` | `2` | Hull maximum beam (width along X), in units. | | `freeboard` | `number` | `0.5` | Deck height above the waterline (local y=0), in units. | | `draft` | `number` | `0.6` | Keel depth below the waterline, in units. | | `mastHeight` | `number` | `4.6` | Mast height above the deck, in units. | | `boomLength` | `number` | `3.2` | Boom length aft of the mast, in units. | | `sail` | `boolean` | `true` | Raise the mainsail. | | `bob` | `boolean` | `true` | Gentle moored bobbing on the swell. | | `collider` | `boolean` | `true` | Solid hull collider. | | `physics` | `boolean` | `true` | Render the hull/rig as a bare visual, with no `RigidBody` or collider — for a parent controller (e.g. a steerable vehicle) that owns the physics body. When false, `position` and `rotation` place the visual directly and the `collider` prop is ignored. Default true. | | `color` | `color` | | Hull color; defaults to the world palette's `wood`. | | `trimColor` | `color` | | Deck, mast, and boom color; defaults to the palette's `woodDark`. | | `sailColor` | `color` | `"#eee6d0"` | Sail color. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/sailboat.json](https://runek.nullorder.org/r/components/sailboat.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add sailboat` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Shelf Source: https://runek.nullorder.org/docs/components/shelf > Wall shelf with planks. ## Add it ```bash npx @runek/cli add shelf ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Shelf } from './runek/Shelf' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Shelf", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `1` | Width, in units. | | `height` | `number` | `1.8` | Height, in units. | | `depth` | `number` | `0.3` | Depth, in units. | | `shelves` | `number` | `4` | | | `color` | `color` | palette `wood` | Defaults to the world palette's `wood` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/shelf.json](https://runek.nullorder.org/r/components/shelf.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add shelf` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Shore Source: https://runek.nullorder.org/docs/components/shore > Sloped beach/shore strip to meet a Lake. ## Add it ```bash npx @runek/cli add shore ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Shore } from './runek/Shore' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Shore", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `size` | `[number, number]` | `[24, 24]` | `[width, depth]` in units. The sandy top sits at the component origin. | | `thickness` | `number` | `0.3` | Thickness, in units. | | `color` | `color` | palette `sand` | Defaults to the world palette's `sand` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/shore.json](https://runek.nullorder.org/r/components/shore.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add shore` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Sign Source: https://runek.nullorder.org/docs/components/sign > In-world text rendered in a world font (the one font-using component); falls back to the default bundled in @runek/core. ## Add it ```bash npx @runek/cli add sign ``` Pulls `@react-three/drei@^10.7.7`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Sign } from './runek/Sign' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Sign", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `children` | `string` | **required** | The text to render. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `variant` | `"display" \| "body"` | `"display"` | Which world font role to render in (`display` for titles/signage, `body` for labels). The world declares the actual face via ``; an undeclared role falls back to the font bundled in `@runek/core`. (Named `variant`, not `role`, so it doesn't trip JSX a11y linters.) | | `font` | `string` | | Explicit font URL, overriding the world role. | | `size` | `number` | `0.3` | Cap height in units. | | `color` | `color` | palette `accent` | Text color. Defaults to the world palette's `accent` slot. | | `maxWidth` | `number` | | Wrap width in units; omit to keep the text on one line. | | `letterSpacing` | `number` | `0` | | | `anchorX` | `"left" \| "center" \| "right"` | `"center"` | | | `anchorY` | `"top" \| "bottom" \| "middle"` | `"middle"` | | | `glow` | `boolean` | `false` | Soft colored halo around the glyphs, for a glow without bloom. | | `outline` | `color` | | A crisp edge in this color, so the text reads against any background. Wins over `glow`. | | `onBounds` | `function` (code only) | | Called with the laid-out text's width and height, in world units, whenever it changes: for fitting a backdrop behind it. | ## Registry manifest [https://runek.nullorder.org/r/components/sign.json](https://runek.nullorder.org/r/components/sign.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add sign` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Signpost Source: https://runek.nullorder.org/docs/components/signpost > A wooden post-and-plank signboard carrying a name in the world's display font; the post sits behind the board so the text reads clean. ## Add it ```bash npx @runek/cli add signpost ``` Pulls `sign`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Signpost } from './runek/Signpost' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Signpost", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `name` | `string` | `""` | The name shown on the board. A plain string, **not** `children`: a world renderer overrides a node's `children` prop with its nested nodes, so text authored in JSON must be a named prop to survive. Empty by default — `` is a blank board. | | `height` | `number` | `2.6` | Post height, in units. | | `width` | `number` | `3.4` | Board width, in units. | | `color` | `color` | | Post + board wood color; defaults to the palette's `wood`. | | `textColor` | `color` | | Name color; defaults to the palette's `sand` (legible on the wood). | | `size` | `number` | `0.5` | Name cap height, in units. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/signpost.json](https://runek.nullorder.org/r/components/signpost.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add signpost` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Sky Source: https://runek.nullorder.org/docs/components/sky > Procedural atmosphere (drei Sky). ## Add it ```bash npx @runek/cli add sky ``` Pulls `@react-three/drei@^10.7.7`, `@react-three/fiber@^9.6.1`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Sky } from './runek/Sky' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Sky", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `sunPosition` | `[x, y, z]` | | Direction of the sun; also where the bright spot appears. Set this to pin the sky to a fixed sun and bypass the world's day/night cycle. | | `turbidity` | `number` | `8` | | | `rayleigh` | `number` | `1.4` | | | `nightColor` | `color` | `"#04060e"` | Background when the sun is below the horizon. | ## Registry manifest [https://runek.nullorder.org/r/components/sky.json](https://runek.nullorder.org/r/components/sky.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add sky` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Slab Source: https://runek.nullorder.org/docs/components/slab > A rounded structural plate (pill or ellipse) for curved-modernist decks, cantilevered roofs, terraces, or a lawn disc. Walkable, with a single convex-hull collider. ## Add it ```bash npx @runek/cli add slab ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Slab } from './runek/Slab' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Slab", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `size` | `[number, number]` | `[8, 8]` | Footprint `[width, depth]`, in units. The top surface sits at the component origin. | | `shape` | `"pill" \| "disc"` | `"pill"` | `pill` is a rounded rectangle, `disc` an ellipse. | | `cornerRadius` | `number` | | Pill corner radius, in units; clamped to half the smaller extent (a full stadium). | | `thickness` | `number` | `0.35` | Thickness, in units. | | `curveSegments` | `number` | `24` | Curve smoothness of the rounded edges. | | `color` | `color` | palette `wall` | Defaults to the world palette's `wall` slot. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/slab.json](https://runek.nullorder.org/r/components/slab.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add slab` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Sofa Source: https://runek.nullorder.org/docs/components/sofa > A sofa seating one to four: upholstered frame, seat and back cushions in soft shades of the fabric, square, rolled, or slim arms, pegs or a plinth, and throw pillows. Seeded, each choice pinnable. ## Add it ```bash npx @runek/cli add sofa ``` Pulls `@react-three/drei@^10.7.7`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Sofa } from './runek/Sofa' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Sofa", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `seats` | `number` | `3` | Seats across, 1 to 4. | | `depth` | `number` | `0.9` | Depth along local Z, in units. Seats face +Z. | | `seatHeight` | `number` | `0.44` | Top of the seat cushions, in units. | | `arms` | `"slim" \| "none" \| "square" \| "rolled"` | | Seeded when unset. | | `cushions` | `"split" \| "bench"` | | Seeded when unset. | | `legs` | `"pegs" \| "plinth"` | | Seeded when unset. | | `pillows` | `number` | | Throw pillows, 0 to 2. Seeded when unset. | | `color` | `color` | | Upholstery; defaults to the palette's `fabric`. | | `pillowColor` | `color` | | Throw pillows; defaults to the palette's `accent`. | | `legColor` | `color` | | Legs; defaults to the palette's `woodDark`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/sofa.json](https://runek.nullorder.org/r/components/sofa.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add sofa` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Staircase Source: https://runek.nullorder.org/docs/components/staircase > Stepped staircase with per-step colliders. ## Add it ```bash npx @runek/cli add staircase ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Staircase } from './runek/Staircase' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Staircase", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `steps` | `number` | `6` | | | `totalHeight` | `number` | `1.5` | Total rise, in units. Ascends along +y and +z from the origin. | | `width` | `number` | `1.2` | Width, in units. | | `depth` | `number` | `2.4` | Total run (depth), in units. | | `color` | `color` | palette `stone` | Defaults to the world palette's `stone` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/staircase.json](https://runek.nullorder.org/r/components/staircase.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add staircase` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Stool Source: https://runek.nullorder.org/docs/components/stool > A round bar stool: a disc seat on three splayed legs; a thin cylinder collider. ## Add it ```bash npx @runek/cli add stool ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Stool } from './runek/Stool' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Stool", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `height` | `number` | `0.95` | Seat height above the ground, in units. | | `radius` | `number` | `0.24` | Seat radius, in units. | | `color` | `color` | | Color; defaults to the palette's `wood`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/stool.json](https://runek.nullorder.org/r/components/stool.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add stool` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Table Source: https://runek.nullorder.org/docs/components/table > Table with a top on four legs, or on pedestals with rounded ends for a conference table. ## Add it ```bash npx @runek/cli add table ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Table } from './runek/Table' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Table", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `1.2` | Width, in units. | | `depth` | `number` | `0.8` | Depth, in units. | | `height` | `number` | `0.75` | Height, in units. | | `thickness` | `number` | `0.05` | Thickness, in units. | | `base` | `"legs" \| "pedestal"` | `"legs"` | Four `legs` at the corners, or `pedestal` columns on crossed feet (one, or two for a table longer than 2 units): a conference table. | | `ends` | `"square" \| "round"` | `"square"` | `round` makes the short ends semicircles: a racetrack top, as conference tables have. | | `color` | `color` | palette `wood` | Defaults to the world palette's `wood` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/table.json](https://runek.nullorder.org/r/components/table.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add table` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Tent Source: https://runek.nullorder.org/docs/components/tent > An A-frame ridge tent: striped wind-rippled fabric, entrance flaps pinned open, guy ropes and stakes; solid sides and back, open front. ## Add it ```bash npx @runek/cli add tent ``` Pulls `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Tent } from './runek/Tent' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Tent", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `width` | `number` | `3` | Width across the tent (local X), in units. | | `depth` | `number` | `3.6` | Depth front-to-back (local Z); the entrance faces +Z. | | `height` | `number` | `2.2` | Ridge height, in units. | | `color` | `color` | | Fabric color; defaults to the palette's `fabric`. | | `stripeColor` | `color` | | Alternate stripe color; defaults to the palette's `wall`. | | `poleColor` | `color` | | Pole color; defaults to the palette's `woodDark`. | | `wind` | `number` | `0.05` | Billow depth as a fraction of the width. | | `windSpeed` | `number` | `2.5` | Wind ripple speed. | | `sag` | `number` | `0.03` | Static inward drape of the fabric between ridge and ground, as a fraction of the width. | | `collider` | `boolean` | `true` | Solid walls (the two sides + the back) you can't walk through; the front stays open. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | ## Registry manifest [https://runek.nullorder.org/r/components/tent.json](https://runek.nullorder.org/r/components/tent.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add tent` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Terrain Source: https://runek.nullorder.org/docs/components/terrain > Procedural fbm-displaced ground with a matching trimesh collider, a flat build-pad option, and an optional radial island falloff. Its surface is queryable (`useGround`, anchors, `runek check-world`). ## Add it ```bash npx @runek/cli add terrain ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Terrain } from './runek/Terrain' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Terrain", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `size` | `[number, number]` | `[40, 40]` | Ground extent `[width, depth]`, in units. | | `thickness` | `number` | `0.4` | Thickness, in units. | | `color` | `color` | palette `ground` | Defaults to the world palette's `ground` slot. | | `relief` | `number` | `0` | Vertical relief amplitude, in units. 0 keeps the ground flat. | | `resolution` | `number` | `64` | Grid subdivisions for displaced ground. | | `frequency` | `number` | `0.04` | Noise frequency. | | `flatRadius` | `number` | `0` | Radius from center kept flat (for a build pad), in units. | | `falloff` | `number` | `0` | Radial island falloff (0 = off). When set, the ground domes up toward the center and sinks below the world ground at its rim, so the mesh reads as a landmass surrounded by water. The value is the fraction of the half-extent that stays land before the coast (e.g. 0.8 = land out to 80% of the radius, then a shoreline into the sea). | | `collider` | `boolean` | `true` | Register a collider (default true). Set false for distant/backdrop terrain the player never walks, to skip a large trimesh collider. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/terrain.json](https://runek.nullorder.org/r/components/terrain.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add terrain` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Trees Source: https://runek.nullorder.org/docs/components/trees > L-system trees grown by a 3D turtle, deterministic from seed. ## Add it ```bash npx @runek/cli add trees ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Trees } from './runek/Trees' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Trees", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | | `iterations` | `number` | `2` | | | `segmentLength` | `number` | `0.7` | Base branch length, in units. | | `angle` | `number` | `0.5` | Branching angle, in radians. | | `trunkColor` | `color` | palette `bark` | Defaults to the world palette's `bark` slot. | | `leafColor` | `color` | palette `foliage` | Defaults to the world palette's `foliage` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/trees.json](https://runek.nullorder.org/r/components/trees.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add trees` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Tv Source: https://runek.nullorder.org/docs/components/tv > A flat-screen TV on a stand, a low media console, or a wall. Switched on, it shows a seeded picture (sky, sun, hills) drawn from flat shapes. ## Add it ```bash npx @runek/cli add tv ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Tv } from './runek/Tv' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Tv", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `width` | `number` | `1.2` | Screen width, in units. | | `mount` | `"stand" \| "console" \| "wall"` | | `stand` and `console` are seeded when unset; `wall` must be asked for. On a wall the origin is the middle of the screen with its back against the wall (local -Z). | | `on` | `boolean` | `true` | Showing a seeded picture (a sky, a sun, hills) in this tint, or dark glass when off. | | `screen` | `color` | | Picture tint. Seeded when unset. | | `color` | `color` | | Bezel and stand; defaults to the palette's `metal`. | | `consoleColor` | `color` | | The console's wood; defaults to the palette's `woodDark`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/tv.json](https://runek.nullorder.org/r/components/tv.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add tv` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Wall Source: https://runek.nullorder.org/docs/components/wall > Wall segment with door/window openings and a fixed collider. ## Add it ```bash npx @runek/cli add wall ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Wall } from './runek/Wall' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Wall", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `4` | Length along the wall's local X axis, in units. | | `height` | `number` | `3` | Height, in units. | | `thickness` | `number` | `0.2` | Thickness, in units. | | `color` | `color` | palette `wall` | Defaults to the world palette's `wall` slot. | | `openings` | `WallOpening[]` | | Holes cut into the wall (doors, windows). Must not overlap horizontally. | ### `WallOpening` | Prop | Type | Default | Description | |---|---|---|---| | `offset` | `number` | | Horizontal center offset from the wall center, in units. | | `width` | `number` | **required** | | | `height` | `number` | **required** | | | `sill` | `number` | | Height of the opening's base above the wall base, in units. | ## Migrate **v0.11.0 → v0.12.0.** The single `opening` prop became `openings`, an array, so one wall can carry a door and windows together. The `WallOpening` shape itself is unchanged (`offset`, `width`, `height`, `sill`). **v0.11.0** ```tsx ``` **v0.12.0** ```tsx ``` - Mechanical migration: wrap the object in an array — `opening={X}` becomes `openings={[X]}`; in world files, `"opening": {…}` becomes `"openings": [{…}]`. - Openings are laid out by `offset` (center offset from the wall center, in units) and must not overlap horizontally. ## Registry manifest [https://runek.nullorder.org/r/components/wall.json](https://runek.nullorder.org/r/components/wall.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add wall` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Well Source: https://runek.nullorder.org/docs/components/well > Stone well with a dark pool, posts, a little pyramid roof, and a bucket on a rope. ## Add it ```bash npx @runek/cli add well ``` Pulls `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`. ## Use it ```tsx import { Well } from './runek/Well' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Well", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `radius` | `number` | `0.7` | Radius, in units. | | `wallHeight` | `number` | `0.8` | | | `color` | `color` | palette `stone` | Stone color; defaults to the world palette's `stone` slot. | | `roofColor` | `color` | palette `wood` | Roof + frame color; defaults to the world palette's `wood` slot. | ## Registry manifest [https://runek.nullorder.org/r/components/well.json](https://runek.nullorder.org/r/components/well.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add well` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Whiteboard Source: https://runek.nullorder.org/docs/components/whiteboard > A whiteboard on a rolling stand or a wall, with seeded marker diagrams (boxes, arrows, lines of writing) and optional text across the top. ## Add it ```bash npx @runek/cli add whiteboard ``` Pulls `sign`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Whiteboard } from './runek/Whiteboard' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Whiteboard", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `width` | `number` | `1.6` | Board width, in units. | | `height` | `number` | `1` | Board height, in units. | | `mount` | `"stand" \| "wall"` | `"stand"` | | | `text` | `string` | | Written across the top of the board in marker. | | `doodles` | `boolean` | `true` | Seeded marker diagrams (boxes, arrows, lines). Set false for a clean board. | | `color` | `color` | | Frame and stand; defaults to the palette's `metal`. | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/whiteboard.json](https://runek.nullorder.org/r/components/whiteboard.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add whiteboard` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Windmill Source: https://runek.nullorder.org/docs/components/windmill > A tapered tower with a conical cap and four sails that turn each frame; tower collider, seeded blade phase. ## Add it ```bash npx @runek/cli add windmill ``` Pulls `@react-three/fiber@^9.6.1`, `@react-three/rapier@^2.2.0`, `@runek/core@^0.13.0`, `three@^0.184.0`. ## Use it ```tsx import { Windmill } from './runek/Windmill' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Windmill", "props": { "position": [0, 0, 0], "seed": 1 } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `height` | `number` | `7` | Tower height, in units. | | `radius` | `number` | `2` | Tower base radius, in units. | | `sailLength` | `number` | `4.5` | Sail (blade) length, in units. | | `sailSpeed` | `number` | `0.4` | Sail rotation speed, in radians per second. | | `color` | `color` | | Tower color (defaults to the world palette's `wall`). | | `trimColor` | `color` | | Cap, door, and sail-frame color (defaults to the world palette's `wood`). | | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | `1` | Seed for the deterministic variation: same seed, same result. | ## Registry manifest [https://runek.nullorder.org/r/components/windmill.json](https://runek.nullorder.org/r/components/windmill.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add windmill` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**. --- # Window Source: https://runek.nullorder.org/docs/components/window > Window with frame and translucent pane. ## Add it ```bash npx @runek/cli add window ``` Pulls `@runek/core@^0.13.0`. ## Use it ```tsx import { Window } from './runek/Window' ``` Or as a node in a world file ([worlds as data](https://runek.nullorder.org/docs/worlds-as-data.md)): ```json { "type": "Window", "props": { "position": [0, 0, 0] } } ``` ## Props | Prop | Type | Default | Description | |---|---|---|---| | `position` | `[x, y, z]` | `[0, 0, 0]` | Position [x, y, z] in units (1 unit = 1 m, Y-up). | | `rotation` | `[x, y, z]` | `[0, 0, 0]` | Euler rotation [x, y, z] in radians. | | `seed` | `number` | | Accepted for the component contract; this component has no seeded variation yet. | | `width` | `number` | `1.2` | Width, in units. | | `height` | `number` | `1.2` | Height, in units. | | `frame` | `number` | `0.08` | Frame bar thickness, in units. | | `depth` | `number` | `0.1` | Depth, in units. | | `color` | `color` | `"#e8e2d6"` | | | `glassColor` | `color` | `"#acd4e6"` | | ## Registry manifest [https://runek.nullorder.org/r/components/window.json](https://runek.nullorder.org/r/components/window.json): Self-contained JSON: inlined source plus resolved dependencies, exactly what `runek add window` fetches. Browse the whole catalog in the **[gallery →](https://runek.nullorder.org/gallery)**.