FERRETGame engine over Babylon.js
Ferret

@ferret/levels

Level manifests, the level registry, load, unload and travel, environment and the .ferret package format.

156 exports · source in packages/levels/src

Classes

AssetLibrary class

class AssetLibrary implements Service
readonly name: "asset-library"
constructor(private readonly o: AssetLibraryOptions)
init(world: World): void
async open(url?: string): Promise<Catalog>Open the catalog (once; again to refresh).
list(q: LibraryQuery = {}): LibraryEntry[]Items of the catalog, filtered; no package is opened.
has(id: string): boolean
entry(id: string): LibraryEntry
previewUrl(id: string): string | undefinedAbsolute URL of an item's preview image (from the catalog, nothing is opened).
async pkg(pkgId: string): Promise<FerretPackage>The package of the catalog, open in the mount (with its dependencies).
async item<T extends LibraryItem = LibraryItem>(id: string): Promise<T>The item file, read from its package.
async url(ref: string): Promise<string>A URL the engine's loaders can read for a ferret://<package>/<path> reference (a clip set's file, a map).
async texture(id: string, o: { scene?: BABYLON.Scene } = {}): Promise<Partial<Record<'basecolor' | 'normal' | 'orm' | 'emissive', BABYLON.Texture>>>The maps of a texture item as Babylon textures (colour spaces set: base colour and emissive are sRGB, the others linear).
async material(id: string, o: MaterialOptions = {}): Promise<BABYLON.PBRMaterial>A PBR material from a material item (and a variant of it): the description maps one to one to PBRMaterial.
async clipsOf(characterId: string, o: { compatible?: boolean } = {}): Promise<ClipRef[]>The clip sets a character names (and the clip sets of its skeleton in the catalog), with the reference of each file.
async bindClips(c: LibraryCharacter, o: BindOptions = {}): Promise<BoundClips>Put the clip sets of a character on its bones (matched by bone name; a clip set is loaded once however many bodies use it). Waits for the model. groups are one AnimationGroup per clip name, ready for an Animator or group.start().
async character(id: string, o: CharacterOptions = {}): Promise<LibraryCharacter>A character in the world at once (a placeholder until the model is in).
async model(id: string, o: ModelPlacement = {}): Promise<LibraryModel>A static model of the library in the world at once (a placeholder of its bounds until the file is in).
async attach(character: LibraryCharacter, partId: string, o: AttachOptions = {}): Promise<LibraryCharacter | null>Hang a part (a weapon, a hat, a rider) on a body that is in, by a socket of the body's item (sockets: a bone and an offset) or by a bone and an offset of your own. The part follows the bone through every clip. Resolves with the part (dispose it to take it off), or null when the body or the bone is not found. Offsets are in the glTF's own space: metres of the bone's local frame and a quaternion.
async preload(id: string, tier?: LibraryTier): Promise<void>Bring an item's files to the device now (a model's file, a texture's maps), so a later call does not wait.
release(): voidFree what the library made: materials, textures, characters. Packages stay open (they cost the mount nothing but the index).
dispose(): void

packages/levels/src/library.ts:100

CatalogClient class

class CatalogClient
readonly registrations: CatalogRegistration[]every catalog registered through this client, in order
constructor(private readonly levels: LevelService, private readonly mount: PackageMount, private readonly store: AssetStore, private readonly defaults: CatalogOptions = {})
async register(url: string, o: CatalogOptions = {}): Promise<CatalogRegistration>

packages/levels/src/catalog.ts:31

CodeLoader class

class CodeLoader
constructor(readonly world: World, readonly mount: PackageMount, private o: CodeLoaderOptions = {})
list(): LoadedCode[]The code loaded so far, in load order.
exports(id: string): LibraryModule | null | undefinedThe exports of a loaded package's code, or undefined.
require(id: string, range = '*'): Promise<LoadedCode>Load the code of a package (and of its dependencies first) and run its install once. Resolves at once when it is loaded; concurrent calls share one load. Rejects with the reason when the package may not run code or something is missing.
dispose(): voidCall the dispose that install returned, newest first (the modules themselves stay: a page cannot unload them).

packages/levels/src/code.ts:64

CollisionChunks class

class CollisionChunks

Keeps the chunks round the player resident. update is cheap (a few dozen boxes); fetching and instancing happen in the background.

constructor(private def: CollisionChunksDef, private assets: PackageAssets, private at: (file: string) => string, private name: string, private places: () => [number, number, number][], private onError: (message: string) => void = () => {})
async ready(): Promise<void>Start the chunks near places and wait for them (the level is not playable before the ground round the spawn is solid).
update(): void
get loaded(): number
dispose(): void

packages/levels/src/collision-chunks.ts:31

LevelService class

class LevelService implements Service

The level registry and travel between levels. travel(id) shows the loading screen (events), removes the old level with everything it made, builds or loads the new one, moves the persistent entities (the player) to the spawn point and announces the new level. Events: level:loading { id, progress }, level:ready { id, report }, level:unloaded { id }, level:failed { id, error }. state is the game's data that survives travel and goes to Platform.state with saveData.

