@kvyverse/world-runtime 0.3.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,9 +35,9 @@ in your `node_modules`:
35
35
  | Package | When |
36
36
  | --- | --- |
37
37
  | `three`, `three-start` | always |
38
- | `howler`, `vanjs-core`, `eventemitter3` | always — npm 7+ installs them for you |
39
- | `@dimforge/rapier3d-compat` | worlds with physics |
40
- | `@tweenjs/tween.js`, `animejs`, `nipplejs` | worlds that enable these built-in libs |
38
+ | `howler`, `vanjs-core`, `eventemitter3`, `nanostores` | always — npm 7+ installs them for you |
39
+ | `@dimforge/rapier3d-compat` | worlds with physics (a collider on the scene is enough) |
40
+ | `@tweenjs/tween.js`, `animejs`, `nipplejs`, `stats.js` | worlds that enable these optional libs |
41
41
 
42
42
  The optional ones are listed in the dialog only when the world actually uses
43
43
  them. Skip one it does need and the build fails with a clear
@@ -82,8 +82,15 @@ world.unmount(), world.runLoop(), world.stopLoop(), world.dispose()
82
82
  ```
83
83
 
84
84
  On top of that: `world.getUrl(path)` resolves an asset URL the same way scripts
85
- inside the world do, and `world.events` is the world lifecycle emitter (the same
86
- `world.events` your scripts use).
85
+ inside the world do, `world.events` is the world lifecycle emitter (the same
86
+ `world.events` your scripts use), and `world.stores` is the world's shared
87
+ reactive state (the same `world.stores`) — your host page can read it, write it
88
+ and subscribe to keys the world's scripts use:
89
+
90
+ ```js
91
+ world.stores.listen("score", (value) => hudElement.textContent = value);
92
+ world.stores.set("difficulty", "hard");
93
+ ```
87
94
 
88
95
  ## Good to know
89
96
 
@@ -1,3 +1,4 @@
1
+ import { a as LibId } from "./lib-registry-D1VyVPBg.js";
1
2
  import { ContextModule, ThreeContextEvents, defineProps, readOnly } from "three-start";
2
3
  //#region packages/world-runtime/src/modules/physics/RapierPhysics.ts
3
4
  var RapierPhysics = class RapierPhysics extends ContextModule {
@@ -23,7 +24,7 @@ var RapierPhysics = class RapierPhysics extends ContextModule {
23
24
  this.timeStep = timeStep;
24
25
  }
25
26
  onAwake() {
26
- const Rapier = this.modules.addons.rapier;
27
+ const Rapier = this.modules.addons.lib(LibId.Rapier);
27
28
  if (!Rapier) throw new Error("RapierPhysics: the physics engine is not loaded — `addons` has no rapier");
28
29
  const [x, y, z] = this._gravity;
29
30
  console.log(`Rapier version: ${Rapier.version()}`);
package/dist/World.d.ts CHANGED
@@ -14,6 +14,8 @@ export declare class World extends ThreeStart {
14
14
  getUrl: (source: string | object) => string;
15
15
  /** World lifecycle event bus — the same one as `world.events` in scripts. */
16
16
  get events(): WorldRuntime["emitter"];
17
+ /** Shared reactive state — the same one as `world.stores` in scripts. */
18
+ get stores(): WorldRuntime["stores"];
17
19
  start(): this;
18
20
  dispose(): void;
19
21
  }
@@ -1,6 +1,7 @@
1
1
  import EventEmitter from "eventemitter3";
2
- import * as THREE from "three/webgpu";
3
2
  import { AddonsRuntime } from "./addons/AddonsRuntime";
3
+ import { WorldStores } from "./world-stores";
4
+ import type * as THREE from "three/webgpu";
4
5
  import type { Object3DBehaviourConstructor, ThreeContext, ThreeStart } from "three-start";
5
6
  import type { SceneBloom } from "./rendering/SceneBloom";
6
7
  import type { LoadWorldOptions, RuntimeApi, WorldDefinition } from "./types";
@@ -15,6 +16,8 @@ export declare class WorldRuntime implements RuntimeApi {
15
16
  readonly options: LoadWorldOptions;
16
17
  /** World lifecycle event bus (`world.events` in scripts). */
17
18
  readonly emitter: EventEmitter<string | symbol, any>;
19
+ /** Shared reactive state of the world (`world.stores` in scripts). */
20
+ readonly stores: WorldStores;
18
21
  private readonly _addons;
19
22
  private readonly _executedScripts;
20
23
  private readonly _builtins;
@@ -34,10 +37,8 @@ export declare class WorldRuntime implements RuntimeApi {
34
37
  setContext(ctx: ThreeContext): void;
35
38
  /** @internal World bloom, if enabled in the renderer config. */
36
39
  setBloom(bloom: SceneBloom): void;
37
- /** @internal Built-in libs, externals and the physics engine, if the world needs one. */
40
+ /** @internal Libs (config + required by modules/behaviours) and externals. */
38
41
  loadAddons(): Promise<void>;
39
- /** Physics is on in the config, or a built-in behaviour on the scene needs it. */
40
- private _usesPhysics;
41
42
  /** @internal World resources by asset path — result of `loadAssets`. */
42
43
  setResources(resources: Record<string, unknown>): void;
43
44
  reportProgress(stage: string, details?: string): void;
@@ -1,34 +1,36 @@
1
1
  import { ContextModule } from "three-start";
2
2
  import type { AddonsModuleApi } from "../modules/contracts/addons.module-api";
3
+ import type { LibId } from "./lib-registry";
3
4
  import type { AddonsConfig, WorldImports } from "./types";
4
- import type RAPIER from "@dimforge/rapier3d-compat";
5
5
  /**
6
- * External dependencies of the world: built-in libs, externals and the physics
7
- * engine. Loaded before the world starts; scripts reach them through
8
- * `resolve(key)` (`require`), the physics module through `rapier`.
6
+ * External dependencies of the world: libs and external scripts. Built-in libs
7
+ * are registered right away (the runtime bundles them), optional ones are loaded
8
+ * before the world starts — enabled in `settings.addons.json → libs` or declared
9
+ * by a module/behaviour through `requiredLibs`. Scripts reach all of them by
10
+ * `require("<key>")`.
9
11
  *
10
12
  * This is the `addons` ctx module, but it also works without a ctx: launcher
11
13
  * worlds have no 3d context while `require` still has to work, so there the
12
- * instance is used directly. How to import packages is up to the runtime owner
13
- * (see `WorldImports`).
14
+ * instance is used directly. How to import optional packages is up to the
15
+ * runtime owner (see `WorldImports`).
14
16
  */
15
17
  export declare class AddonsRuntime extends ContextModule implements AddonsModuleApi {
16
18
  private readonly _imports;
17
19
  /** libs — by requireKey (`@tweenjs/tween.js`), externals — by accessor (`Tone`). */
18
20
  private readonly _byKey;
21
+ private readonly _byLibId;
19
22
  private readonly _scriptTags;
20
- private _rapier;
21
23
  constructor(_imports?: WorldImports);
22
- /** `physics` — whether the world needs rapier (config or a physics behaviour). */
23
- load(addons: AddonsConfig, options?: {
24
- physics?: boolean;
25
- }): Promise<void>;
26
- /** Resolves a require key: enabled lib or external accessor; undefined otherwise. */
24
+ /** `usedBuiltinUuids` — built-in behaviours on the scene; they may require libs. */
25
+ load(addons: AddonsConfig, usedBuiltinUuids?: readonly string[]): Promise<void>;
26
+ /** Resolves a require key: a lib or an external accessor; undefined otherwise. */
27
27
  resolve(key: string): unknown;
28
- /** The physics engine, loaded and initialized; `null` in worlds without physics. */
29
- get rapier(): typeof RAPIER | null;
28
+ /** The lib itself, for runtime code that knows what it needs (physics → rapier). */
29
+ lib<T>(id: LibId): T | undefined;
30
+ /** Libs bundled with the runtime: no importer, no config, always available. */
31
+ private _registerBuiltins;
30
32
  private _loadLibs;
31
- private _loadRapier;
33
+ private _register;
32
34
  private _loadExternals;
33
35
  private _mountScript;
34
36
  dispose(): void;
@@ -0,0 +1,20 @@
1
+ import * as THREE from "three/webgpu";
2
+ import * as TSL from "three/tsl";
3
+ import { LibId } from "./lib-registry";
4
+ /**
5
+ * Libraries the runtime bundles itself: `require("<key>")` hands these back in
6
+ * every world, no config and no importer involved. Metadata (keys, labels) is
7
+ * in `lib-registry`; the modules live here so the registry stays importable
8
+ * from the editor without dragging three & co along.
9
+ */
10
+ export declare const BUILTIN_LIB_MODULES: Partial<Record<LibId, unknown>>;
11
+ /**
12
+ * The subset of built-ins that is also a bare global in script scope — same
13
+ * objects as above, so `THREE === require("three")`. Typed literally: the
14
+ * script context contract depends on these exact shapes.
15
+ */
16
+ export declare const BUILTIN_GLOBALS: {
17
+ readonly THREE: typeof THREE;
18
+ readonly TSL: typeof TSL;
19
+ readonly van: import("vanjs-core").Van;
20
+ };
@@ -1,21 +1,36 @@
1
1
  /**
2
- * Registry of built-in libraries enabled via `settings.addons.json → libs`.
2
+ * Registry of the libraries scripts can `require`. One entry describes one
3
+ * library; the flags say how it gets into the world:
3
4
  *
4
- * Separates two keys: `id` (short config key under `libs`) and `requireKey`
5
- * (what the user writes in `require(...)` in scripts — and the import specifier
6
- * importers are built from). Single source for:
7
- * - game: the app's importer map (`core/game/settings/optional-imports`),
8
- * - export: the `imports.libs` block the build emits into the world module,
9
- * - editor: JSON schema (`libs` props) + require typings (`AddonsRequireTypings`).
5
+ * - **built-in** (`builtin: true`) — the runtime already bundles it, so it is
6
+ * available in every world with no config and no importer. Some of them are
7
+ * additionally exposed as a bare global (`globalName`) — that is the only
8
+ * difference between `THREE` and, say, `nanostores`.
9
+ * - **optional** — enabled in `settings.addons.json → libs`, or pulled in by
10
+ * something that declares it in `requiredLibs` (physics → rapier). The
11
+ * importer comes from the runtime owner (`WorldImports`), never from the
12
+ * package itself — see `./optional-imports.ts`.
10
13
  *
11
- * Adding a lib = add an entry here (+ importer in the app, + d.ts loader in editor).
12
- * The package itself never imports these — see `./optional-imports.ts`.
14
+ * `id` is the config key under `libs`, `requireKey` is what the user writes in
15
+ * `require(...)` (and the import specifier importers are built from). Single
16
+ * source for: the app's importer map, the `imports.libs` block of an exported
17
+ * world, the JSON schema and the editor's require typings.
13
18
  */
14
19
  export declare enum LibId {
20
+ Three = "three",
21
+ Tsl = "tsl",
22
+ ThreeStart = "threeStart",
23
+ Van = "van",
24
+ Nanostores = "nanostores",
25
+ EventEmitter = "eventemitter3",
26
+ Howler = "howler",
15
27
  Tween = "tween",
16
28
  Anime = "anime",
17
29
  Nipplejs = "nipplejs",
18
- Stats = "stats"
30
+ Stats = "stats",
31
+ Rapier = "rapier",
32
+ Css2d = "css2d",
33
+ Css3d = "css3d"
19
34
  }
20
35
  export interface LibDescriptor {
21
36
  readonly id: LibId;
@@ -25,10 +40,21 @@ export interface LibDescriptor {
25
40
  readonly label: string;
26
41
  /** npm package to install; defaults to `requireKey` (differs for subpaths). */
27
42
  readonly packageName?: string;
43
+ /** Bundled with the runtime: always available, no importer, nothing to install. */
44
+ readonly builtin?: boolean;
45
+ /** Built-in libs that are also bare globals in script scope. */
46
+ readonly globalName?: string;
28
47
  /** The package exposes its API as the default export — unwrap it on load. */
29
48
  readonly defaultExport?: boolean;
49
+ /** Post-import step, e.g. wasm init. Returns what `require` should hand back. */
50
+ readonly init?: (module: unknown) => Promise<unknown>;
30
51
  }
31
52
  export declare const LIBS: Readonly<Record<LibId, LibDescriptor>>;
32
53
  export declare const LIB_IDS: LibId[];
33
54
  export declare const LIB_DESCRIPTORS: LibDescriptor[];
55
+ /** Bundled with the runtime — available in every world. */
56
+ export declare const BUILTIN_LIB_DESCRIPTORS: LibDescriptor[];
57
+ /** Enabled in `settings.addons.json → libs` or required by a module/behaviour. */
58
+ export declare const OPTIONAL_LIB_DESCRIPTORS: LibDescriptor[];
59
+ export declare const OPTIONAL_LIB_IDS: LibId[];
34
60
  export declare function libByRequireKey(requireKey: string): LibDescriptor | undefined;
@@ -0,0 +1,9 @@
1
+ import type { LibId } from "./lib-registry";
2
+ import type { AddonsConfig } from "./types";
3
+ /**
4
+ * Libs the world needs beyond what `libs` lists: an enabled module or a built-in
5
+ * behaviour on the scene declares them itself (`requiredLibs`), so physics keeps
6
+ * working without a line in `settings.addons.json`. Same answer for the game and
7
+ * for the export — the build emits importers from it.
8
+ */
9
+ export declare function collectRequiredLibs(addons: AddonsConfig | undefined, usedBuiltinUuids?: readonly string[]): LibId[];
@@ -12,9 +12,9 @@ export type PackageImport = () => Promise<unknown>;
12
12
  * `imports` in `defineWorld`.
13
13
  */
14
14
  export interface WorldImports {
15
- /** `@dimforge/rapier3d-compat` — worlds with physics. */
16
- rapier?: PackageImport;
17
- /** Built-in libs by `LibId` (`settings.addons.json → libs`). */
15
+ /** Optional libs by `LibId` — enabled in `settings.addons.json → libs` or
16
+ * required by a module/behaviour (physics → rapier). Built-in libs are
17
+ * bundled with the runtime and need no importer. */
18
18
  libs?: Partial<Record<LibId, PackageImport>>;
19
19
  }
20
20
  /** `settings.addons.json` — pluggable modules, built-in libs, external scripts. */
@@ -1,14 +1,12 @@
1
+ import type { RequiredPatch } from "../registry";
1
2
  import type { RapierPhysics } from "../../modules/physics/RapierPhysics";
2
3
  import type { NormalizedPhysics } from "../../modules/physics/physics.module-descriptor";
3
- import type { ThreeStart } from "three-start";
4
4
  /** The only place the physics module is created — called by the registry
5
5
  * descriptor. The module itself takes rapier from `ctx.modules.addons` on awake. */
6
6
  export declare function createRapierPhysicsModule(config: NormalizedPhysics): Promise<RapierPhysics>;
7
7
  /**
8
- * Registers physics on demand: a built-in physics behaviour declares it via
9
- * `meta.requiredPatch`, so a world with a collider on the scene gets
10
- * `ctx.modules.rapier` even without a line in `settings.addons.json`.
11
- * Idempotent, and strictly before `start()` — three-start forbids `addModules`
12
- * afterwards.
8
+ * What a physics behaviour needs on top of its class: the rapier lib (loaded
9
+ * with the other addons, before the world starts) and the physics ctx module.
10
+ * A collider on the scene is enough — no line in `settings.addons.json`.
13
11
  */
14
- export declare function applyRapierPhysicsPatch(starter: ThreeStart): Promise<void>;
12
+ export declare const rapierPhysicsPatch: RequiredPatch;
@@ -1,4 +1,5 @@
1
1
  import type { Object3DBehaviourConstructor, ThreeStart } from "three-start";
2
+ import type { LibId } from "../addons/lib-registry";
2
3
  /**
3
4
  * Built-in behaviour: uuid is its identity in user worlds — it must never
4
5
  * change. The class loads lazily, so a world without physics doesn't pull rapier.
@@ -7,8 +8,14 @@ export interface BuiltinBehaviour {
7
8
  uuid: string;
8
9
  name: string;
9
10
  load: () => Promise<Object3DBehaviourConstructor>;
10
- /** Registers ctx modules the behaviour needs. Strictly before `start()`. */
11
- requiredPatch?: (starter: ThreeStart) => Promise<void>;
11
+ /** ctx modules (and their libs) the behaviour cannot work without. */
12
+ requiredPatch?: RequiredPatch;
13
+ }
14
+ /** What a behaviour needs beyond its own class: libs to load before the world
15
+ * starts, and ctx modules to register strictly before `start()`. */
16
+ export interface RequiredPatch {
17
+ requiredLibs?: LibId[];
18
+ apply: (starter: ThreeStart) => Promise<void>;
12
19
  }
13
20
  /** Registry of built-in behaviours by uuid — source of truth for both game and export. */
14
21
  export declare const BUILTIN_BEHAVIOURS: Record<string, BuiltinBehaviour | undefined>;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,9 @@ export type { InstantiateOptions, PrefabFactory } from "./WorldRuntime";
4
4
  export { setColor } from "./values";
5
5
  export { RUNTIME_VERSION, WORLD_FORMAT_VERSION } from "./version";
6
6
  export { WorldEvent } from "./world-events";
7
+ export { lazyProxy } from "./utils/lazy";
8
+ export { WorldStores } from "./world-stores";
9
+ export type { WorldStore } from "./world-stores";
7
10
  export { createWorldModules, EnvModule, HtmlHudModule, InputSystemModule, isMobileDevice, SoundsModule, UtilsModule, } from "./modules";
8
11
  export type { EnvMode, WorldModules } from "./modules";
9
12
  export { DEFAULT_RESOURCE_IDS, DefaultResourceId, getDefaultResource, isDefaultResourceId, } from "./resources/default-resources";
@@ -19,8 +22,8 @@ export { AudioSource, createAudio } from "./resources/audio";
19
22
  export { createVideo, VideoSource } from "./resources/video";
20
23
  export { createFont, FontSource, fontFormatFromMime } from "./resources/font";
21
24
  export { BUILTIN_BEHAVIOUR_LIST, BUILTIN_BEHAVIOURS, getBuiltinBehaviour, } from "./behaviours/registry";
22
- export type { BuiltinBehaviour } from "./behaviours/registry";
23
- export { applyRapierPhysicsPatch } from "./behaviours/physics/_required-patch";
25
+ export type { BuiltinBehaviour, RequiredPatch } from "./behaviours/registry";
26
+ export { rapierPhysicsPatch } from "./behaviours/physics/_required-patch";
24
27
  export { AddonsRuntime } from "./addons/AddonsRuntime";
25
28
  export { applyRendererConfig } from "./rendering/renderer-apply";
26
29
  export { enableSceneEmissiveMrt, SceneBloom } from "./rendering/SceneBloom";
@@ -28,7 +31,8 @@ export { createTslNode, TslNode } from "./rendering/tsl-node";
28
31
  export type { TslNodeBuilder } from "./rendering/tsl-node";
29
32
  export { createUniformNode, isUniformType, UNIFORM_DEFAULTS, UNIFORM_TYPES, uniformType, } from "./rendering/tsl-uniforms";
30
33
  export type { UniformType } from "./rendering/tsl-uniforms";
31
- export { LIB_DESCRIPTORS, LIB_IDS, LIBS, LibId, libByRequireKey } from "./addons/lib-registry";
34
+ export { BUILTIN_LIB_DESCRIPTORS, LIB_DESCRIPTORS, LIB_IDS, LIBS, LibId, OPTIONAL_LIB_DESCRIPTORS, OPTIONAL_LIB_IDS, libByRequireKey, } from "./addons/lib-registry";
35
+ export { collectRequiredLibs } from "./addons/required-libs";
32
36
  export type { LibDescriptor } from "./addons/lib-registry";
33
37
  export { OPTIONAL_MODULE_IDS, OptionalModuleId } from "./modules/optional-module-descriptor";
34
38
  export { OPTIONAL_MODULES, getOptionalModuleDescriptor } from "./modules/optional-modules";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { a as OPTIONAL_MODULE_IDS, i as normalizeChat, n as CHAT_INPUT_MODES, o as OptionalModuleId, r as chatModuleDescriptor } from "./chat.module-descriptor-BTsep-Kq.js";
2
+ import { a as LibId, c as libByRequireKey, i as LIB_IDS, n as LIBS, o as OPTIONAL_LIB_DESCRIPTORS, r as LIB_DESCRIPTORS, s as OPTIONAL_LIB_IDS, t as BUILTIN_LIB_DESCRIPTORS } from "./lib-registry-D1VyVPBg.js";
2
3
  import { n as getRadiusScale, t as getHalfheightRadiusScale } from "./get-scale-from-obj-B7JlVPni.js";
3
4
  import * as THREE from "three/webgpu";
4
5
  import { BufferGeometryLoader, LoadingManager, TextureLoader } from "three/webgpu";
@@ -7,10 +8,13 @@ import { ContextModule, ThreeContextEvents, ThreeStart } from "three-start";
7
8
  import * as TSL from "three/tsl";
8
9
  import { emissive, mrt, output } from "three/tsl";
9
10
  import van from "vanjs-core";
11
+ import * as howler from "howler";
10
12
  import { Howl, Howler } from "howler";
11
13
  import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
12
14
  import { clone } from "three/examples/jsm/utils/SkeletonUtils.js";
13
15
  import EventEmitter from "eventemitter3";
16
+ import * as nanostores from "nanostores";
17
+ import { atom } from "nanostores";
14
18
  //#region packages/world-runtime/src/world-events.ts
15
19
  /**
16
20
  * World lifecycle events, emitted on `game.emitter` (`world.events` in scripts)
@@ -46,6 +50,10 @@ var World = class extends ThreeStart {
46
50
  get events() {
47
51
  return this._runtime.emitter;
48
52
  }
53
+ /** Shared reactive state — the same one as `world.stores` in scripts. */
54
+ get stores() {
55
+ return this._runtime.stores;
56
+ }
49
57
  start() {
50
58
  const wasStarted = this.isStarted;
51
59
  super.start();
@@ -59,6 +67,26 @@ var World = class extends ThreeStart {
59
67
  };
60
68
  //#endregion
61
69
  //#region packages/world-runtime/src/utils/lazy.ts
70
+ /**
71
+ * Object that forwards every access to `resolve()`. For script globals that are
72
+ * shortcuts into something created later (`modules` → `ctx.modules`): the script
73
+ * bag is destructured eagerly, so a plain reference or a getter would be read
74
+ * before the target exists.
75
+ */
76
+ function lazyProxy(resolve) {
77
+ return new Proxy({}, {
78
+ get: (_target, key) => Reflect.get(resolve(), key),
79
+ has: (_target, key) => Reflect.has(resolve(), key),
80
+ ownKeys: () => Reflect.ownKeys(resolve()),
81
+ getOwnPropertyDescriptor: (_target, key) => {
82
+ const descriptor = Reflect.getOwnPropertyDescriptor(resolve(), key);
83
+ return descriptor && {
84
+ ...descriptor,
85
+ configurable: true
86
+ };
87
+ }
88
+ });
89
+ }
62
90
  /** Lazy cached value: the factory is called once on the first `get()`. */
63
91
  function lazy(factory) {
64
92
  let has = false;
@@ -414,18 +442,24 @@ var ControlsModule = class extends ContextModule {
414
442
  };
415
443
  //#endregion
416
444
  //#region packages/world-runtime/src/behaviours/physics/_required-patch.ts
417
- var lazyRapierPhysicsModule = lazy(async () => (await import("./RapierPhysics-zLwVZpwF.js")).default);
445
+ var lazyRapierPhysicsModule = lazy(async () => (await import("./RapierPhysics-CU6X--c7.js")).default);
418
446
  /** The only place the physics module is created — called by the registry
419
447
  * descriptor. The module itself takes rapier from `ctx.modules.addons` on awake. */
420
448
  async function createRapierPhysicsModule(config) {
421
449
  return new (await (lazyRapierPhysicsModule.get()))({ debug: config.debug });
422
450
  }
423
451
  /**
424
- * Registers physics on demand: a built-in physics behaviour declares it via
425
- * `meta.requiredPatch`, so a world with a collider on the scene gets
426
- * `ctx.modules.rapier` even without a line in `settings.addons.json`.
427
- * Idempotent, and strictly before `start()` — three-start forbids `addModules`
428
- * afterwards.
452
+ * What a physics behaviour needs on top of its class: the rapier lib (loaded
453
+ * with the other addons, before the world starts) and the physics ctx module.
454
+ * A collider on the scene is enough — no line in `settings.addons.json`.
455
+ */
456
+ var rapierPhysicsPatch = {
457
+ requiredLibs: [LibId.Rapier],
458
+ apply: (starter) => applyRapierPhysicsPatch(starter)
459
+ };
460
+ /**
461
+ * Registers the physics module. Idempotent, and strictly before `start()` —
462
+ * three-start forbids `addModules` afterwards.
429
463
  */
430
464
  async function applyRapierPhysicsPatch(starter) {
431
465
  if (starter.ctx.modules.rapier) return;
@@ -456,6 +490,7 @@ var physicsModuleDescriptor = {
456
490
  debug: false
457
491
  }),
458
492
  create: (config) => createRapierPhysicsModule(config),
493
+ requiredLibs: [LibId.Rapier],
459
494
  schema: {
460
495
  description: "Physics engine module. false | \"rapier\" | { engine, debug }.",
461
496
  oneOf: [
@@ -531,19 +566,25 @@ var EnvModule = class extends ContextModule {
531
566
  //#endregion
532
567
  //#region packages/world-runtime/src/modules/HtmlHudModule.ts
533
568
  var { div } = van.tags;
569
+ var LAYER_STYLE = "position:fixed;inset:0;pointer-events:none;";
534
570
  var HtmlHudModule = class extends ContextModule {
535
- root;
571
+ overlay;
572
+ underlay;
536
573
  crosshair;
537
574
  constructor() {
538
575
  super();
539
576
  this.crosshair = div({
540
- id: "kvyverse-game-gui-crosshair",
577
+ id: "kvy-crosshair",
541
578
  style: "position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);"
542
579
  });
543
- this.root = div({
544
- id: "kvyverse-game-gui-root",
545
- style: "position:fixed;inset:0;pointer-events:none;"
580
+ this.overlay = div({
581
+ id: "kvy-overlay",
582
+ style: LAYER_STYLE
546
583
  }, this.crosshair);
584
+ this.underlay = div({
585
+ id: "kvy-underlay",
586
+ style: LAYER_STYLE
587
+ });
547
588
  }
548
589
  onAwake() {
549
590
  const ctx = this.ctx;
@@ -551,8 +592,13 @@ var HtmlHudModule = class extends ContextModule {
551
592
  ctx.on(ThreeContextEvents.Mount, this.onMount);
552
593
  ctx.on(ThreeContextEvents.Unmount, this.onUnmount);
553
594
  }
595
+ /** Appends nodes over the canvas (the HUD). */
554
596
  append(...nodes) {
555
- this.root.append(...nodes);
597
+ this.overlay.append(...nodes);
598
+ }
599
+ /** Appends nodes under the canvas — visible only through a transparent canvas. */
600
+ appendUnderlay(...nodes) {
601
+ this.underlay.append(...nodes);
556
602
  }
557
603
  setCrosshair(innerHtml) {
558
604
  this.crosshair.innerHTML = innerHtml;
@@ -563,10 +609,14 @@ var HtmlHudModule = class extends ContextModule {
563
609
  this.ctx.off(ThreeContextEvents.Unmount, this.onUnmount);
564
610
  }
565
611
  onMount = (container) => {
566
- container.append(this.root);
612
+ container.prepend(this.underlay);
613
+ container.append(this.overlay);
614
+ const canvas = this.ctx.renderer.domElement;
615
+ if (!canvas.style.position) canvas.style.position = "relative";
567
616
  };
568
617
  onUnmount = () => {
569
- this.root.remove();
618
+ this.overlay.remove();
619
+ this.underlay.remove();
570
620
  };
571
621
  };
572
622
  //#endregion
@@ -921,55 +971,55 @@ var entries = [
921
971
  uuid: "rrb",
922
972
  name: "Rigidbody",
923
973
  load: () => loadDefault(import("./Rigidbody-DLhN8ddv.js").then((n) => n.n)),
924
- requiredPatch: applyRapierPhysicsPatch
974
+ requiredPatch: rapierPhysicsPatch
925
975
  },
926
976
  {
927
977
  uuid: "rapierballcol",
928
978
  name: "BallCollider",
929
979
  load: () => loadDefault(import("./BallCollider-3L5vOrcL.js")),
930
- requiredPatch: applyRapierPhysicsPatch
980
+ requiredPatch: rapierPhysicsPatch
931
981
  },
932
982
  {
933
983
  uuid: "rapiercapscol",
934
984
  name: "CapsuleCollider",
935
985
  load: () => loadDefault(import("./CapsuleCollider-CBTsR-Ys.js")),
936
- requiredPatch: applyRapierPhysicsPatch
986
+ requiredPatch: rapierPhysicsPatch
937
987
  },
938
988
  {
939
989
  uuid: "rapierconecol",
940
990
  name: "ConeCollider",
941
991
  load: () => loadDefault(import("./ConeCollider-C3XU3Y9X.js")),
942
- requiredPatch: applyRapierPhysicsPatch
992
+ requiredPatch: rapierPhysicsPatch
943
993
  },
944
994
  {
945
995
  uuid: "rapierconvxcol",
946
996
  name: "ConvexMeshCollider",
947
997
  load: () => loadDefault(import("./ConvexMeshCollider-DzNV4HTO.js")),
948
- requiredPatch: applyRapierPhysicsPatch
998
+ requiredPatch: rapierPhysicsPatch
949
999
  },
950
1000
  {
951
1001
  uuid: "rapiercubcol",
952
1002
  name: "CuboidCollider",
953
1003
  load: () => loadDefault(import("./CuboidCollider-Dp1DZoFM.js")),
954
- requiredPatch: applyRapierPhysicsPatch
1004
+ requiredPatch: rapierPhysicsPatch
955
1005
  },
956
1006
  {
957
1007
  uuid: "rapiercylcol",
958
1008
  name: "CylinderCollider",
959
1009
  load: () => loadDefault(import("./CylinderCollider-C0h_nhB5.js")),
960
- requiredPatch: applyRapierPhysicsPatch
1010
+ requiredPatch: rapierPhysicsPatch
961
1011
  },
962
1012
  {
963
1013
  uuid: "rapiertricol",
964
1014
  name: "TrimeshCollider",
965
1015
  load: () => loadDefault(import("./TrimeshCollider-7dIs_xtZ.js")),
966
- requiredPatch: applyRapierPhysicsPatch
1016
+ requiredPatch: rapierPhysicsPatch
967
1017
  },
968
1018
  {
969
1019
  uuid: "rapierfpsplayercontroller",
970
1020
  name: "RapierFpsPlayerController",
971
1021
  load: () => loadDefault(import("./RapierFpsPlayerController-DthD8dw9.js")),
972
- requiredPatch: applyRapierPhysicsPatch
1022
+ requiredPatch: rapierPhysicsPatch
973
1023
  }
974
1024
  ];
975
1025
  /** Registry of built-in behaviours by uuid — source of truth for both game and export. */
@@ -1044,122 +1094,123 @@ function createFont(url, family, mime, name) {
1044
1094
  return source;
1045
1095
  }
1046
1096
  //#endregion
1047
- //#region packages/world-runtime/src/addons/lib-registry.ts
1097
+ //#region packages/world-runtime/src/addons/builtin-libs.ts
1048
1098
  /**
1049
- * Registry of built-in libraries enabled via `settings.addons.json → libs`.
1050
- *
1051
- * Separates two keys: `id` (short config key under `libs`) and `requireKey`
1052
- * (what the user writes in `require(...)` in scripts — and the import specifier
1053
- * importers are built from). Single source for:
1054
- * - game: the app's importer map (`core/game/settings/optional-imports`),
1055
- * - export: the `imports.libs` block the build emits into the world module,
1056
- * - editor: JSON schema (`libs` props) + require typings (`AddonsRequireTypings`).
1057
- *
1058
- * Adding a lib = add an entry here (+ importer in the app, + d.ts loader in editor).
1059
- * The package itself never imports these — see `./optional-imports.ts`.
1099
+ * Libraries the runtime bundles itself: `require("<key>")` hands these back in
1100
+ * every world, no config and no importer involved. Metadata (keys, labels) is
1101
+ * in `lib-registry`; the modules live here so the registry stays importable
1102
+ * from the editor without dragging three & co along.
1060
1103
  */
1061
- var LibId = /* @__PURE__ */ function(LibId) {
1062
- LibId["Tween"] = "tween";
1063
- LibId["Anime"] = "anime";
1064
- LibId["Nipplejs"] = "nipplejs";
1065
- LibId["Stats"] = "stats";
1066
- return LibId;
1067
- }({});
1068
- var LIBS = {
1069
- ["tween"]: {
1070
- id: "tween",
1071
- requireKey: "@tweenjs/tween.js",
1072
- label: "Tween.js"
1073
- },
1074
- ["anime"]: {
1075
- id: "anime",
1076
- requireKey: "animejs/animation",
1077
- packageName: "animejs",
1078
- label: "Anime.js (animate)"
1079
- },
1080
- ["nipplejs"]: {
1081
- id: "nipplejs",
1082
- requireKey: "nipplejs",
1083
- label: "nipplejs (on-screen joystick)",
1084
- defaultExport: true
1085
- },
1086
- ["stats"]: {
1087
- id: "stats",
1088
- requireKey: "stats.js",
1089
- label: "Stats.js (fps overlay)",
1090
- defaultExport: true
1091
- }
1104
+ var BUILTIN_LIB_MODULES = {
1105
+ [LibId.Three]: THREE,
1106
+ [LibId.Tsl]: TSL,
1107
+ [LibId.ThreeStart]: ThreeStartNS,
1108
+ [LibId.Van]: van,
1109
+ [LibId.Nanostores]: nanostores,
1110
+ [LibId.EventEmitter]: EventEmitter,
1111
+ [LibId.Howler]: howler
1092
1112
  };
1093
- var LIB_IDS = Object.values(LibId);
1094
- var LIB_DESCRIPTORS = LIB_IDS.map((id) => LIBS[id]);
1095
- function libByRequireKey(requireKey) {
1096
- return LIB_DESCRIPTORS.find((l) => l.requireKey === requireKey);
1113
+ /**
1114
+ * The subset of built-ins that is also a bare global in script scope — same
1115
+ * objects as above, so `THREE === require("three")`. Typed literally: the
1116
+ * script context contract depends on these exact shapes.
1117
+ */
1118
+ var BUILTIN_GLOBALS = {
1119
+ THREE,
1120
+ TSL,
1121
+ van
1122
+ };
1123
+ //#endregion
1124
+ //#region packages/world-runtime/src/addons/required-libs.ts
1125
+ /**
1126
+ * Libs the world needs beyond what `libs` lists: an enabled module or a built-in
1127
+ * behaviour on the scene declares them itself (`requiredLibs`), so physics keeps
1128
+ * working without a line in `settings.addons.json`. Same answer for the game and
1129
+ * for the export — the build emits importers from it.
1130
+ */
1131
+ function collectRequiredLibs(addons, usedBuiltinUuids = []) {
1132
+ const required = /* @__PURE__ */ new Set();
1133
+ const configured = addons?.libs ?? {};
1134
+ for (const id of Object.keys(configured)) if (configured[id] && LIB_IDS.includes(id) && !LIBS[id].builtin) required.add(id);
1135
+ for (const descriptor of OPTIONAL_MODULES) {
1136
+ if (!descriptor.requiredLibs?.length) continue;
1137
+ if (descriptor.normalize(addons?.modules?.[descriptor.id]) === null) continue;
1138
+ for (const id of descriptor.requiredLibs) required.add(id);
1139
+ }
1140
+ for (const uuid of usedBuiltinUuids) {
1141
+ const libs = getBuiltinBehaviour(uuid)?.requiredPatch?.requiredLibs;
1142
+ if (libs) for (const id of libs) required.add(id);
1143
+ }
1144
+ return [...required];
1097
1145
  }
1098
1146
  //#endregion
1099
1147
  //#region packages/world-runtime/src/addons/AddonsRuntime.ts
1100
1148
  /**
1101
- * External dependencies of the world: built-in libs, externals and the physics
1102
- * engine. Loaded before the world starts; scripts reach them through
1103
- * `resolve(key)` (`require`), the physics module through `rapier`.
1149
+ * External dependencies of the world: libs and external scripts. Built-in libs
1150
+ * are registered right away (the runtime bundles them), optional ones are loaded
1151
+ * before the world starts — enabled in `settings.addons.json → libs` or declared
1152
+ * by a module/behaviour through `requiredLibs`. Scripts reach all of them by
1153
+ * `require("<key>")`.
1104
1154
  *
1105
1155
  * This is the `addons` ctx module, but it also works without a ctx: launcher
1106
1156
  * worlds have no 3d context while `require` still has to work, so there the
1107
- * instance is used directly. How to import packages is up to the runtime owner
1108
- * (see `WorldImports`).
1157
+ * instance is used directly. How to import optional packages is up to the
1158
+ * runtime owner (see `WorldImports`).
1109
1159
  */
1110
1160
  var AddonsRuntime = class extends ContextModule {
1111
1161
  _imports;
1112
1162
  /** libs — by requireKey (`@tweenjs/tween.js`), externals — by accessor (`Tone`). */
1113
1163
  _byKey = /* @__PURE__ */ new Map();
1164
+ _byLibId = /* @__PURE__ */ new Map();
1114
1165
  _scriptTags = [];
1115
- _rapier = null;
1116
1166
  constructor(_imports = {}) {
1117
1167
  super();
1118
1168
  this._imports = _imports;
1169
+ this._registerBuiltins();
1119
1170
  }
1120
- /** `physics` — whether the world needs rapier (config or a physics behaviour). */
1121
- async load(addons, options = {}) {
1122
- await this._loadLibs(addons.libs);
1171
+ /** `usedBuiltinUuids` — built-in behaviours on the scene; they may require libs. */
1172
+ async load(addons, usedBuiltinUuids = []) {
1173
+ await this._loadLibs(collectRequiredLibs(addons, usedBuiltinUuids));
1123
1174
  await this._loadExternals(addons.externals);
1124
- if (options.physics) await this._loadRapier();
1125
1175
  }
1126
- /** Resolves a require key: enabled lib or external accessor; undefined otherwise. */
1176
+ /** Resolves a require key: a lib or an external accessor; undefined otherwise. */
1127
1177
  resolve(key) {
1128
1178
  return this._byKey.get(key);
1129
1179
  }
1130
- /** The physics engine, loaded and initialized; `null` in worlds without physics. */
1131
- get rapier() {
1132
- return this._rapier;
1180
+ /** The lib itself, for runtime code that knows what it needs (physics → rapier). */
1181
+ lib(id) {
1182
+ return this._byLibId.get(id);
1183
+ }
1184
+ /** Libs bundled with the runtime: no importer, no config, always available. */
1185
+ _registerBuiltins() {
1186
+ for (const descriptor of BUILTIN_LIB_DESCRIPTORS) {
1187
+ const module = BUILTIN_LIB_MODULES[descriptor.id];
1188
+ if (module === void 0) {
1189
+ console.error(`[addons] built-in lib "${descriptor.id}" has no module`);
1190
+ continue;
1191
+ }
1192
+ this._register(descriptor.id, module);
1193
+ }
1133
1194
  }
1134
- async _loadLibs(libs) {
1135
- if (!libs) return;
1136
- for (const id of LIB_IDS) {
1137
- if (!libs[id]) continue;
1195
+ async _loadLibs(ids) {
1196
+ for (const id of ids) {
1197
+ const descriptor = LIBS[id];
1138
1198
  const load = this._imports.libs?.[id];
1139
1199
  if (!load) {
1140
- console.error(`[addons] lib "${id}" is enabled but has no importer`);
1200
+ console.error(`[addons] lib "${id}" is needed but has no importer`);
1141
1201
  continue;
1142
1202
  }
1143
1203
  try {
1144
- this._byKey.set(LIBS[id].requireKey, unwrapLib(id, await load()));
1204
+ const module = await load();
1205
+ this._register(id, unwrapLib(id, await (descriptor.init?.(module) ?? module)));
1145
1206
  } catch (e) {
1146
1207
  console.error(`[addons] failed to load lib "${id}"`, e);
1147
1208
  }
1148
1209
  }
1149
1210
  }
1150
- async _loadRapier() {
1151
- const load = this._imports.rapier;
1152
- if (!load) {
1153
- console.error("[addons] the world needs physics but has no importer for \"@dimforge/rapier3d-compat\"");
1154
- return;
1155
- }
1156
- try {
1157
- const rapier = await load();
1158
- await rapier.init();
1159
- this._rapier = rapier;
1160
- } catch (e) {
1161
- console.error("[addons] failed to load rapier", e);
1162
- }
1211
+ _register(id, value) {
1212
+ this._byLibId.set(id, value);
1213
+ this._byKey.set(LIBS[id].requireKey, value);
1163
1214
  }
1164
1215
  async _loadExternals(externals) {
1165
1216
  if (!externals) return;
@@ -1190,7 +1241,7 @@ var AddonsRuntime = class extends ContextModule {
1190
1241
  for (const tag of this._scriptTags) tag.remove();
1191
1242
  this._scriptTags.length = 0;
1192
1243
  this._byKey.clear();
1193
- this._rapier = null;
1244
+ this._byLibId.clear();
1194
1245
  }
1195
1246
  };
1196
1247
  /** A package like nipplejs exposes its API as the default export — that is what
@@ -1200,6 +1251,67 @@ function unwrapLib(id, module) {
1200
1251
  return module.default ?? module;
1201
1252
  }
1202
1253
  //#endregion
1254
+ //#region packages/world-runtime/src/world-stores.ts
1255
+ /**
1256
+ * Shared reactive key-value state of a world (`world.stores` in scripts) — the
1257
+ * state counterpart of the `world.events` bus. Stores are created lazily on
1258
+ * first touch; `undefined` means "no value", so `delete`/`clear` only reset
1259
+ * values and never invalidate a reference taken from `of()`.
1260
+ */
1261
+ var WorldStores = class {
1262
+ _stores = /* @__PURE__ */ new Map();
1263
+ /** The store behind a key, created on first touch. */
1264
+ of(key) {
1265
+ let store = this._stores.get(key);
1266
+ if (!store) {
1267
+ store = atom(void 0);
1268
+ this._stores.set(key, store);
1269
+ }
1270
+ return store;
1271
+ }
1272
+ get(key) {
1273
+ return this.of(key).get();
1274
+ }
1275
+ set(key, value) {
1276
+ this.of(key).set(value);
1277
+ }
1278
+ /** Subscribe to a key; returns unsubscribe. Not called with the current value. */
1279
+ listen(key, listener) {
1280
+ return this.of(key).listen(listener);
1281
+ }
1282
+ /** Same as `listen`, but also fires immediately with the current value. */
1283
+ subscribe(key, listener) {
1284
+ return this.of(key).subscribe(listener);
1285
+ }
1286
+ /** A value has been written to the key and not deleted since. */
1287
+ has(key) {
1288
+ return this._stores.get(key)?.get() !== void 0;
1289
+ }
1290
+ /** Keys that currently hold a value. */
1291
+ keys() {
1292
+ const keys = [];
1293
+ for (const [key, store] of this._stores) if (store.get() !== void 0) keys.push(key);
1294
+ return keys;
1295
+ }
1296
+ /** Plain object with all current values — for debugging and saves. */
1297
+ snapshot() {
1298
+ const result = {};
1299
+ for (const [key, store] of this._stores) {
1300
+ const value = store.get();
1301
+ if (value !== void 0) result[key] = value;
1302
+ }
1303
+ return result;
1304
+ }
1305
+ /** Resets the key to `undefined`; listeners are notified. */
1306
+ delete(key) {
1307
+ this._stores.get(key)?.set(void 0);
1308
+ }
1309
+ /** Resets every key; listeners are notified. */
1310
+ clear() {
1311
+ for (const store of this._stores.values()) store.set(void 0);
1312
+ }
1313
+ };
1314
+ //#endregion
1203
1315
  //#region packages/world-runtime/src/WorldRuntime.ts
1204
1316
  /**
1205
1317
  * Owner of the world's runtime state: assets, script execution, ctx access.
@@ -1210,6 +1322,8 @@ var WorldRuntime = class {
1210
1322
  options;
1211
1323
  /** World lifecycle event bus (`world.events` in scripts). */
1212
1324
  emitter = new EventEmitter();
1325
+ /** Shared reactive state of the world (`world.stores` in scripts). */
1326
+ stores = new WorldStores();
1213
1327
  _addons;
1214
1328
  _executedScripts = /* @__PURE__ */ new Map();
1215
1329
  _builtins = /* @__PURE__ */ new Map();
@@ -1242,14 +1356,9 @@ var WorldRuntime = class {
1242
1356
  setBloom(bloom) {
1243
1357
  this._bloom = bloom;
1244
1358
  }
1245
- /** @internal Built-in libs, externals and the physics engine, if the world needs one. */
1359
+ /** @internal Libs (config + required by modules/behaviours) and externals. */
1246
1360
  async loadAddons() {
1247
- await this._addons.load(this.definition.addons ?? {}, { physics: this._usesPhysics() });
1248
- }
1249
- /** Physics is on in the config, or a built-in behaviour on the scene needs it. */
1250
- _usesPhysics() {
1251
- if (normalizePhysics(this.definition.addons?.modules?.physics)) return true;
1252
- return (this.definition.usedBuiltins ?? []).some((uuid) => getBuiltinBehaviour(uuid)?.requiredPatch);
1361
+ await this._addons.load(this.definition.addons ?? {}, this.definition.usedBuiltins ?? []);
1253
1362
  }
1254
1363
  /** @internal World resources by asset path — result of `loadAssets`. */
1255
1364
  setResources(resources) {
@@ -1306,7 +1415,7 @@ var WorldRuntime = class {
1306
1415
  const patch = getBuiltinBehaviour(uuid)?.requiredPatch;
1307
1416
  if (!patch) continue;
1308
1417
  try {
1309
- await patch(starter);
1418
+ await patch.apply(starter);
1310
1419
  } catch (error) {
1311
1420
  console.error(`Required patch of "${uuid}" failed`, error);
1312
1421
  }
@@ -1380,6 +1489,7 @@ var WorldRuntime = class {
1380
1489
  this._styleElements.length = 0;
1381
1490
  this._executedScripts.clear();
1382
1491
  this.emitter.removeAllListeners();
1492
+ this.stores.clear();
1383
1493
  }
1384
1494
  /** Narrow bloom facade for scripts: mode is read-only, params are live
1385
1495
  * (uniforms, can be animated per frame). `null` — bloom is disabled. */
@@ -1425,20 +1535,16 @@ var WorldRuntime = class {
1425
1535
  },
1426
1536
  events: this.emitter,
1427
1537
  emitter: this.emitter,
1538
+ stores: this.stores,
1428
1539
  rendering: { get bloom() {
1429
1540
  return runtime.bloomApi;
1430
1541
  } }
1431
1542
  },
1543
+ modules: lazyProxy(() => runtime.ctx.modules),
1432
1544
  require: this.require,
1433
1545
  instantiate: this.instantiate,
1434
1546
  getUrl: this.getUrl,
1435
- THREE,
1436
- TSL,
1437
- van,
1438
- /** Physics engine global; null in worlds without physics. */
1439
- get RAPIER() {
1440
- return runtime.addons.rapier;
1441
- }
1547
+ ...BUILTIN_GLOBALS
1442
1548
  };
1443
1549
  return this._scriptContext;
1444
1550
  }
@@ -1460,7 +1566,7 @@ function isAbsoluteUrl(url) {
1460
1566
  }
1461
1567
  //#endregion
1462
1568
  //#region packages/world-runtime/src/version.gen.ts
1463
- var RUNTIME_VERSION = "0.3.2";
1569
+ var RUNTIME_VERSION = "0.4.2";
1464
1570
  //#endregion
1465
1571
  //#region packages/world-runtime/src/version.ts
1466
1572
  /**
@@ -1990,4 +2096,4 @@ var TONE_MAPPING_NAMES = [
1990
2096
  "Neutral"
1991
2097
  ];
1992
2098
  //#endregion
1993
- export { AddonsRuntime, AssetLoaders, AudioSource, BLOOM_MODES, BUILTIN_BEHAVIOURS, BUILTIN_BEHAVIOUR_LIST, CHAT_INPUT_MODES, DEFAULT_RESOURCE_IDS, DefaultResourceId, EXTERNAL_TYPES, EnvModule, FontSource, HtmlHudModule, InputSystemModule, LIBS, LIB_DESCRIPTORS, LIB_IDS, LOADING_MANAGER, LibId, OPTIONAL_MODULES, OPTIONAL_MODULE_IDS, OptionalModuleId, PHYSICS_ENGINES, RUNTIME_VERSION, SHADOW_MAP_TYPES, SceneBloom, SoundsModule, TONE_MAPPING_NAMES, TslNode, UNIFORM_DEFAULTS, UNIFORM_TYPES, UtilsModule, VideoSource, WORLD_FORMAT_VERSION, World, WorldEvent, applyRapierPhysicsPatch, applyRendererConfig, applyTextureDefaults, collectModelSubResources, configureAssetLoaders, createAudio, createFont, createTslNode, createUniformNode, createVideo, createWorldModules, defineWorld, enableSceneEmissiveMrt, fontFormatFromMime, getBuiltinBehaviour, getDefaultResource, getHalfheightRadiusScale, getOptionalModuleDescriptor, getRadiusScale, isDefaultResourceId, isMobileDevice, isUniformType, libByRequireKey, loadGeometry, loadHdr, loadModel, loadTexture, normalizeChat, normalizePhysics, parseGeometryJson, parseHdr, parseModel, setColor, uniformType };
2099
+ export { AddonsRuntime, AssetLoaders, AudioSource, BLOOM_MODES, BUILTIN_BEHAVIOURS, BUILTIN_BEHAVIOUR_LIST, BUILTIN_LIB_DESCRIPTORS, CHAT_INPUT_MODES, DEFAULT_RESOURCE_IDS, DefaultResourceId, EXTERNAL_TYPES, EnvModule, FontSource, HtmlHudModule, InputSystemModule, LIBS, LIB_DESCRIPTORS, LIB_IDS, LOADING_MANAGER, LibId, OPTIONAL_LIB_DESCRIPTORS, OPTIONAL_LIB_IDS, OPTIONAL_MODULES, OPTIONAL_MODULE_IDS, OptionalModuleId, PHYSICS_ENGINES, RUNTIME_VERSION, SHADOW_MAP_TYPES, SceneBloom, SoundsModule, TONE_MAPPING_NAMES, TslNode, UNIFORM_DEFAULTS, UNIFORM_TYPES, UtilsModule, VideoSource, WORLD_FORMAT_VERSION, World, WorldEvent, WorldStores, applyRendererConfig, applyTextureDefaults, collectModelSubResources, collectRequiredLibs, configureAssetLoaders, createAudio, createFont, createTslNode, createUniformNode, createVideo, createWorldModules, defineWorld, enableSceneEmissiveMrt, fontFormatFromMime, getBuiltinBehaviour, getDefaultResource, getHalfheightRadiusScale, getOptionalModuleDescriptor, getRadiusScale, isDefaultResourceId, isMobileDevice, isUniformType, lazyProxy, libByRequireKey, loadGeometry, loadHdr, loadModel, loadTexture, normalizeChat, normalizePhysics, parseGeometryJson, parseHdr, parseModel, rapierPhysicsPatch, setColor, uniformType };
@@ -0,0 +1,142 @@
1
+ //#region packages/world-runtime/src/addons/lib-registry.ts
2
+ /**
3
+ * Registry of the libraries scripts can `require`. One entry describes one
4
+ * library; the flags say how it gets into the world:
5
+ *
6
+ * - **built-in** (`builtin: true`) — the runtime already bundles it, so it is
7
+ * available in every world with no config and no importer. Some of them are
8
+ * additionally exposed as a bare global (`globalName`) — that is the only
9
+ * difference between `THREE` and, say, `nanostores`.
10
+ * - **optional** — enabled in `settings.addons.json → libs`, or pulled in by
11
+ * something that declares it in `requiredLibs` (physics → rapier). The
12
+ * importer comes from the runtime owner (`WorldImports`), never from the
13
+ * package itself — see `./optional-imports.ts`.
14
+ *
15
+ * `id` is the config key under `libs`, `requireKey` is what the user writes in
16
+ * `require(...)` (and the import specifier importers are built from). Single
17
+ * source for: the app's importer map, the `imports.libs` block of an exported
18
+ * world, the JSON schema and the editor's require typings.
19
+ */
20
+ var LibId = /* @__PURE__ */ function(LibId) {
21
+ LibId["Three"] = "three";
22
+ LibId["Tsl"] = "tsl";
23
+ LibId["ThreeStart"] = "threeStart";
24
+ LibId["Van"] = "van";
25
+ LibId["Nanostores"] = "nanostores";
26
+ LibId["EventEmitter"] = "eventemitter3";
27
+ LibId["Howler"] = "howler";
28
+ LibId["Tween"] = "tween";
29
+ LibId["Anime"] = "anime";
30
+ LibId["Nipplejs"] = "nipplejs";
31
+ LibId["Stats"] = "stats";
32
+ LibId["Rapier"] = "rapier";
33
+ LibId["Css2d"] = "css2d";
34
+ LibId["Css3d"] = "css3d";
35
+ return LibId;
36
+ }({});
37
+ var LIBS = {
38
+ ["three"]: {
39
+ id: "three",
40
+ requireKey: "three",
41
+ label: "three.js (WebGPU build)",
42
+ builtin: true,
43
+ globalName: "THREE"
44
+ },
45
+ ["tsl"]: {
46
+ id: "tsl",
47
+ requireKey: "three/tsl",
48
+ packageName: "three",
49
+ label: "Three Shading Language",
50
+ builtin: true,
51
+ globalName: "TSL"
52
+ },
53
+ ["threeStart"]: {
54
+ id: "threeStart",
55
+ requireKey: "three-start",
56
+ label: "three-start (engine core)",
57
+ builtin: true
58
+ },
59
+ ["van"]: {
60
+ id: "van",
61
+ requireKey: "vanjs-core",
62
+ label: "VanJS (HTML UI)",
63
+ builtin: true,
64
+ globalName: "van",
65
+ defaultExport: true
66
+ },
67
+ ["nanostores"]: {
68
+ id: "nanostores",
69
+ requireKey: "nanostores",
70
+ label: "nanostores (atoms behind world.stores)",
71
+ builtin: true
72
+ },
73
+ ["eventemitter3"]: {
74
+ id: "eventemitter3",
75
+ requireKey: "eventemitter3",
76
+ label: "EventEmitter3",
77
+ builtin: true,
78
+ defaultExport: true
79
+ },
80
+ ["howler"]: {
81
+ id: "howler",
82
+ requireKey: "howler",
83
+ label: "howler.js (audio)",
84
+ builtin: true
85
+ },
86
+ ["tween"]: {
87
+ id: "tween",
88
+ requireKey: "@tweenjs/tween.js",
89
+ label: "Tween.js"
90
+ },
91
+ ["anime"]: {
92
+ id: "anime",
93
+ requireKey: "animejs/animation",
94
+ packageName: "animejs",
95
+ label: "Anime.js (animate)"
96
+ },
97
+ ["nipplejs"]: {
98
+ id: "nipplejs",
99
+ requireKey: "nipplejs",
100
+ label: "nipplejs (on-screen joystick)",
101
+ defaultExport: true
102
+ },
103
+ ["stats"]: {
104
+ id: "stats",
105
+ requireKey: "stats.js",
106
+ label: "Stats.js (fps overlay)",
107
+ defaultExport: true
108
+ },
109
+ ["rapier"]: {
110
+ id: "rapier",
111
+ requireKey: "@dimforge/rapier3d-compat",
112
+ label: "Rapier (physics engine)",
113
+ init: async (module) => {
114
+ await module.init();
115
+ return module;
116
+ }
117
+ },
118
+ ["css2d"]: {
119
+ id: "css2d",
120
+ requireKey: "three/addons/renderers/CSS2DRenderer.js",
121
+ packageName: "three",
122
+ label: "CSS2DRenderer (DOM labels at 3d positions)"
123
+ },
124
+ ["css3d"]: {
125
+ id: "css3d",
126
+ requireKey: "three/addons/renderers/CSS3DRenderer.js",
127
+ packageName: "three",
128
+ label: "CSS3DRenderer (DOM elements in 3d)"
129
+ }
130
+ };
131
+ var LIB_IDS = Object.values(LibId);
132
+ var LIB_DESCRIPTORS = LIB_IDS.map((id) => LIBS[id]);
133
+ /** Bundled with the runtime — available in every world. */
134
+ var BUILTIN_LIB_DESCRIPTORS = LIB_DESCRIPTORS.filter((lib) => lib.builtin);
135
+ /** Enabled in `settings.addons.json → libs` or required by a module/behaviour. */
136
+ var OPTIONAL_LIB_DESCRIPTORS = LIB_DESCRIPTORS.filter((lib) => !lib.builtin);
137
+ var OPTIONAL_LIB_IDS = OPTIONAL_LIB_DESCRIPTORS.map((lib) => lib.id);
138
+ function libByRequireKey(requireKey) {
139
+ return LIB_DESCRIPTORS.find((l) => l.requireKey === requireKey);
140
+ }
141
+ //#endregion
142
+ export { LibId as a, libByRequireKey as c, LIB_IDS as i, LIBS as n, OPTIONAL_LIB_DESCRIPTORS as o, LIB_DESCRIPTORS as r, OPTIONAL_LIB_IDS as s, BUILTIN_LIB_DESCRIPTORS as t };
@@ -1,11 +1,15 @@
1
1
  import { ContextModule } from "three-start";
2
2
  import type { HtmlModuleApi } from "./contracts/html.module-api";
3
3
  export declare class HtmlHudModule extends ContextModule implements HtmlModuleApi {
4
- readonly root: HTMLDivElement;
4
+ readonly overlay: HTMLDivElement;
5
+ readonly underlay: HTMLDivElement;
5
6
  readonly crosshair: HTMLDivElement;
6
7
  constructor();
7
8
  onAwake(): void;
9
+ /** Appends nodes over the canvas (the HUD). */
8
10
  append(...nodes: Array<Node | string>): void;
11
+ /** Appends nodes under the canvas — visible only through a transparent canvas. */
12
+ appendUnderlay(...nodes: Array<Node | string>): void;
9
13
  setCrosshair(innerHtml: string): void;
10
14
  /** three-start ContextModule has no onDestroy; teardown is explicit. */
11
15
  dispose(): void;
@@ -1,15 +1,15 @@
1
- import type RAPIER from "@dimforge/rapier3d-compat";
1
+ import type { LibId } from "../../addons/lib-registry";
2
2
  /**
3
- * Public API of the `addons` module — external dependencies of the world:
4
- * built-in libs and external scripts enabled in `settings.addons.json`, loaded
5
- * before the world starts.
3
+ * Public API of the `addons` module — the world's libraries and external
4
+ * scripts: built-in ones, those enabled in `settings.addons.json` and those
5
+ * required by an enabled module or a behaviour on the scene.
6
6
  *
7
7
  * Scripts normally reach a lib by `require("<key>")`; this module is the same
8
- * bag behind that call, plus the physics engine.
8
+ * bag behind that call.
9
9
  */
10
10
  export interface AddonsModuleApi {
11
- /** Value by require key (a lib) or by accessor (an external); null if absent. */
11
+ /** Value by require key (a lib) or by accessor (an external); undefined if absent. */
12
12
  resolve: (key: string) => unknown;
13
- /** The physics engine — loaded and initialized; null in worlds without physics. */
14
- readonly rapier: typeof RAPIER | null;
13
+ /** The lib by its registry id — for runtime code that knows what it needs. */
14
+ lib: <T>(id: LibId) => T | undefined;
15
15
  }
@@ -1,15 +1,27 @@
1
1
  /**
2
- * Public API of the `html` module — fullscreen HTML HUD on top of the canvas.
2
+ * Public API of the `html` module — two fullscreen DOM layers around the canvas:
3
+ * the HUD above it (`overlay`) and the underlay below it (`underlay`).
3
4
  *
4
- * The root is `pointer-events: none` on purpose: the HUD covers the whole screen
5
- * and must not swallow clicks meant for the world. Anything interactive you
6
- * append has to set `pointer-events: auto` on itself.
5
+ * Both are `pointer-events: none` on purpose: they cover the whole screen and
6
+ * must not swallow clicks meant for the world. Anything interactive appended to
7
+ * the HUD has to set `pointer-events: auto` on itself; the underlay can never
8
+ * be clicked at all — the canvas is on top of it.
9
+ *
10
+ * The underlay is only visible through a transparent canvas, which needs all
11
+ * three: `renderer.alpha` in `settings.renderer.json`, an empty scene
12
+ * background (a background colour paints over everything), and bloom off —
13
+ * the bloom pass currently loses the scene alpha, see
14
+ * `/docs/potential-issues.md`.
7
15
  */
8
16
  export interface HtmlModuleApi {
9
17
  /** HUD container over the canvas; click-through (`pointer-events: none`). */
10
- readonly root: HTMLDivElement;
18
+ readonly overlay: HTMLDivElement;
19
+ /** Container under the canvas — for DOM that the world should render on top of. */
20
+ readonly underlay: HTMLDivElement;
11
21
  readonly crosshair: HTMLDivElement;
12
- /** Appends nodes into the click-through root — see the note above. */
22
+ /** Appends nodes over the canvas — see the note above. */
13
23
  append: (...nodes: Array<Node | string>) => void;
24
+ /** Appends nodes under the canvas — see the note above. */
25
+ appendUnderlay: (...nodes: Array<Node | string>) => void;
14
26
  setCrosshair: (innerHtml: string) => void;
15
27
  }
@@ -1,4 +1,5 @@
1
1
  import type { ContextModule } from "three-start";
2
+ import type { LibId } from "../addons/lib-registry";
2
3
  /**
3
4
  * Keys of the pluggable modules — the same keys as in
4
5
  * `settings.addons.json → modules`. Core modules (`addons`/`env`/`html`/`input`/
@@ -27,6 +28,8 @@ export interface OptionalModuleDescriptor<TConfig = unknown> {
27
28
  defaultConfig: () => TConfig;
28
29
  /** The module class is pulled here — a disabled module never reaches the bundle. */
29
30
  create: (config: TConfig) => Promise<ContextModule>;
31
+ /** Libs the module cannot work without: enabling it loads them (physics → rapier). */
32
+ requiredLibs?: LibId[];
30
33
  /**
31
34
  * JSON Schema fragment for `modules.<id>`: the platform assembles the
32
35
  * `settings.addons.json` schema from these, so module keys never drift from the
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Object that forwards every access to `resolve()`. For script globals that are
3
+ * shortcuts into something created later (`modules` → `ctx.modules`): the script
4
+ * bag is destructured eagerly, so a plain reference or a getter would be read
5
+ * before the target exists.
6
+ */
7
+ export declare function lazyProxy<T extends object>(resolve: () => T): T;
1
8
  /** Lazy cached value: the factory is called once on the first `get()`. */
2
9
  export declare function lazy<T>(factory: () => T): {
3
10
  get: () => T;
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.3.2";
1
+ export declare const RUNTIME_VERSION = "0.4.2";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A single reactive value of `world.stores`. Narrow facade over a nanostores
3
+ * atom: scripts see exactly these four methods.
4
+ */
5
+ export interface WorldStore<T> {
6
+ get: () => T | undefined;
7
+ set: (value: T | undefined) => void;
8
+ /** Fires on every change; returns unsubscribe. */
9
+ listen: (listener: (value: T | undefined) => void) => () => void;
10
+ /** Same as `listen`, but also fires immediately with the current value. */
11
+ subscribe: (listener: (value: T | undefined) => void) => () => void;
12
+ }
13
+ /**
14
+ * Shared reactive key-value state of a world (`world.stores` in scripts) — the
15
+ * state counterpart of the `world.events` bus. Stores are created lazily on
16
+ * first touch; `undefined` means "no value", so `delete`/`clear` only reset
17
+ * values and never invalidate a reference taken from `of()`.
18
+ */
19
+ export declare class WorldStores {
20
+ private readonly _stores;
21
+ /** The store behind a key, created on first touch. */
22
+ of<T = unknown>(key: string): WorldStore<T>;
23
+ get<T = unknown>(key: string): T | undefined;
24
+ set<T = unknown>(key: string, value: T | undefined): void;
25
+ /** Subscribe to a key; returns unsubscribe. Not called with the current value. */
26
+ listen<T = unknown>(key: string, listener: (value: T | undefined) => void): () => void;
27
+ /** Same as `listen`, but also fires immediately with the current value. */
28
+ subscribe<T = unknown>(key: string, listener: (value: T | undefined) => void): () => void;
29
+ /** A value has been written to the key and not deleted since. */
30
+ has(key: string): boolean;
31
+ /** Keys that currently hold a value. */
32
+ keys(): string[];
33
+ /** Plain object with all current values — for debugging and saves. */
34
+ snapshot(): Record<string, unknown>;
35
+ /** Resets the key to `undefined`; listeners are notified. */
36
+ delete(key: string): void;
37
+ /** Resets every key; listeners are notified. */
38
+ clear(): void;
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvyverse/world-runtime",
3
- "version": "0.3.2",
3
+ "version": "0.4.2",
4
4
  "description": "Runtime for worlds exported from the Kvyverse platform for self-hosting",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "Vladislav Kruteniuk",
@@ -35,6 +35,7 @@
35
35
  "animejs": "^4.5.0",
36
36
  "eventemitter3": "^5.0.1",
37
37
  "howler": "^2.2.4",
38
+ "nanostores": "^1.3.0",
38
39
  "nipplejs": "^0.10.2",
39
40
  "stats.js": "^0.17.0",
40
41
  "three": "^0.184.0",