@ferret/levels
Level manifests, the level registry, load, unload and travel, environment and the .ferret package format.
Classes
AssetLibrary class
class AssetLibrary implements Servicereadonly 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 | undefined | Absolute 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(): void | Free 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 CatalogClientreadonly 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 CodeLoaderconstructor(readonly world: World, readonly mount: PackageMount, private o: CodeLoaderOptions = {}) | |
list(): LoadedCode[] | The code loaded so far, in load order. |
exports(id: string): LibraryModule | null | undefined | The 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(): void | Call 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 CollisionChunksKeeps 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 ServiceThe 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: TransitionRegistry | the 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): boolean | Remove a level from the registry (a catalog that no longer lists it). The level being played is not touched. |
replace(def: LevelDef): this | Register, 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): this | An 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): this | Entities that go with the player to every level. |
async travel(id: string, opts: TravelOptions = {}): Promise<LevelReport> | |
spawn(id?: string): SpawnPoint | null | The 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(): number | how 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 | null | Restore what saveData made; returns the level to travel to (or null). |
dispose() |
packages/levels/src/levels.ts:108
LightBudget class
class LightBudgetreadonly def: LightBudgetDef | |
onChange?: (entered: number[], left: number[]) => void | called after a re-ranking when real lights changed hands: indices that entered and left the pool |
onSounds?: (start: number[], stop: number[]) => void | called 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): number | the 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 PackageMountConnects 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: MountAssets | give this to the level loader instead of the asset cache (installPackages does) |
readonly loader: PackageLoaderOptions | the 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 | null | The 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): void | Drop 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 Servicereadonly name: "prefetch" | |
constructor(private readonly levels: LevelService, o: PrefetchOptions = {}) | |
init(world: World): void | |
near(zone: string, levelId: string, o: { inSeconds?: number } = {}): () => void | Doors and zones: entering zone makes levelId wanted (the player is probably about to go there). |
hint(id: string, o: { inSeconds?: number; from?: 'level' | 'game' } = {}): void | id 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[]): void | Only these are wanted from now on: every other background download stops at its next file. |
cancel(id: string): void | |
get bandwidth(): number | The 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 ProbeVolumeThe 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 Servicereadonly 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 = {}): ModelHandle | Ask 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 StreamingDebugViewconstructor(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 Servicereadonly 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 | null | the block being streamed (for the debug view and tools) |
begin(block: StreamingBlock, source: CellSource, o: StreamingOptions): void | Start streaming a level. The cells within loadRadius of the focus are what ready() waits for. |
end(): void | Stop 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): void | Called 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 | null | the 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 TransitionRegistryregister(name: string, t: Transition | (() => Transition)): this | Register 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): string | The 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 TransitionRunA 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 | null | set 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 ErrorThe 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 } = {}): EnvironmentBuild 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): numberpackages/levels/src/environment.ts:104
buildAtmosphereTable function
function buildAtmosphereTable(def: AtmosphereDef | undefined, sunElevation: number, n = LUT_SIZE): AtmosphereTableFill 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 }): WaterSurfaceViewBuild 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): numberDistance 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]): Vec3The deep colour of the water from Unreal's absorption distances and scattering (linear RGB).
packages/levels/src/water.ts:62
defaultLibraryUrl function
function defaultLibraryUrl(): stringpackages/levels/src/library.ts:94
distanceToBounds function
function distanceToBounds(b: CollisionChunkDef['bounds'], x: number, y: number, z: number): numberDistance from a point to a box (0 inside).
packages/levels/src/collision-chunks.ts:13
effectiveFogDensity function
function effectiveFogDensity(f: HeightFogDef, h: number): numberthe 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): numberthe 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): numbera 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): numberextinction per metre at height h
packages/levels/src/height-fog.ts:21
fogOpticalDepth function
function fogOpticalDepth(f: HeightFogDef, h0: number, dirY: number, dist: number): numberOptical 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): Float32ArraySpread 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): voidThe 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 = {}): CatalogClientPlug 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 = {}): CodeLoaderLet a mount run the code of its packages.
packages/levels/src/code.ts:171
installCodeCommands function
function installCodeCommands(world: World, code: CodeLoader): voidpkg.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): voidMake 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 = {}): voidThe 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): voidTeach 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 = {}): PackageMountSet a LevelService up to play packages.
packages/levels/src/ferret.ts:154
installStreamCommands function
function installStreamCommands(world: World, store: AssetStore): voidstream.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): StreamingDebugViewstream.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): voidThe 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'): stringThe 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): numberthe 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): numberpackages/levels/src/atmosphere.ts:82
pickTier function
function pickTier<T>(have: Partial<Record<LibraryTier, T>>, want: LibraryTier = 'desktop'): { tier: LibraryTier; value: T } | nullThe 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): numberpackages/levels/src/atmosphere.ts:81
refKind function
function refKind(ref: string): RefKindWhich scheme a reference uses.
packages/levels/src/runtime.ts:16
resolveAtmosphere function
function resolveAtmosphere(a: AtmosphereDef = {}): ResolvedAtmospherepackages/levels/src/atmosphere.ts:59
skyRadiance function
function skyRadiance(t: AtmosphereTable, elevation: number, cosSun: number, sunScale = 1, sunColor: Vec3 = [1, 1, 1]): Vec3Radiance 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
waveHeight function
function waveHeight(waves: WaterWave[], x: number, z: number, t: number): numberHeight 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 ApplyLandscapeOptionsat: (file: string) => string | turn 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 AssetLibraryOptionsstore: AssetStore | |
mount: PackageMount | |
assets?: RuntimeAssets | needed for characters |
tier?: LibraryTier | the package tier to open (default desktop); a tier a package does not have falls back to its plain file |
policy?: CatalogPolicy | the catalog policy (trusted keys, allowUnsigned) |
packages/levels/src/library.ts:23
AtmosphereDef interface
interface AtmosphereDefUnreal's SkyAtmosphere component. Every field is optional and defaults to the component's own default. Lengths km, coefficients per km.
groundRadius?: number | planet radius, km (6360) |
atmosphereHeight?: number | thickness of the air, km (60) |
rayleighScattering?: Vec3 | Rayleigh scattering per km (0.005802, 0.013558, 0.0331) |
rayleighScaleHeight?: number | Rayleigh scale height, km (8) |
mieScattering?: Vec3 | Mie scattering per km (0.003996 each) |
mieAbsorption?: Vec3 | Mie absorption per km (0.00444 each) |
mieScaleHeight?: number | Mie scale height, km (1.2) |
mieAnisotropy?: number | Mie anisotropy g, -1..1 (0.8) |
absorption?: Vec3 | ozone absorption per km at the middle of its layer (0.00065, 0.001881, 0.000085) |
groundAlbedo?: Vec3 | colour of the ground the air sits on (0.402 each) |
multiScatteringFactor?: number | how much of the single scattering comes back as multiple scattering (1) |
skyLuminanceFactor?: Vec3 | colour factor on the sky's luminance (1 each) |
observerAltitude?: number | how far the observer is above the ground, km (0) |
packages/levels/src/atmosphere.ts:17
AtmosphereTable interface
interface AtmosphereTablen: 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: Vec3 | share 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: Vec3 | colour 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 AttachOptionssocket?: string | a socket of the body's item (grip_r, axe_r...) |
bone?: string | a 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?: boolean | also 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 BindOptionsmap?: 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 BudgetLightA 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?: string | a name for events and tests (the source actor's) |
group?: string | a group of lamps of one kind ('torch', 'candle'), for statistics and the sound budget |
position: Vec3 | the level's metres |
color: Vec3 | linear colour 0..1 |
intensity: number | Babylon intensity at 1 m (already converted from the source engine's units, see tools/ue-export/lights.mjs) |
range: number | the metres where the light reaches zero (the source engine's attenuation radius) |
kind?: 'point' | 'spot' | |
direction?: Vec3 | a 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?: number | nearer than a light of priority 1 by this factor when ranking (a chandelier over a candle); default 1 |
size?: number | the size of the glow dot in metres (default from the range) |
sound?: string | a 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 CatalogPolicytier?: string | device tier: packages with a tiers[tier] file use it |
prefix?: string | put in front of every level id of this catalog (two catalogs may share ids) |
packages/levels/src/catalog.ts:11
CatalogRegistration interface
interface CatalogRegistrationreadonly url: string | |
readonly catalog: Catalog | |
readonly stale: boolean | the catalog came from the copy kept by an earlier session (no network) |
readonly unsigned: boolean | development 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 CellSourceWhat 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?: boolean | also 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 ClipRefA 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?: string | set 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 CodeLoaderOptionsorigin?: (pkg: FerretPackage) => boolean | does 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> | undefined | key ids whose signatures are trusted; without them any valid signature is enough |
ferret?: unknown | the engine's API handed to the code (default globalThis.Ferret, the shared engine) |
esm?: boolean | the 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) => void | add 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 CollisionChunkDeffile: string | |
bounds: [number, number, number, number, number, number] |
packages/levels/src/collision-chunks.ts:9
CollisionChunksDef interface
interface CollisionChunksDefloadRadius: number | |
unloadRadius: number | |
chunks: CollisionChunkDef[] |
packages/levels/src/collision-chunks.ts:10
Environment interface
interface Environmentpost?: 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 EnvironmentDefThe 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?: AtmosphereDef | Unreal'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?: ProbeVolumeDef | baked 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?: LightBudgetDef | hundreds 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 FocusA 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 HeightFogDefdensity: number | |
heightFalloff: number | |
height: number | height of the fog actor, metres |
second?: HeightFogLayer |
packages/levels/src/height-fog.ts:9
HeightFogLayer interface
interface HeightFogLayerdensity: number | |
heightFalloff: number | |
heightOffset?: number |
packages/levels/src/height-fog.ts:7
LandscapeDef interface
interface LandscapeDefname: string | |
mesh?: string | regex 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?: boolean | the 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 LandscapeLayerDefname: string | |
albedo: string | colour image (sRGB); with heightBlend its alpha channel is the layer's height map |
normal?: string | optional tangent-space normal image |
tile: number | metres of one repeat of the layer's textures (Unreal: 100 / the material's UV scale) |
tint?: Vec3 | multiplies the albedo (linear) |
heightBlend?: number | 0 = 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 LandscapeTexturesweights: BABYLON.BaseTexture[] | |
albedo: BABYLON.BaseTexture[] | |
normal: (BABYLON.BaseTexture | undefined)[] |
packages/levels/src/landscape.ts:192
LandscapeView interface
interface LandscapeViewdef: LandscapeDef | |
meshes: BABYLON.AbstractMesh[] | |
virtualTexture?: BABYLON.BaseTexture | the baked virtual texture (colour in rgb, ground height in a), when the landscape has one |
requiredMaxZ?: number | camera 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 LevelContextWhat 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 }): void | declare where the player may start |
progress(p: number): void | report progress 0..1 to the loading screen |
packages/levels/src/levels.ts:10
LevelDef interface
interface LevelDefOne entry of the level registry (the equivalent of a row of UE's DT_LevelInfo).
id: string | |
name?: string | |
url?: string | a 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?: string | the 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 LevelManifestversion: number | |
name?: string | |
scene?: string | file 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?: string | a mesh of simple collision shapes (invisible, solid). When present, the visible meshes of the scene do not collide |
collisionChunks?: CollisionChunksDef | the 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?: string | the spawn used when the level is entered without naming one |
environment?: EnvironmentDef | sun, 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?: StreamingBlock | streaming 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 LevelOptionsMinimum time the loading screen stays up, so it does not flash (ms; 0 in tests).
minLoadingMs?: number | |
slowMs?: number | no new bytes for this long while the target downloads: travel:slow {slow: true} (default 5000) |
holdMs?: number | the 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?: boolean | true on devices with room: a failed travel rebuilds the old level and returns the player to the spot; false (console): to the hub |
transitionEnterMs?: number | how 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 LevelPlanbytes: number | |
files: number | |
cached: boolean |
packages/levels/src/levels.ts:45
LevelReport interface
interface LevelReportwarnings: 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 LibraryCharacterreadonly 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 LibraryContextWhat install gets.
world: World | |
ferret: any | the 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): unknown | the exports of a declared dependency's code (already loaded before this one) |
packages/levels/src/code.ts:18
LibraryEntry interface
interface LibraryEntry extends CatalogAssetid: string |
packages/levels/src/library.ts:35
LibraryModel interface
interface LibraryModelA 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 LibraryModuleWhat 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 LibraryQuerytype?: LibraryItem['type'] | |
tags?: string[] | |
text?: string |
packages/levels/src/library.ts:34
LightBudgetDef interface
interface LightBudgetDefbudget: number | how 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?: number | seconds between two re-rankings (default 0.25) |
fade?: number | seconds a light takes to fade out of / into a real light (default 0.35) |
hysteresis?: number | a 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?: number | lights 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 LightBudgetOptionsname?: string | |
viewer?: () => Vec3 | undefined | where the viewer is, in the level's metres; default the active camera |
materials?: boolean | raise 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 LightBudgetStatsitems: number | |
budget: number | |
real: number | |
glowing: number | |
reranks: number | |
soundsOn: number |
packages/levels/src/light-budget.ts:117
LoadedCode interface
interface LoadedCodeid: string | |
version: string | |
exports: LibraryModule | null | the 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 ManifestEntityid: string | |
behaviour?: string | a 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?: string | the 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?: string | the 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 ManifestIssuepath: string | |
message: string |
packages/levels/src/manifest.ts:105
ManifestSpawn interface
interface ManifestSpawnid: string | |
position: [number, number, number] | |
yaw?: number |
packages/levels/src/manifest.ts:65
MaterialOptions interface
interface MaterialOptionsvariant?: string | |
name?: string | |
scene?: BABYLON.Scene |
packages/levels/src/library.ts:83
ModelHandle interface
interface ModelHandleA 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?: PlaceholderSpec | shown at once, replaced by the model (default: a small box); 'none' shows nothing until the model is in |
parent?: BABYLON.TransformNode | the 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 PackageAssetsWhat 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 MountOptionspackages?: PackageSet | the 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 PackageAssetsWhat 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 PackageCommandHooksresolve?: (url: string) => PackageInput | Promise<PackageInput> | turn a url or name into something FerretPackage.open reads (default: the url itself) |
opened?: (pkg: FerretPackage, url: string) => void | told 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> | undefined | key ids the reader trusts (for \pkg.verify\ and the code check) |
packages/levels/src/commands.ts:7
PackageLoaderOptions interface
interface PackageLoaderOptionsassets: PackageAssets | |
fetch?: (url: string) => Promise<{ ok: boolean; status: number; json(): Promise<any>; arrayBuffer(): Promise<ArrayBuffer> }> | replace fetch (tests, a Platform file API) |
collisions?: boolean | make the level's meshes collide (default true) |
manifest?: (def: LevelDef) => LevelManifest | undefined | a manifest to use instead of the one in the package: the editor's draft (return undefined to load the file) |
streamingRadiusScale?: number | scale the streaming radii of every streamed level (a tier may ask for less) |
forceNavBuild?: (def: LevelDef) => boolean | build the NavMesh from navBuild even if navmesh.bin exists (the editor, after a rebuild) |
packages/levels/src/package.ts:18
PrefetchOptions interface
interface PrefetchOptionssettleMs?: number | wait this long after a level is ready before the first background download (the level's own first seconds come first) |
margin?: number | safety factor on the estimate: start early enough to finish in eta * margin (default 1.5) |
assumedRate?: number | assumed 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 PreviewOptionsseconds?: number | how long the fake download lasts |
bytes?: number | |
files?: number | |
fail?: boolean | end with a failure instead of readiness (to see the game's failure screen) |
cached?: boolean | cached: plan says the wait is short |
signal?: AbortSignal |
packages/levels/src/transitions.ts:146
ProbeVolumeDef interface
interface ProbeVolumeDefBaked 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: string | the baked grid, relative to the package: probes.bin |
origin: [number, number, number] | the position of the first probe, metres |
spacing: number | metres between probes |
dims: [number, number, number] | probes along x, y, z |
gain?: number | scale of the baked light (default 1) |
fill?: number | how 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?: number | the 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 RunOptionsenterTimeoutMs?: number | |
log?: (event: string, detail: Record<string, unknown>) => void |
packages/levels/src/transitions.ts:84
RuntimeAssetsOptions interface
interface RuntimeAssetsOptionsstore: AssetStore | |
models: MountAssets | loads and instantiates glTF (an AssetCache, or mount.assets when packages are in use) |
mount?: PackageMount | needed for ferret:// references |
packages/levels/src/runtime.ts:46
SpawnPoint interface
interface SpawnPointid: string | |
position: BABYLON.Vector3 | |
yaw: number |
packages/levels/src/levels.ts:7
StreamCellDef interface
interface StreamCellDefOne 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?: boolean | always resident (the play area round the spawn): fetched first, part of the ready gate, never released and never evicted for memory |
layer?: string | a key of layers: the cell uses that layer's radii instead of the block's (foliage is wanted closer than houses) |
memory?: number | bytes the cell takes once resident (geometry plus decoded textures); counted against memoryBudget. Default: size |
lod?: string | a 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 StreamingBlockThe streaming block. A level without it loads whole, as before.
cell: number | the grid size in metres the cells were cut with (informational for tools) |
cells: Record<string, StreamCellDef> | |
loadRadius: number | metres from the player: cells inside are loaded (and are part of the ready gate at the spawn) |
unloadRadius: number | cells 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?: number | metres 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?: number | bytes 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?: number | pacing 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?: boolean | reserved: low mips first, upgrade later; nothing acts on it yet |
packages/levels/src/manifest.ts:42
StreamingOptions interface
interface StreamingOptionsat(file: string): string | where 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?: number | scale both radii (a tier may ask for less) |
memoryBudget?: number | bytes of resident cells, over the block's memoryBudget (a tier or a device may ask for less) |
maxPerFrame?: number | most cells instantiated in one update |
budgetMs?: number | milliseconds of instantiation work per update; at least one cell is always added when something waits, so streaming cannot starve |
concurrency?: number | most cells fetched at once |
now?: () => number |
packages/levels/src/streaming.ts:26
Transition interface
interface Transitionenter(ctx: TransitionContext): Promise<void> | void | The travel starts. Show something at once. Resolve when it is on screen (after 2 s the engine's bar takes over). |
update(p: TransitionProgress): void | The target's progress, many times a second. |
ready(ctx: TransitionContext): Promise<void> | void | The 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 TransitionContextfrom: LevelDef | null | |
to: LevelDef | |
plan: LevelPlan | what the travel will download; cached means a short wait is expected |
world: World | |
events: EventBus | |
ui?: UIService | the 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?: SceneHost | the 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: AbortSignal | the player cancelled |
packages/levels/src/transitions.ts:15
TransitionProgress interface
interface TransitionProgressWhat 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 TravelOptionsspawn?: string | |
keep?: Entity[] | keep these entities: they move to the new level's spawn instead of being removed (the player) |
keepOld?: boolean | Keep 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?: AbortSignal | Cancel from a menu: before the old level is released the player simply stays; later they are put back. |
transition?: string | The 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 ValidateOptionspackages?: PackageSet | the 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?: boolean | check the sha256 of every file (default true) |
engine?: EngineVersions | false | the engine that is running; default this build's (FERRET_VERSION, BABYLON_VERSION); false skips the check of the manifest's engine |
tierView?: boolean | set 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 VirtualTextureDefresolution?: number | texels along the longer side of the rectangle (default 2048) |
blendHeight?: number | metres above the ground over which a prop's albedo fades from the ground colour to its own (default 1.5) |
strength?: number | 0..1, how much of the ground colour a prop takes at its foot (default 1) |
meshes: string | regex source: the meshes (by name, anywhere in the level) whose materials sample the virtual texture |
packages/levels/src/landscape.ts:34
WaterDef interface
interface WaterDeflevel: number | y of the surface at rest |
centre?: [number, number] | centre (x, z) and size (x, z) of the surface, metres |
size?: [number, number] | |
absorption?: Vec3 | the 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?: number | 0..1 (default 0.02) |
specular?: number | 0..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?: number | 0..1, how opaque the surface draws (default 0.92) |
foam?: number | 0..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 WaterSurfaceViewmesh: BABYLON.Mesh | |
material: BABYLON.ShaderMaterial | |
setWaves(waves: WaterWave[]): void | |
dispose(): void |
packages/levels/src/water.ts:142
WaterWave interface
interface WaterWavewavelength: number | metres crest to crest |
amplitude: number | metres, crest above the rest level |
steepness?: number | 0..1: how sharp the crests are (Gerstner Q) |
direction?: number | degrees 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 | nullpackages/levels/src/transitions.ts:52
Constants
ATMOSPHERE_EXPOSURE const
const ATMOSPHERE_EXPOSURE: 30packages/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: Transitionpackages/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.2how 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: 24packages/levels/src/atmosphere.ts:48
MANIFEST_VERSION const
const MANIFEST_VERSION: 1packages/levels/src/manifest.ts:9
MAX_LANDSCAPE_LAYERS const
const MAX_LANDSCAPE_LAYERS: 8packages/levels/src/landscape.ts:59
MAX_LANDSCAPE_SAMPLERS const
const MAX_LANDSCAPE_SAMPLERS: 11WebGL 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: 8most 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.3packages/levels/src/atmosphere.ts:47