readonly name: "levels"
state: Record<string, unknown>persistent state across levels: inventory, flags, health ... plain JSON
readonly spawns: Map<string, SpawnPoint>
readonly transitions: TransitionRegistrythe game's transitions: register them by name and choose which one a travel uses
current: LevelDef | null
travelling: boolean
report: LevelReport
animGraphs: Record<string, any>the animation graphs of the current level package (its manifest's animGraphs), by name
movement: Record<string, number>the player character values of the current level package (its manifest's movement), over the engine's defaults
libraries?: (manifest: import('./manifest').LevelManifest, levelId: string) => Promise<void>plugged in by installLevelLibraries (./libraries): require the libraries a level's manifest needs before the level is built
packageLoader?: (ctx: LevelContext, level: LevelDef, report: LevelReport) => Promise<void>
readonly persistent: Set<Entity>entities that always travel with the player (the player itself): they are placed at the spawn, never removed
constructor(readonly opts: LevelOptions = {})
init(world: World)
register(def: LevelDef): this
unregister(id: string): booleanRemove a level from the registry (a catalog that no longer lists it). The level being played is not touched.
replace(def: LevelDef): thisRegister, or replace a level of the same id that is not the one being travelled to (a catalog update).
catalogs?: { register(url: string, o?: unknown): Promise<unknown> }Plugged in by installCatalogs (./catalog): the remote levels of a catalog.
registerCatalog(url: string, o?: unknown): Promise<unknown>Fetch a catalog and register its levels; needs installCatalogs.
has(id: string): boolean
get(id: string): LevelDef
list(): LevelDef[]
exempt(e: Entity): thisAn entity that is not part of any level though it appears while one loads (another player's avatar): it is not removed with the level.
keep(e: Entity): thisEntities that go with the player to every level.
async travel(id: string, opts: TravelOptions = {}): Promise<LevelReport>
spawn(id?: string): SpawnPoint | nullThe spawn point with that id, or the level's default one.
place(e: Entity, s: SpawnPoint)Put an entity at a spawn point (any entity with a CharacterMovement is teleported properly).
hold<T>(work: Promise<T>, label = 'artefact'): Promise<T>An artefact the level is not ready without. An entity of the level calls this while the level is built (a behaviour's onAttach) with the promise of what it still loads (a character's model, its clips). level:ready is announced when the last of them has settled, or after holdMs; a rejected one is a warning of the report. Outside a travel the hold is ignored. Returns the promise.
get holding(): numberhow many holds of the level being built are not taken up yet or still open
unload()
onUnload(fn: () => void)Run fn when the current level is removed (release assets, clear a service).
saveData(): { level: string | null; state: Record<string, unknown> }What to hand to Platform.saveState: the game's state and where the player is.
loadData(data: { level?: string | null; state?: Record<string, unknown> } | null | undefined): string | nullRestore what saveData made; returns the level to travel to (or null).
dispose()

packages/levels/src/levels.ts:108

LightBudget class

class LightBudget
readonly def: LightBudgetDef
onChange?: (entered: number[], left: number[]) => voidcalled after a re-ranking when real lights changed hands: indices that entered and left the pool
onSounds?: (start: number[], stop: number[]) => voidcalled when the nearest lamps with a sound change: indices to start and to stop
constructor(readonly scene: BABYLON.Scene, def: LightBudgetDef, readonly opts: LightBudgetOptions = {})
get name()
get size()
get real(): number[]the lights that are real now (fading ones included)
get glowMesh()
glowSize(i: number): numberthe size in metres of lamp i's glow dot now (0: hidden, because it is a real light or out of reach)
get lights(): (BABYLON.PointLight | BABYLON.SpotLight)[]
stats(): LightBudgetStats
setBudget(n: number)change the budget while running (a quality switch): the pool grows or shrinks, and the next re-ranking fills it
update(dt: number)one step of the budget: dt seconds since the last. It runs by itself every frame; tests call it with a clock of their own
dispose()

packages/levels/src/light-budget.ts:130

PackageMount class

class PackageMount

Connects packages to a LevelService. add(pkg) registers the package's levels (as <id> for the first package, <pkgid>/<id> for the others, or with the prefix given); url(pkg, path) gives a blob URL for any file of a package, for sounds, textures and video. Nothing is unpacked: a file is read from the package when a level asks for it, and a model's blob URL goes when the level that held it is gone.

readonly packages: PackageSet
readonly assets: MountAssetsgive this to the level loader instead of the asset cache (installPackages does)
readonly loader: PackageLoaderOptionsthe options the level loader runs with: hand this to installEditor as its loader so the editor's draft shows in reloads
constructor(readonly levels: LevelService, private inner: MountAssets, readonly opts: MountOptions = {})
add(pkg: FerretPackage, o: { prefix?: string } = {}): LevelDef[]Open a package into the mount and register its levels. Returns what was registered.
startLevel(pkg: FerretPackage): string | nullThe level a package starts in (entry.level of a game, else its first), as registered.
async url(pkg: FerretPackage, path: string): Promise<string>A blob URL for a file of a package (path inside pkg, or ferret://other/path). Stays valid until dispose.
async fetch(url: string): Promise<FetchResult>The loader's fetch for package urls; anything else goes to the real one.
forget(pkgId: string): voidDrop everything cached for a package (its blob URLs): it was replaced by another version.
dispose(): void

packages/levels/src/ferret.ts:35

PrefetchService class

class PrefetchService implements Service
readonly name: "prefetch"
constructor(private readonly levels: LevelService, o: PrefetchOptions = {})
init(world: World): void
near(zone: string, levelId: string, o: { inSeconds?: number } = {}): () => voidDoors and zones: entering zone makes levelId wanted (the player is probably about to go there).
hint(id: string, o: { inSeconds?: number; from?: 'level' | 'game' } = {}): voidid is wanted. With inSeconds (the player will need it in about that long) the start is put off as long as the estimate allows it to still finish in time; without it the download starts at once (when no travel is running).
focus(ids: string[]): voidOnly these are wanted from now on: every other background download stops at its next file.
cancel(id: string): void
get bandwidth(): numberThe smoothed link speed in bytes per second, 0 when nothing was measured.
state(id: string): JobState | 'none'
async idle(id: string): Promise<void>Resolves when the level's background download has finished (done, cancelled or failed).
status(): { rate: number; jobs: Record<string, { state: JobState; wanted: boolean; bytes: number }> }
dispose(): void

packages/levels/src/prefetch.ts:30

ProbeVolume class

class ProbeVolume

The baked grid on the GPU and the plugin on every PBR material of the scene (also those added later).

readonly textures: BABYLON.RawTexture3D[]
enabled: boolean
constructor(readonly scene: BABYLON.Scene, readonly def: ProbeVolumeDef, data: ArrayBuffer)
setEnabled(on: boolean)Switch the baked light on or off (the materials recompile).
dispose()

packages/levels/src/probe-volume.ts:88

RuntimeAssets class

class RuntimeAssets implements Service
readonly name: "runtime-assets"
constructor(private readonly o: RuntimeAssetsOptions)
init(world: World): void
async bytes(ref: string, o: Pick<RequireOptions, 'priority' | 'signal' | 'sha256' | 'size' | 'pkg'> = {}): Promise<Uint8Array>The verified bytes of any reference.
async url(ref: string, o: Pick<RequireOptions, 'priority' | 'signal' | 'sha256' | 'size' | 'pkg'> = {}): Promise<string>A URL the engine's loaders (Audio, Texture, video) can read, for any reference.
status(ref: string): string
model(ref: string, o: ModelOptions = {}): ModelHandleAsk for a model. Returns at once with the placeholder in place.
get scene(): BABYLON.Scene
get events()
get models()
forget(h: ModelHandleImpl): void
async modelUrl(ref: string, o: ModelOptions): Promise<string>
dispose(): void

packages/levels/src/runtime.ts:54

StreamingDebugView class

class StreamingDebugView
constructor(private readonly world: World, private readonly sv: StreamingService, private readonly block: () => { cells: Record<string, { bounds: number[] }> } | null)
get visible(): boolean
show(): void
hide(): void

packages/levels/src/streaming-debug.ts:12

StreamingService class

class StreamingService implements Service
readonly name: "streaming"
tuning: { radiusScale: number; budgetScale: number; memoryScale: number; }Runtime scales on top of the options
init(world: World): void
get active(): boolean
blockDef(): StreamingBlock | nullthe block being streamed (for the debug view and tools)
begin(block: StreamingBlock, source: CellSource, o: StreamingOptions): voidStart streaming a level. The cells within loadRadius of the focus are what ready() waits for.
end(): voidStop streaming and release everything this service added. Called when the level is unloaded.
memory(): { resident: number; cap: number | null; pinned: number }Bytes the resident cells take against the cap (pinned ones are counted and never evicted).
gate(): string[]The cells that must be in before the level is playable: those within loadRadius of the focus.
ready(): Promise<void>Resolves when every cell of the gate is loaded (or has failed: a missing far cell must not hold the player).
update(_dt = 0): voidCalled once per rendered frame.
async loadBundle(id: string): Promise<void>Load a bundle (the files an interior needs) now; resolves when it is in. Loading it twice is one load.
bundleState(id: string): State | undefined
bundleRoots(id: string): BABYLON.TransformNode[]
hasCell(id: string): boolean
cellRoot(id: string): BABYLON.TransformNode | nullthe root of a resident cell's instance (what an entity that needs the cell looks its node up in), else null
cellState(id: string): State | undefined
snapshot(): { cells: Record<string, State>; bundles: Record<string, State> }
dispose(): void

packages/levels/src/streaming.ts:54

TransitionRegistry class

class TransitionRegistry
register(name: string, t: Transition | (() => Transition)): thisRegister a transition by name: one object, or a function that makes a fresh one per travel. Registering default sets what is used when nothing else is chosen.
has(name: string): boolean
names(): string[]
choose(fn: TransitionChooser): this(from, to, plan) => name: decides per travel (a theme in to.tags, a short wait in plan.cached).
pick(from: LevelDef | null, to: LevelDef, plan: LevelPlan, named?: string): stringThe name to use: the one the call named, else the chooser's, else default, else the bar. A name that is not registered falls back to the bar.
create(name: string): Transition

packages/levels/src/transitions.ts:54

TransitionRun class

class TransitionRun

A transition with the guarantees of 5.1 around it: enter has 2 s to be on screen; a throw anywhere is logged and the bar takes over; a broken transition never blocks the travel. After a fallback every further call is a no-op and name reads bar.

name: string
fellBack: string | nullset when the game's transition failed and the engine's bar replaced it
constructor(readonly wanted: string, private readonly t: Transition, private readonly ctx: TransitionContext, private readonly o: RunOptions = {})
async enter(): Promise<void>
update(p: TransitionProgress): void
async ready(): Promise<void>Resolves when the game lets the player in. A cancel does not hold the player in a transition that waits for a button.
async fail(err: StreamError): Promise<boolean>True when the transition showed the failure itself.
dispose(): void

packages/levels/src/transitions.ts:90

TravelAborted class

class TravelAborted extends Error

The player (or the menu) cancelled the travel.

constructor(readonly id: string)

packages/levels/src/levels.ts:60

Functions

applyEnvironment function

function applyEnvironment(scene: BABYLON.Scene, def: EnvironmentDef, name = 'env', o: { /** the game's shared clock in seconds (the world's time): waves are drawn from it, so every peer sees the same sea at the same moment */ clock?: () => number } = {}): Environment

Build the environment in a scene. Dispose it when the level unloads.

packages/levels/src/environment.ts:203

applyLandscape function

async function applyLandscape(scene: BABYLON.Scene, def: LandscapeDef, o: ApplyLandscapeOptions): Promise<LandscapeView>

Put a landscape's layer blend on its mesh's material, mark a far plane, and bake the virtual texture and hand it to the props. A problem (a texture that does not load, a mesh that is not there) is a warning in the view, never a thrown error: the level still loads with the baked look.

packages/levels/src/landscape.ts:349

atmosphereColours function

function atmosphereColours(t: AtmosphereTable, sunScale = 1, sunColor: Vec3 = [1, 1, 1]): { top: Vec3; horizon: Vec3; ground: Vec3 }

colours of the sky that the rest of the environment uses (water, reflection, fog): the zenith, the horizon away from the sun, the ground

packages/levels/src/atmosphere.ts:189

blendWeights function

function blendWeights(weights: number[], heights: number[] = [], heightBlend: number[] = []): number[]

Weights of the layers at a point -> the weights the material uses: normalised to sum 1 (no paint at all = the first layer), then height blended.

packages/levels/src/landscape.ts:107

bloomWeight function

function bloomWeight(intensity: number): number

packages/levels/src/environment.ts:104

buildAtmosphereTable function

function buildAtmosphereTable(def: AtmosphereDef | undefined, sunElevation: number, n = LUT_SIZE): AtmosphereTable

Fill the table for the sun at sunElevation radians above the observer's horizon.

packages/levels/src/atmosphere.ts:117

buildWater function

function buildWater(scene: BABYLON.Scene, def: WaterDef, o: { name: string; skyTop: Vec3; skyHorizon: Vec3; sunDir: Vec3; sunColor: Vec3; /** the game's shared clock in seconds: with it every peer draws the same waves at the same moment (default: this surface's own time since it was built) */ clock?: () => number }): WaterSurfaceView

Build one water surface in a scene. sky and sun come from the rest of the environment.

packages/levels/src/water.ts:145

cellDistance function

function cellDistance(b: StreamCellDef['bounds'], x: number, z: number): number

Distance on the ground plane from a point to a cell's box (0 inside it).

packages/levels/src/streaming.ts:49

deepColour function

function deepColour(absorption: Vec3 = [110, 158, 350], scattering: Vec3 = [0.05, 0.05, 0.05]): Vec3

The deep colour of the water from Unreal's absorption distances and scattering (linear RGB).

packages/levels/src/water.ts:62

defaultLibraryUrl function

function defaultLibraryUrl(): string

packages/levels/src/library.ts:94

distanceToBounds function

function distanceToBounds(b: CollisionChunkDef['bounds'], x: number, y: number, z: number): number

Distance from a point to a box (0 inside).

packages/levels/src/collision-chunks.ts:13

effectiveFogDensity function

function effectiveFogDensity(f: HeightFogDef, h: number): number

the flat density Babylon's exponential fog takes for a view at height h: the density of the air there (a ray along the ground is what a walker sees most)

packages/levels/src/height-fog.ts:39

environmentBabylonParts function

function environmentBabylonParts(env?: EnvironmentDef): string[]

The Babylon parts a level's environment needs beyond the core: the post-processing pipeline for bloom, particles for fire.

packages/levels/src/environment.ts:97

flickerFactor function

function flickerFactor(l: Pick<BudgetLight, 'flicker'>, phase: number, t: number): number

the multiplier of a light's intensity at time t seconds

packages/levels/src/light-budget.ts:104

flickerValue function

function flickerValue(phase: number, t: number): number

a smooth repeatable noise in 0..1: two sines of unrelated rate, per light phase (no random state, so tests and replays agree)

packages/levels/src/light-budget.ts:99

fogDensityAt function

function fogDensityAt(f: HeightFogDef, h: number): number

extinction per metre at height h

packages/levels/src/height-fog.ts:21

fogOpticalDepth function

function fogOpticalDepth(f: HeightFogDef, h0: number, dirY: number, dist: number): number

Optical depth of a straight ray from a point at height h0 going dist metres along a direction with vertical component dirY (-1..1): the integral of the density, exact for each layer. The transmittance is exp(-depth).

packages/levels/src/height-fog.ts:29

gradientFaces function

function gradientFaces(top: Vec3, horizon: Vec3, ground: Vec3, n = 8): Uint8Array[]

A cube map of the sky's gradient (ground below the horizon, top above): every face a few texels, smooth at every mip level, so any roughness reads it.

packages/levels/src/environment.ts:154

heightRaster function

function heightRaster(positions: ArrayLike<number>, rect: [number, number, number, number], w: number, h: number): Float32Array

Spread vertices (x, y, z; z is the plane's second axis) over a w x h raster of the rectangle: the height of the nearest vertex per texel, holes filled.

packages/levels/src/landscape.ts:127

installCatalogCommands function

function installCatalogCommands(world: import('@ferret/core').World, levels: LevelService, client: CatalogClient): void

The catalog commands: the viewer and an agent open a catalog, list its levels with their state on the device, refresh it and travel into a level. catalog.levels {check: true} opens each level's package (a few ranged reads) to say whether its core set is cached or missing and how many bytes a visit would download; without check the state is unknown.

packages/levels/src/catalog.ts:161

installCatalogs function

function installCatalogs(levels: LevelService, mount: PackageMount, store: AssetStore, defaults: CatalogOptions = {}): CatalogClient

Plug catalog support into a LevelService: afterwards levels.registerCatalog(url) works.

packages/levels/src/catalog.ts:150

installCode function

function installCode(world: World, mount: PackageMount, o: CodeLoaderOptions = {}): CodeLoader

Let a mount run the code of its packages.

packages/levels/src/code.ts:171

installCodeCommands function

function installCodeCommands(world: World, code: CodeLoader): void

pkg.require and pkg.loaded: run the code of an open package (and of its dependencies) through a CodeLoader, and list what runs.

packages/levels/src/commands.ts:120

installLevelLibraries function

function installLevelLibraries(levels: LevelService, code: CodeLoader): void

Make travel require the libraries of every package level before it is built. The ones the manifest names are found through the loader's open hook (the catalog) when they are not open yet; the ones only the open libraries' provides imply are already open. Emits level:libraries { level, libraries } with what was required.

packages/levels/src/libraries.ts:62

installPackageCommands function

function installPackageCommands(world: World, mount: PackageMount, hooks: PackageCommandHooks = {}): void

The pkg.* commands (PLAN.md 4.9), by hand and through MCP alike, over a PackageMount: pkg.open, pkg.unlock, pkg.level, pkg.list, pkg.info, pkg.validate, pkg.deps, pkg.verify, pkg.code, pkg.pack, pkg.save, pkg.keys, pkg.catalog and pkg.publish. installCodeCommands adds pkg.require and pkg.loaded (running a package's code).

packages/levels/src/commands.ts:32

installPackageLoader function

function installPackageLoader(levels: LevelService, o: PackageLoaderOptions): void

Teach a LevelService to load level packages: a folder with manifest.json, scene.glb and optionally navmesh.bin. Problems with the wiring (a node the manifest names is missing, a behaviour is not registered) do not stop the level: they are collected in the report so the game still runs and the artist sees what to fix.

packages/levels/src/package.ts:37

installPackages function

function installPackages(levels: LevelService, assets: MountAssets, o: MountOptions = {}): PackageMount

Set a LevelService up to play packages.

packages/levels/src/ferret.ts:154

installStreamCommands function

function installStreamCommands(world: World, store: AssetStore): void

stream.status and stream.cache: what the store is doing and what it holds on the device.

packages/levels/src/runtime.ts:204

installStreamingDebug function

function installStreamingDebug(world: World, sv: StreamingService): StreamingDebugView

stream.debug {on?}: show or hide the cell boxes of the streamed level; returns whether they are shown.

packages/levels/src/streaming-debug.ts:56

installTravelCommands function

function installTravelCommands(world: World, registry: TransitionRegistry): void

The travel.* commands: travel.preview {name, seconds?, bytes?, fail?, cached?} and travel.transitions.

packages/levels/src/transitions.ts:195

layerBlendGlsl function

function layerBlendGlsl(layers: LandscapeLayerDef[], pos: string, tex = 'texture', linear = 'toLinearSpace'): string

The layer blend as GLSL statements. Needs in scope: vec2 <pos> (metres in the landscape's coordinates), uniform vec4 ferretLsRect (minX, minZ, 1/sizeX, 1/sizeZ), samplers ferretLsW<i>, ferretLsA<k>, ferretLsN<k>. Leaves vec3 lsColour (linear) and, with normal maps, vec3 lsNormal (tangent space) set. tex is the sampling function (texture in GLSL ES 3, texture2D in the procedural texture's shader), linear converts sRGB to linear.

packages/levels/src/landscape.ts:154

levelLibraries function

function levelLibraries(manifest: LevelManifest, providers: Iterable<Provider>): Record<string, string>

Which libraries a level needs: those its manifest names, and every one of providers whose provides the level uses (a level key with content, a capability it switches on, an actuator or rotor kind, a flow source). Id -> version range, sorted by id.

packages/levels/src/libraries.ts:41

lutElevation function

function lutElevation(i: number, n = LUT_SIZE): number

the view elevation (radians above the horizon) of table entry i: dense near the horizon, where the sky changes fastest

packages/levels/src/atmosphere.ts:79

miePhase function

function miePhase(c: number, g: number): number

packages/levels/src/atmosphere.ts:82

pickTier function

function pickTier<T>(have: Partial<Record<LibraryTier, T>>, want: LibraryTier = 'desktop'): { tier: LibraryTier; value: T } | null

The first tier at or below want that the item has (a low-end device asks for low and gets low, never desktop).

packages/levels/src/library.ts:88

planCollisionChunks function

function planCollisionChunks(def: CollisionChunksDef, at: [number, number, number][], resident: ReadonlySet<number>): { load: number[]; drop: number[] }

Which chunks to load and which to drop for the places in at (metres), given the ones resident. A chunk stays while it is within unloadRadius of any place; it comes when within loadRadius.

packages/levels/src/collision-chunks.ts:19

previewTransition function

async function previewTransition(world: World, registry: TransitionRegistry, name: string, o: PreviewOptions = {}): Promise<{ name: string; fellBack: string | null; updates: number; ended: 'ready' | 'failed' }>

Run a registered transition against a fake travel, without travelling: the author sees it in the viewer and tests it in Playwright. Returns what the transition was told.

packages/levels/src/transitions.ts:162

rankNearest function

function rankNearest(items: readonly { position: Vec3; priority?: number }[], at: Vec3, n: number, opts: { held?: ReadonlySet<number>; hysteresis?: number; maxDistance?: number; filter?: (i: number) => boolean } = {}): number[]

The indices of the n lights that matter most to a viewer at at: the smallest distance / priority, within maxDistance. Lights in held (the ones that are real now) count as nearer by hysteresis, so a newcomer has to be clearly nearer to take over.

packages/levels/src/light-budget.ts:85

rayleighPhase function

function rayleighPhase(c: number): number

packages/levels/src/atmosphere.ts:81

refKind function

function refKind(ref: string): RefKind

Which scheme a reference uses.

packages/levels/src/runtime.ts:16

resolveAtmosphere function

function resolveAtmosphere(a: AtmosphereDef = {}): ResolvedAtmosphere

packages/levels/src/atmosphere.ts:59

skyRadiance function

function skyRadiance(t: AtmosphereTable, elevation: number, cosSun: number, sunScale = 1, sunColor: Vec3 = [1, 1, 1]): Vec3

Radiance of the sky in a direction, in the picture's units for a sun of unit colour and strength: elevation of the view above the horizon, cosSun the cosine of the angle between the view and the sun. The dome's shader does the same on the GPU.

packages/levels/src/atmosphere.ts:174

spawnFromManifest function

function spawnFromManifest(s: ManifestSpawn): { id: string; position: [number, number, number]; yaw?: number }

packages/levels/src/levels.ts:429

validateAtmosphere function

function validateAtmosphere(a: unknown, at = 'environment.skyAtmosphere'): { path: string; message: string }[]

packages/levels/src/atmosphere.ts:63

validateEnvironment function

function validateEnvironment(e: unknown, at = 'environment'): ManifestIssue[]

packages/levels/src/environment.ts:65

validateLandscapes function

function validateLandscapes(l: unknown, at: string): { path: string; message: string }[]

packages/levels/src/landscape.ts:67

validateLightBudget function

function validateLightBudget(d: unknown, at = 'environment.lights'): ManifestIssue[]

packages/levels/src/light-budget.ts:57

validateManifest function

function validateManifest(m: unknown): ManifestIssue[]

Check a manifest against the schema. Returns the problems; an empty list means the manifest is usable.

packages/levels/src/manifest.ts:111

validatePackage function

async function validatePackage(pkg: FerretPackage, o: ValidateOptions = {}): Promise<PackageIssue[]>

pkg.validate: the package's own checks (manifest, files, sha256, no code in kinds without code) and then every level: its manifest against the schema, and that the scene, collision mesh and navmesh it names exist — in the package or, through ferret://, in a declared dependency.

packages/levels/src/ferret.ts:185

validateProbes function

function validateProbes(p: unknown, at = 'environment.probes'): { path: string; message: string }[]

packages/levels/src/probe-volume.ts:23

validateWater function

function validateWater(w: unknown, at: string): { path: string; message: string }[]

packages/levels/src/water.ts:46

virtualTextureShare function

function virtualTextureShare(above: number, blendHeight: number, strength = 1): number

The colour a prop takes at height above metres over the ground: the share of the ground colour (virtual texture) it keeps.

packages/levels/src/landscape.ts:121

waveHeight function

function waveHeight(waves: WaterWave[], x: number, z: number, t: number): number

Height of the waves at (x, z) and time t: the sum of Gerstner waves' vertical part, metres above the level.

packages/levels/src/water.ts:69

Interfaces

ApplyLandscapeOptions interface

interface ApplyLandscapeOptions
at: (file: string) => stringturn a file name of the def into a URL
texture?: (url: string, scene: BABYLON.Scene, o: { srgb: boolean; repeat: boolean }) => BABYLON.BaseTexture | Promise<BABYLON.BaseTexture>create a texture from a URL (default: a Babylon Texture, resolved once loaded); tests pass their own
scope?: BABYLON.AbstractMesh[]the name pattern of the meshes of the virtual texture's props is searched among these (default: all meshes of the scene)

packages/levels/src/landscape.ts:326

AssetLibraryOptions interface

interface AssetLibraryOptions
store: AssetStore
mount: PackageMount
assets?: RuntimeAssetsneeded for characters
tier?: LibraryTierthe package tier to open (default desktop); a tier a package does not have falls back to its plain file
policy?: CatalogPolicythe catalog policy (trusted keys, allowUnsigned)

packages/levels/src/library.ts:23

AtmosphereDef interface

interface AtmosphereDef

Unreal's SkyAtmosphere component. Every field is optional and defaults to the component's own default. Lengths km, coefficients per km.

groundRadius?: numberplanet radius, km (6360)
atmosphereHeight?: numberthickness of the air, km (60)
rayleighScattering?: Vec3Rayleigh scattering per km (0.005802, 0.013558, 0.0331)
rayleighScaleHeight?: numberRayleigh scale height, km (8)
mieScattering?: Vec3Mie scattering per km (0.003996 each)
mieAbsorption?: Vec3Mie absorption per km (0.00444 each)
mieScaleHeight?: numberMie scale height, km (1.2)
mieAnisotropy?: numberMie anisotropy g, -1..1 (0.8)
absorption?: Vec3ozone absorption per km at the middle of its layer (0.00065, 0.001881, 0.000085)
groundAlbedo?: Vec3colour of the ground the air sits on (0.402 each)
multiScatteringFactor?: numberhow much of the single scattering comes back as multiple scattering (1)
skyLuminanceFactor?: Vec3colour factor on the sky's luminance (1 each)
observerAltitude?: numberhow far the observer is above the ground, km (0)

packages/levels/src/atmosphere.ts:17

AtmosphereTable interface

interface AtmosphereTable
n: number
rayleigh: Vec3[]Rayleigh in-scatter per view elevation, phase left out, per channel, in units of the sun's irradiance
mie: Vec3[]the same for Mie
sunTransmittance: Vec3share of the sun's light that reaches the observer: the sun disc's and the lit ground's colour (zero with the sun under the horizon)
ground: Vec3colour of the ground below the horizon, in the same units (albedo x sun and sky light)
g: number
skyLuminanceFactor: Vec3
multiScatter: number
sunElevation: number

packages/levels/src/atmosphere.ts:87

AttachOptions interface

interface AttachOptions
socket?: stringa socket of the body's item (grip_r, axe_r...)
bone?: stringa bone by name, with position and rotation of your own (they win over the socket's)
position?: [number, number, number]
rotation?: [number, number, number, number]
compatibleClips?: booleanalso bind the clip sets of other skeletons that fit the part by bone names, and the other sets of its own skeleton (as for CharacterOptions): a rider's riding clips

packages/levels/src/library.ts:56

BindOptions interface

interface BindOptions
map?: Record<string, string>graph clip name -> clip name in the clip sets (default: every clip under its own name)
oneShot?: string[]graph clip names that do not loop

packages/levels/src/library.ts:76

BudgetLight interface

interface BudgetLight

A level with hundreds of lamps, candles, torches and fires (Unreal draws them all as movable lights) cannot give each a real light: every real light is paid for in every pixel of every lit surface. The light budget keeps a small pool of real lights and moves it to the lights nearest the camera, from time to time; every other lamp is a glowing dot (an emissive instance of one shared mesh: one draw call for all). The pool never changes size while it runs, so no shader is rebuilt when a light changes hands. A light that changes hands fades out and in instead of popping. This is a performance limit, not a design: Unreal lights all of them.

id?: stringa name for events and tests (the source actor's)
group?: stringa group of lamps of one kind ('torch', 'candle'), for statistics and the sound budget
position: Vec3the level's metres
color: Vec3linear colour 0..1
intensity: numberBabylon intensity at 1 m (already converted from the source engine's units, see tools/ue-export/lights.mjs)
range: numberthe metres where the light reaches zero (the source engine's attenuation radius)
kind?: 'point' | 'spot'
direction?: Vec3a spot's axis (the way the light travels) and full cone angle in radians
angle?: number
flicker?: { min: number; max: number; rate?: number }a flame's flicker: the intensity moves between min and max times its value, rate times a second
priority?: numbernearer than a light of priority 1 by this factor when ranking (a chandelier over a candle); default 1
size?: numberthe size of the glow dot in metres (default from the range)
sound?: stringa looping sound this lamp makes (a torch's crackle); it is budgeted by sounds, the caller plays it

packages/levels/src/light-budget.ts:12

CatalogOptions interface

interface CatalogOptions extends CatalogPolicy
tier?: stringdevice tier: packages with a tiers[tier] file use it
prefix?: stringput in front of every level id of this catalog (two catalogs may share ids)

packages/levels/src/catalog.ts:11

CatalogRegistration interface

interface CatalogRegistration
readonly url: string
readonly catalog: Catalog
readonly stale: booleanthe catalog came from the copy kept by an earlier session (no network)
readonly unsigned: booleandevelopment only: the catalog was not signed
readonly levels: string[]ids registered in the LevelService
refresh(): Promise<{ changed: boolean; stale: boolean }>Ask the server again. changed when a new version replaced the registered levels; a failure leaves everything as it was.

packages/levels/src/catalog.ts:18

CellSource interface

interface CellSource

What the service needs from the asset layer (an AssetCache fits): fetch and parse a file, then instantiate it.

load(url: string): Promise<void>
instantiate(url: string, name?: string): Promise<{ root: BABYLON.TransformNode; dispose(): void }>
release(url: string): void

packages/levels/src/streaming.ts:17

CharacterOptions interface

interface CharacterOptions extends Omit<ModelOptions, 'pkg'>
compatibleClips?: booleanalso bind the clip sets of other skeletons that list this body's skeleton as compatible, and the other sets of its own skeleton (as <set id>/<clip>)
tier?: LibraryTier
yaw?: number

packages/levels/src/library.ts:44

ClipRef interface

interface ClipRef

A clip set a character can use: where its file is, and the clips in it. Bind them with bindClips of @ferret/animation.

id: string
ref: string
skeleton: string
clips: ClipsItem['clips']
prefix?: stringset for a clip set that only fits by bone names (compatible): its clips are bound as <prefix><clip>, so they never replace the body's own

packages/levels/src/library.ts:38

CodeLoaderOptions interface

interface CodeLoaderOptions
origin?: (pkg: FerretPackage) => booleandoes this package come from where the game does (shipped with it, its own site)? Then its code is the game's (CodePolicy.origin)
trusted?: () => Iterable<string> | undefinedkey ids whose signatures are trusted; without them any valid signature is enough
ferret?: unknownthe engine's API handed to the code (default globalThis.Ferret, the shared engine)
esm?: booleanthe engine is ES modules: the code imports @ferret/* itself, so no engine API object is needed and ctx.ferret may be undefined
extendContext?: (ctx: LibraryContext, pkg: FerretPackage) => voidadd to the context handed to a package's install (the player adds game, levels, root, args, tier)
open?: (id: string, range: string) => Promise<FerretPackage | null>a package that is not open yet (a dependency, a require by id): open it, e.g. through the catalog; null when there is none
importModule?: (bytes: Uint8Array, path: string, pkg: FerretPackage) => Promise<LibraryModule>import the module from its checked bytes (default: a blob URL and import(); tests use a data URL)

packages/levels/src/code.ts:47

CollisionChunkDef interface

interface CollisionChunkDef
file: string
bounds: [number, number, number, number, number, number]

packages/levels/src/collision-chunks.ts:9

CollisionChunksDef interface

interface CollisionChunksDef
loadRadius: number
unloadRadius: number
chunks: CollisionChunkDef[]

packages/levels/src/collision-chunks.ts:10

Environment interface

interface Environment
post?: BABYLON.DefaultRenderingPipeline
sun?: BABYLON.DirectionalLight
ambient?: BABYLON.HemisphericLight
sky?: BABYLON.Mesh
particles: BABYLON.ParticleSystem[]
water: WaterSurfaceView[]
lights?: LightBudget
warnings: string[]effects that could not be made (an unknown kind)
dispose(): void

packages/levels/src/environment.ts:106

EnvironmentDef interface

interface EnvironmentDef

The look of a level beyond its meshes: sun, ambient light, fog, sky and fixed effects. It is the manifest's environment; tools/ue-export writes it from an Unreal level (sun, sky light, height fog, particle emitters). Every part is optional.

sun?: { direction: Vec3; color?: Vec3; intensity?: number }the directional light; direction is where the light travels. intensity is in the source engine's units (Unreal lux-like): 4 is about 1 here
ambient?: { color?: Vec3; intensity?: number; direction?: Vec3; ground?: Vec3 }ambient light from the sky: a hemispheric light, a bit darker below. direction is the way its bright side faces (default up: floors are lit most); ground is the colour of the dark side (default color x 0.45). An interior lit from its ceiling faces it down: [0, -1, 0].
fog?: { density?: number; color?: Vec3; heightFalloff?: number; height?: number; maxOpacity?: number; start?: number; cutoff?: number; second?: { density: number; heightFalloff: number; heightOffset?: number } }exponential fog; density in the source engine's units (per metre, about 10 times smaller here). With heightFalloff it is Unreal's ExponentialHeightFog: the density halves every 1 / (falloff x 0.1) metres above height (the fog actor's height, metres) and grows below it; the view's density follows the camera's height every frame. second is the component's second fog layer. Not done: maxOpacity, start and cutoff (Babylon's exponential fog has no such terms; they are carried in the manifest for a tool that wants them).
skyAtmosphere?: AtmosphereDefUnreal's SkyAtmosphere: the sky dome is the scattering of the sun's light in the planet's air (see atmosphere.ts) instead of the colour gradient. {} is the component's defaults. The sun gives the direction and colour; top / horizon / ground of sky still override the colours the water, the reflection and the fog take from it. The level's BP_Sky_Sphere actor adds nothing: the engine's sky sphere only recolours itself when a level asks it to, and the dome replaces it (postprocess drops the actor).
sky?: { top?: Vec3; horizon?: Vec3; ground?: Vec3 }the sky dome: a gradient from horizon to top with the sun's glow; colours default from the fog colour
reflection?: { intensity?: number; top?: Vec3; horizon?: Vec3; ground?: Vec3 }what metal and gloss reflect: a small cube map filled with a gradient (top / horizon / ground of its own, else the sky's), as the scene's environment texture. Without it a metal surface has nothing to reflect and draws near black (an interior has no sky for it). intensity scales both the reflection and the light the map adds to every surface (default 1; interiors want 0.3-0.6).
post?: { bloom?: number; threshold?: number }post-processing of the picture: bloom is the source engine's bloom intensity (Unreal's post process volume: 0.675 default, a glowing interior 3-4); bright parts (lamps, windows) glow around. A tier for weak GPUs leaves it out. threshold is the brightness that starts to glow (default 0.95).
probes?: ProbeVolumeDefbaked global illumination (tools/ue-export/bake-gi): when present it lights every surface instead of the flat ambient light
effects?: { name?: string; kind?: 'fire'; template?: string; position: Vec3; scale?: number }[]fixed effects placed in the level
water?: WaterDef | WaterDef[]open water: an ocean or a lake (see water.ts); swimming in it is the physics medium of the same surface
landscapes?: LandscapeDef[]layered landscapes (near ground, far backdrop) and the virtual texture blend of the props on them (see landscape.ts); the package loader applies them
lights?: LightBudgetDefhundreds of lamps and flames under a budget: the nearest few are real lights, the rest glow (see light-budget.ts)

packages/levels/src/environment.ts:16

Focus interface

interface Focus

A place the player (or a camera) is, with the way it faces: cells ahead are fetched first.

position: BABYLON.Vector3
forward?: BABYLON.Vector3

packages/levels/src/streaming.ts:24

HeightFogDef interface

interface HeightFogDef
density: number
heightFalloff: number
height: numberheight of the fog actor, metres
second?: HeightFogLayer

packages/levels/src/height-fog.ts:9

HeightFogLayer interface

interface HeightFogLayer
density: number
heightFalloff: number
heightOffset?: number

packages/levels/src/height-fog.ts:7

LandscapeDef interface

interface LandscapeDef
name: string
mesh?: stringregex source for the landscape's mesh names (default ^<name>$)
rect: [number, number, number, number][minX, minZ, maxX, maxZ], metres, in the mesh's own coordinates; the weightmaps and the virtual texture cover it
layers: LandscapeLayerDef[]
weights: string[]weightmap images over rect: file i holds the weights of layers 4i..4i+3 in R, G, B, A
far?: booleanthe far backdrop: no collision, never picked, always drawn; layers and weights are optional for it
virtualTexture?: VirtualTextureDef

packages/levels/src/landscape.ts:45

LandscapeLayerDef interface

interface LandscapeLayerDef
name: string
albedo: stringcolour image (sRGB); with heightBlend its alpha channel is the layer's height map
normal?: stringoptional tangent-space normal image
tile: numbermetres of one repeat of the layer's textures (Unreal: 100 / the material's UV scale)
tint?: Vec3multiplies the albedo (linear)
heightBlend?: number0 = plain weight blend; above 0 = Unreal's height blend, the layer's height (albedo alpha) times this decides who wins where weights overlap

packages/levels/src/landscape.ts:20

LandscapeTextures interface

interface LandscapeTextures
weights: BABYLON.BaseTexture[]
albedo: BABYLON.BaseTexture[]
normal: (BABYLON.BaseTexture | undefined)[]

packages/levels/src/landscape.ts:192

LandscapeView interface

interface LandscapeView
def: LandscapeDef
meshes: BABYLON.AbstractMesh[]
virtualTexture?: BABYLON.BaseTexturethe baked virtual texture (colour in rgb, ground height in a), when the landscape has one
requiredMaxZ?: numbercamera depth (maxZ) a far landscape needs to be seen whole: the rectangle's diagonal plus the camera's height over it, rounded up
warnings: string[]
dispose(): void

packages/levels/src/landscape.ts:315

LevelContext interface

interface LevelContext

What a level's builder gets. Everything it makes (nodes, lights, materials, entities) is removed again when the level goes.

world: World
scene: BABYLON.Scene
level: LevelDef
addSpawn(s: { id: string; position: BABYLON.Vector3 | [number, number, number]; yaw?: number }): voiddeclare where the player may start
progress(p: number): voidreport progress 0..1 to the loading screen

packages/levels/src/levels.ts:10

LevelDef interface

interface LevelDef

One entry of the level registry (the equivalent of a row of UE's DT_LevelInfo).

id: string
name?: string
url?: stringa level package folder: scene.glb, manifest.json, navmesh.bin (see ./package)
build?(ctx: LevelContext): void | Promise<void>or code that builds the level: procedural levels, greyboxes, tests
spawn?: stringthe spawn used when the level is entered without naming one (default: the first declared)
data?: Record<string, unknown>anything the game wants to keep with the level: music, weather, display name
tags?: string[]free tags (a theme, a chapter): handed to the game's choices, never read by the engine
prepare?(progress: (p: number) => void, signal?: AbortSignal): Promise<void>Get what the level needs ready before the old level is touched (a remote level opens its package here). If it throws the travel fails and the player stays in the current level. progress takes 0..1 of this step.
plan?(): Promise<LevelPlan>What fetching the level will download, known after prepare: bytes and files not held yet, and whether everything is already on the device.
fetch?(ctx: { signal: AbortSignal; progress(loaded: number, total: number, files: number): void; /** files read at once (default: the level's own choice; a background prefetch asks for 1) */ concurrency?: number }): Promise<void>Step 4 of a travel: bring the core set of the level to the device. progress takes bytes. A failure or a cancel leaves the old level in place.

packages/levels/src/levels.ts:21

LevelManifest interface

interface LevelManifest
version: number
name?: string
scene?: stringfile names inside the package folder (defaults: scene.glb, navmesh.bin)
navmesh?: string
navBuild?: { agentRadius?: number; agentHeight?: number; maxClimb?: number; maxSlope?: number; cellSize?: number; bounds?: NavBoxDef | NavBoxDef[]; exclude?: (NavBoxDef & { yaw?: number })[] }how to build the NavMesh from the level's solid meshes: used when navmesh.bin is missing (or being rebuilt by the editor)
collision?: stringa mesh of simple collision shapes (invisible, solid). When present, the visible meshes of the scene do not collide
collisionChunks?: CollisionChunksDefthe solid shapes outside the play area, cut into chunks that are fetched as the player nears them (see CollisionChunksDef); written by tools/ue-export/collision.mjs. Needs collision
movement?: Partial<Record<(typeof MOVEMENT_KEYS)[number], number>>the values of the level's player character (metres, seconds; see CharacterParams in
animGraphs?: Record<string, unknown>animation graphs by name (see GraphDef in
entities?: ManifestEntity[]
spawns?: ManifestSpawn[]
zones?: unknown[]
physics?: Record<string, unknown>realistic physics for this level (see PhysicsConfig in
libraries?: Record<string, string>the library packages the level needs, id -> version range: written by the editor from what the open libraries provide; travel requires them before the level is built
colliders?: { node: string; collide: boolean }[]collision overrides by glTF node name: make a node (and its meshes) solid or walk-through whatever the export says
defaultSpawn?: stringthe spawn used when the level is entered without naming one
environment?: EnvironmentDefsun, ambient light, fog, sky and fixed effects (see EnvironmentDef)
tags?: string[]free tags and data for the game (a theme, a chapter, a mood): handed to the game's choices (LevelDef.tags / data), never read by the engine
data?: Record<string, unknown>
streaming?: StreamingBlockstreaming inside the level: scene is then the core (what is visible and solid at the spawn) and the rest arrives by cell

packages/levels/src/manifest.ts:67

LevelOptions interface

interface LevelOptions

Minimum time the loading screen stays up, so it does not flash (ms; 0 in tests).

minLoadingMs?: number
slowMs?: numberno new bytes for this long while the target downloads: travel:slow {slow: true} (default 5000)
holdMs?: numberthe longest the level waits, after it is built, for what its entities asked to be waited for (hold) before it is announced ready anyway (default 20000; what was missing goes to the report's warnings)
keepOldUntilReady?: booleantrue on devices with room: a failed travel rebuilds the old level and returns the player to the spot; false (console): to the hub
transitionEnterMs?: numberhow long a transition has to get on screen before the engine's bar replaces it (default 2000)

packages/levels/src/levels.ts:80

LevelPlan interface

interface LevelPlan
bytes: number
files: number
cached: boolean

packages/levels/src/levels.ts:45

LevelReport interface

interface LevelReport
warnings: string[]things the manifest asked for that the level could not provide: missing nodes, unknown behaviours ...

packages/levels/src/levels.ts:74

LibraryCharacter interface

interface LibraryCharacter
readonly id: string
readonly item: CharacterItem
readonly model: ModelHandle
readonly clips: ClipRef[]the clip sets of the character, resolved from the catalog
dispose(): void

packages/levels/src/library.ts:67

LibraryContext interface

interface LibraryContext

What install gets.

world: World
ferret: anythe engine's API (every package's exports in one namespace)
pkg: { id: string; version: string; manifest: FerretPackage['manifest'] }this package
mount: PackageMount
url(path: string): Promise<string>a blob URL for a file of this package, or ferret://dep/path of a declared dependency
require(id: string): unknownthe exports of a declared dependency's code (already loaded before this one)

packages/levels/src/code.ts:18

LibraryEntry interface

interface LibraryEntry extends CatalogAsset
id: string

packages/levels/src/library.ts:35

LibraryModel interface

interface LibraryModel

A static library model on its way (a tree, a rock): model is the handle, item its file (bounds, collision).

readonly id: string
readonly item: ModelItem
readonly model: ModelHandle
dispose(): void

packages/levels/src/library.ts:54

LibraryModule interface

interface LibraryModule

What a package's code module exports. install may return { dispose } or a promise of it.

install?(ctx: LibraryContext): unknown
[name: string]: unknown

packages/levels/src/code.ts:32

LibraryQuery interface

interface LibraryQuery
type?: LibraryItem['type']
tags?: string[]
text?: string

packages/levels/src/library.ts:34

LightBudgetDef interface

interface LightBudgetDef
budget: numberhow many real lights at most. A quality tier sets it in its own manifest (a weak GPU 2-3, a strong one 8-12)
items: BudgetLight[]
interval?: numberseconds between two re-rankings (default 0.25)
fade?: numberseconds a light takes to fade out of / into a real light (default 0.35)
hysteresis?: numbera new light must be nearer than the one it replaces by this share to take its place (default 0.15): no flapping between two equal lamps
maxDistance?: numberlights farther from the camera than this are nothing at all (default 200 m)
glow?: { enabled?: boolean; /** colour multiplier (above 1 feeds bloom), default 2 */ boost?: number; /** metres, default from each light's range */ size?: number }the dot for lamps that are not real lights
sounds?: { budget: number; maxDistance?: number }the nearest lamps that have a sound: events tell which to start and stop

packages/levels/src/light-budget.ts:39

LightBudgetOptions interface

interface LightBudgetOptions
name?: string
viewer?: () => Vec3 | undefinedwhere the viewer is, in the level's metres; default the active camera
materials?: booleanraise the materials' light limit to fit the pool plus the sun and ambient lights (default true)

packages/levels/src/light-budget.ts:119

LightBudgetStats interface

interface LightBudgetStats
items: number
budget: number
real: number
glowing: number
reranks: number
soundsOn: number

packages/levels/src/light-budget.ts:117

LoadedCode interface

interface LoadedCode
id: string
version: string
exports: LibraryModule | nullthe module's exports; null for a package without code (only its dependencies were loaded)
commands: string[]commands and behaviours that appeared while install ran
behaviours: string[]

packages/levels/src/code.ts:37

ManifestEntity interface

interface ManifestEntity
id: string
behaviour?: stringa behaviour or component registered in the world (world.components)
components?: { name: string; params?: Record<string, unknown> }[]several components, each with its own parameters, when one behaviour is not enough
node?: stringthe name of a glTF node to wrap; without it a new empty node is made
transform?: { position?: [number, number, number]; rotation?: [number, number, number]; scale?: [number, number, number] }local transform applied to the node (or the new node)
params?: Record<string, unknown>
tags?: string[]
needs?: stringthe id of a streaming bundle this entity depends on: it is created once the bundle is in, and a placeholder stands at its position until then. It may also be the id of a streaming cell: the entity wraps a node of that cell, is made whenever the cell is resident and destroyed when the cell is released (no placeholder)

packages/levels/src/manifest.ts:11

ManifestIssue interface

interface ManifestIssue
path: string
message: string

packages/levels/src/manifest.ts:105

ManifestSpawn interface

interface ManifestSpawn
id: string
position: [number, number, number]
yaw?: number

packages/levels/src/manifest.ts:65

MaterialOptions interface

interface MaterialOptions
variant?: string
name?: string
scene?: BABYLON.Scene

packages/levels/src/library.ts:83

ModelHandle interface

interface ModelHandle

A model on its way. node is the placeholder until the model is in, then the model's root.

readonly ref: string
readonly state: ModelState
readonly node: BABYLON.TransformNode | null
readonly ready: Promise<{ root: BABYLON.TransformNode; dispose(): void } | null>resolves with the instance, or null when it failed or was disposed (the reason is in error and the asset:failed event)
readonly error?: StreamError
dispose(): void

packages/levels/src/runtime.ts:36

ModelOptions interface

interface ModelOptions extends Pick<RequireOptions, 'priority' | 'signal' | 'sha256' | 'size' | 'pkg'>
placeholder?: PlaceholderSpecshown at once, replaced by the model (default: a small box); 'none' shows nothing until the model is in
parent?: BABYLON.TransformNodethe node the model hangs from (an entity's node): the placeholder and then the model are its children
position?: [number, number, number]
name?: string

packages/levels/src/runtime.ts:24

ModelPlacement interface

interface ModelPlacement extends Omit<ModelOptions, 'pkg'>
tier?: LibraryTier
yaw?: number

packages/levels/src/library.ts:51

MountAssets interface

interface MountAssets extends PackageAssets

What the mount needs from the asset cache (an AssetCache fits).

load(url: string, ext?: string): Promise<void>

packages/levels/src/ferret.ts:13

MountOptions interface

interface MountOptions
packages?: PackageSetthe packages that are open together (default: a new set); ferret://<id>/<path> is looked up in it
loader?: Partial<PackageLoaderOptions>passed on to the level loader (collisions, an editor's draft manifests ...)
fetch?: (url: string) => Promise<FetchResult>replace fetch for urls that are not in a package

packages/levels/src/ferret.ts:20

PackageAssets interface

interface PackageAssets

What the loader needs from @ferret/assets (an AssetCache fits).

load(url: string): Promise<void>
instantiate(url: string, name?: string): Promise<{ root: BABYLON.TransformNode; dispose(): void }>
release(url: string): void

packages/levels/src/package.ts:12

PackageCommandHooks interface

interface PackageCommandHooks
resolve?: (url: string) => PackageInput | Promise<PackageInput>turn a url or name into something FerretPackage.open reads (default: the url itself)
opened?: (pkg: FerretPackage, url: string) => voidtold when a package was opened from url (so a host can write changes back to where it came from)
write?: (pkg: FerretPackage, name: string, bytes: Uint8Array) => Promise<void>write a built .ferret (a file next to the source, a download); without it pkg.pack only reports what it built
publish?: (files: Record<string, Uint8Array>) => Promise<void>put the files of pkg.publish where the server serves them: { path: bytes }, paths relative to the site root (a folder to upload, a CDN)
persist?: (pkg: FerretPackage) => Promise<{ files?: number; bytes?: number; appended?: number; dead?: number; compacted?: boolean } | void>keep an edited package: a folder package writes its changed files, a zip is packed again. Called by \pkg.save\
compact?: (pkg: FerretPackage) => Promise<{ before: number; after: number }>write a project (.fprj) again without the bytes its saves left behind (compactZip): where it came from, or a download. Called by pkg.compact
behaviours?: () => Iterable<string>behaviours the game registers itself (for \pkg.validate\)
trusted?: () => Iterable<string> | undefinedkey ids the reader trusts (for \pkg.verify\ and the code check)

packages/levels/src/commands.ts:7

PackageLoaderOptions interface

interface PackageLoaderOptions
assets: PackageAssets
fetch?: (url: string) => Promise<{ ok: boolean; status: number; json(): Promise<any>; arrayBuffer(): Promise<ArrayBuffer> }>replace fetch (tests, a Platform file API)
collisions?: booleanmake the level's meshes collide (default true)
manifest?: (def: LevelDef) => LevelManifest | undefineda manifest to use instead of the one in the package: the editor's draft (return undefined to load the file)
streamingRadiusScale?: numberscale the streaming radii of every streamed level (a tier may ask for less)
forceNavBuild?: (def: LevelDef) => booleanbuild the NavMesh from navBuild even if navmesh.bin exists (the editor, after a rebuild)

packages/levels/src/package.ts:18

PrefetchOptions interface

interface PrefetchOptions
settleMs?: numberwait this long after a level is ready before the first background download (the level's own first seconds come first)
margin?: numbersafety factor on the estimate: start early enough to finish in eta * margin (default 1.5)
assumedRate?: numberassumed link speed in bytes/s until something has been measured (default: unknown, so hints start at once)
now?: () => number
setTimer?: (fn: () => void, ms: number) => unknown
clearTimer?: (t: unknown) => void

packages/levels/src/prefetch.ts:15

PreviewOptions interface

interface PreviewOptions
seconds?: numberhow long the fake download lasts
bytes?: number
files?: number
fail?: booleanend with a failure instead of readiness (to see the game's failure screen)
cached?: booleancached: plan says the wait is short
signal?: AbortSignal

packages/levels/src/transitions.ts:146

ProbeVolumeDef interface

interface ProbeVolumeDef

Baked global illumination: a regular grid of L1 spherical harmonics (the radiance arriving at each point, RGB), written by tools/ue-export/bake-gi. The shader of every PBR material looks up the grid trilinearly at the surface and adds the diffuse light the lamps, the sky and the bounces give there. It replaces the flat hemispheric ambient light (which the loader turns off when a volume exists).

file: stringthe baked grid, relative to the package: probes.bin
origin: [number, number, number]the position of the first probe, metres
spacing: numbermetres between probes
dims: [number, number, number]probes along x, y, z
gain?: numberscale of the baked light (default 1)
fill?: numberhow much of the level's flat ambient light stays on beside the baked light, 0..1 (default 0: none). A hemispheric fill that brightens ceilings and darkens floors where the bake's bounce light is short
down?: numberthe share of the baked light that faces turned down (ceilings) get, 0..1 (default 1: all of it)

packages/levels/src/probe-volume.ts:6

RunOptions interface

interface RunOptions
enterTimeoutMs?: number
log?: (event: string, detail: Record<string, unknown>) => void

packages/levels/src/transitions.ts:84

RuntimeAssetsOptions interface

interface RuntimeAssetsOptions
store: AssetStore
models: MountAssetsloads and instantiates glTF (an AssetCache, or mount.assets when packages are in use)
mount?: PackageMountneeded for ferret:// references

packages/levels/src/runtime.ts:46

SpawnPoint interface

interface SpawnPoint
id: string
position: BABYLON.Vector3
yaw: number

packages/levels/src/levels.ts:7

StreamCellDef interface

interface StreamCellDef

One grid cell of a streamed level: visual content only; collision is whole-level and resident. bounds = [minX, minY, minZ, maxX, maxY, maxZ].

bounds: [number, number, number, number, number, number]
file: string
size?: number
priority?: number
pinned?: booleanalways resident (the play area round the spawn): fetched first, part of the ready gate, never released and never evicted for memory
layer?: stringa key of layers: the cell uses that layer's radii instead of the block's (foliage is wanted closer than houses)
memory?: numberbytes the cell takes once resident (geometry plus decoded textures); counted against memoryBudget. Default: size
lod?: stringa low-poly stand-in shown while the cell is farther than loadRadius but within lodRadius (a few percent of the cell's triangles, no textures worth the name)

packages/levels/src/manifest.ts:29

StreamingBlock interface

interface StreamingBlock

The streaming block. A level without it loads whole, as before.

cell: numberthe grid size in metres the cells were cut with (informational for tools)
cells: Record<string, StreamCellDef>
loadRadius: numbermetres from the player: cells inside are loaded (and are part of the ready gate at the spawn)
unloadRadius: numbercells farther than this are released; must be at least loadRadius
bundles?: Record<string, { files: string[]; trigger?: string }>groups of files loaded together when a zone is entered (or on demand)
lodRadius?: numbermetres from the player within which a cell's lod stand-in is shown; at least loadRadius (default: unloadRadius)
layers?: Record<string, { loadRadius?: number; unloadRadius?: number; lodRadius?: number }>radii per kind of cell, over the block's: a cell names its layer in layer. unloadRadius must not be below loadRadius (that gap is the hysteresis)
memoryBudget?: numberbytes of cells that may be resident at once (this tier's cap: every tier package has its own block). Pinned cells are counted but never evicted; a nearer cell takes the place of farther ones, and when it still does not fit it waits (event stream:budget)
maxPerFrame?: numberpacing over the service's defaults (2 cells and 4 ms per update, 3 fetches at once); the host's options win
budgetMs?: number
concurrency?: number
texturesProgressive?: booleanreserved: low mips first, upgrade later; nothing acts on it yet

packages/levels/src/manifest.ts:42

StreamingOptions interface

interface StreamingOptions
at(file: string): stringwhere the level's files are (cell and bundle file names are relative to it)
focus?: () => Focus[]the places that matter, asked every update; when it gives none (or is left out): the persistent entities of the level service, else the active camera
radiusScale?: numberscale both radii (a tier may ask for less)
memoryBudget?: numberbytes of resident cells, over the block's memoryBudget (a tier or a device may ask for less)
maxPerFrame?: numbermost cells instantiated in one update
budgetMs?: numbermilliseconds of instantiation work per update; at least one cell is always added when something waits, so streaming cannot starve
concurrency?: numbermost cells fetched at once
now?: () => number

packages/levels/src/streaming.ts:26

Transition interface

interface Transition
enter(ctx: TransitionContext): Promise<void> | voidThe travel starts. Show something at once. Resolve when it is on screen (after 2 s the engine's bar takes over).
update(p: TransitionProgress): voidThe target's progress, many times a second.
ready(ctx: TransitionContext): Promise<void> | voidThe target is ready. Resolve when the game wants to leave: a door has opened, a button was pressed, a timer ran out.
fail(ctx: TransitionContext, err: StreamError): boolean | void | Promise<boolean | void>The target failed or the player cancelled. Return true when the transition shows the failure itself (Retry / Back); otherwise the engine's screen is shown.
dispose(): void

packages/levels/src/transitions.ts:36

TransitionContext interface

interface TransitionContext
from: LevelDef | null
to: LevelDef
plan: LevelPlanwhat the travel will download; cached means a short wait is expected
world: World
events: EventBus
ui?: UIServicethe services a game's splash usually wants, when the game has them
audio?: unknown
input?: unknown
xr?: { active: boolean; state: string }the VR session service when the game has one: a transition may show its interlude world in the headset; xr.active says whether a session runs
persistent: Set<Entity>the player and what travels with them
host?: SceneHostthe scene host (5.3): make a second world for an interlude with host.create, show it with host.show. The engine shows the level's world again when the player is let in. Present when the game added a SceneHost.
signal: AbortSignalthe player cancelled

packages/levels/src/transitions.ts:15

TransitionProgress interface

interface TransitionProgress

What a transition is told, many times a second, while the target downloads. Numbers are honest bytes of what the target needs.

loaded: number
total: number
files: number
rate: number
eta: number
phase: string

packages/levels/src/transitions.ts:13

TravelOptions interface

interface TravelOptions
spawn?: string
keep?: Entity[]keep these entities: they move to the new level's spawn instead of being removed (the player)
keepOld?: booleanKeep the old level's way back: if the new one fails after the old was released, build the old one again and put the player where they were (default: LevelOptions.keepOldUntilReady, else the hub).
signal?: AbortSignalCancel from a menu: before the old level is released the player simply stays; later they are put back.
transition?: stringThe game's transition for this travel, by the name it registered (else its chooser, else default, else the engine's bar).

packages/levels/src/levels.ts:62

ValidateOptions interface

interface ValidateOptions
packages?: PackageSetthe other open packages, for ferret:// references and dependencies
behaviours?: Iterable<string>behaviours the game registers itself (besides those packages provide); when given, an unknown behaviour is an issue
checksums?: booleancheck the sha256 of every file (default true)
engine?: EngineVersions | falsethe engine that is running; default this build's (FERRET_VERSION, BABYLON_VERSION); false skips the check of the manifest's engine
tierView?: booleanset when pkg is already one tier's view of a game (its own call for each tier)

packages/levels/src/ferret.ts:167

VirtualTextureDef interface

interface VirtualTextureDef
resolution?: numbertexels along the longer side of the rectangle (default 2048)
blendHeight?: numbermetres above the ground over which a prop's albedo fades from the ground colour to its own (default 1.5)
strength?: number0..1, how much of the ground colour a prop takes at its foot (default 1)
meshes: stringregex source: the meshes (by name, anywhere in the level) whose materials sample the virtual texture

packages/levels/src/landscape.ts:34

WaterDef interface

interface WaterDef
level: numbery of the surface at rest
centre?: [number, number]centre (x, z) and size (x, z) of the surface, metres
size?: [number, number]
absorption?: Vec3the colour of the water seen from above when deep: Unreal's single-layer water Absorption is the distance per colour channel (cm) light goes before it is gone, Scattering how much the water sends back; the colour is the scattering tinted by those distances
scattering?: Vec3
sky?: { top?: Vec3; horizon?: Vec3 }the colour of the sky it reflects (default the sky's horizon / top)
roughness?: number0..1 (default 0.02)
specular?: number0..1, the part of the sky a calm surface reflects head-on (default 0.02, about water's Fresnel at normal incidence)
waves?: WaterWave[]
opacity?: number0..1, how opaque the surface draws (default 0.92)
foam?: number0..1, how strongly breaking crests turn to white water (default 0.8; a gentle sea has none whatever this is)

packages/levels/src/water.ts:19

WaterSurfaceView interface

interface WaterSurfaceView
mesh: BABYLON.Mesh
material: BABYLON.ShaderMaterial
setWaves(waves: WaterWave[]): void
dispose(): void

packages/levels/src/water.ts:142

WaterWave interface

interface WaterWave
wavelength: numbermetres crest to crest
amplitude: numbermetres, crest above the rest level
steepness?: number0..1: how sharp the crests are (Gerstner Q)
direction?: numberdegrees about the vertical, the way the wave travels (0 = +x)

packages/levels/src/water.ts:8

Types

ModelState type

type ModelState = 'loading' | 'ready' | 'failed'

packages/levels/src/runtime.ts:33

PlaceholderSpec type

type PlaceholderSpec = 'box' | 'none' | { size?: number | [number, number, number]; color?: [number, number, number] } | ((scene: BABYLON.Scene) => BABYLON.TransformNode)

packages/levels/src/runtime.ts:22

RefKind type

type RefKind = 'package' | 'remote' | 'relative'

packages/levels/src/runtime.ts:13

ResolvedAtmosphere type

type ResolvedAtmosphere = Required<AtmosphereDef>

packages/levels/src/atmosphere.ts:58

TransitionChooser type

type TransitionChooser = (from: LevelDef | null, to: LevelDef, plan: LevelPlan) => string | undefined | null

packages/levels/src/transitions.ts:52

Constants

ATMOSPHERE_EXPOSURE const

const ATMOSPHERE_EXPOSURE: 30

packages/levels/src/atmosphere.ts:46

ATMOSPHERE_FRAGMENT const

const ATMOSPHERE_FRAGMENT: "precision highp float;\nvarying vec3 vDir;\n#define N 24\nuniform vec3 rayleigh[N]; uniform vec3 mie[N];\nuniform vec3 sunDir; uniform vec3 sunColor; uniform vec3 sunDisc; uniform vec3 lum; uniform vec3 ground; uniform vec3 horizonFill;\nuniform float g; uniform float ms; uniform float exposure;\nvoid main() {\n  vec3 d = normalize(vDir);\n  float h = d.y;\n  vec3 toSun = -sunDir;\n  float c = do …

the sky dome's fragment shader body for the table (arrays of LUT_SIZE entries); uniforms: rayleigh[], mie[], sunDir, sunColor (already x strength), g, ms, lum, ground, exposure

packages/levels/src/atmosphere.ts:197

BAR const

const BAR: "bar"

The engine's own transition: it draws nothing itself, the loading screen of

packages/levels/src/transitions.ts:49

barTransition const

const barTransition: Transition

packages/levels/src/transitions.ts:50

DEFAULT_LIBRARY_URL const

const DEFAULT_LIBRARY_URL: "/lib/ferret-assets/catalog.json"

packages/levels/src/library.ts:21

HEIGHT_TRANSITION const

const HEIGHT_TRANSITION: 0.2

how much height a height-blended layer needs to beat the others (score units), as a transition width

packages/levels/src/landscape.ts:63

LUT_SIZE const

const LUT_SIZE: 24

packages/levels/src/atmosphere.ts:48

MANIFEST_VERSION const

const MANIFEST_VERSION: 1

packages/levels/src/manifest.ts:9

MAX_LANDSCAPE_LAYERS const

const MAX_LANDSCAPE_LAYERS: 8

packages/levels/src/landscape.ts:59

MAX_LANDSCAPE_SAMPLERS const

const MAX_LANDSCAPE_SAMPLERS: 11

WebGL guarantees 16 texture units; the PBR material uses a few, so layers + normals + weightmaps stay under this

packages/levels/src/landscape.ts:61

MAX_MATERIAL_LIGHTS const

const MAX_MATERIAL_LIGHTS: 8

most lights one material may light with (uniform-block limit of D3D11 through ANGLE, 12 vertex blocks, 4 taken by scene/material/mesh/bones)

packages/levels/src/light-budget.ts:128

MOVEMENT_KEYS const

const MOVEMENT_KEYS: readonly ["radius", "height", "crouchHeight", "eyeHeight", "walkSpeed", "runSpeed", "crouchSpeed", "jumpVelocity", "gravity", "maxStepHeight", "walkableAngle", "acceleration", "braking", "airControl", "swimSpeed", "buoyancy", "swimAcceleration", "outOfWaterZ", "jumpOutOfWaterPitch", "maxOutOfWaterStepHeight"]

packages/levels/src/manifest.ts:103

MULTI_SCATTER const

const MULTI_SCATTER: 0.3

packages/levels/src/atmosphere.ts:47