@kvyverse/world-runtime 0.1.0 → 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.
Files changed (75) hide show
  1. package/README.md +84 -59
  2. package/dist/{BallCollider-8ZrxReEd.js → BallCollider-3L5vOrcL.js} +2 -2
  3. package/dist/{CapsuleCollider-bynyf6Mc.js → CapsuleCollider-CBTsR-Ys.js} +2 -2
  4. package/dist/ChatModule-2tiWS_Yr.js +411 -0
  5. package/dist/{Collider-4_rCTHVA.js → Collider-CPUCG3RA.js} +2 -2
  6. package/dist/{ConeCollider-DtRZmgK8.js → ConeCollider-C3XU3Y9X.js} +2 -2
  7. package/dist/{ConvexMeshCollider-BSmDM4o3.js → ConvexMeshCollider-DzNV4HTO.js} +1 -1
  8. package/dist/{CuboidCollider-CJvq7--s.js → CuboidCollider-Dp1DZoFM.js} +1 -1
  9. package/dist/{CylinderCollider-CQPSh4tu.js → CylinderCollider-C0h_nhB5.js} +2 -2
  10. package/dist/{FpsCameraController-BE2EirRj.js → FpsCameraController-2GNWJM5D.js} +1 -1
  11. package/dist/RapierDebugRenderer-BQy_m-3C.js +73 -0
  12. package/dist/{RapierFpsPlayerController-VsNfQl6v.js → RapierFpsPlayerController-DthD8dw9.js} +5 -22
  13. package/dist/{RapierPhysics-C_3b7QOn.js → RapierPhysics-CU6X--c7.js} +34 -15
  14. package/dist/{Rigidbody-b7QNF4ev.js → Rigidbody-DLhN8ddv.js} +12 -1
  15. package/dist/World.d.ts +4 -2
  16. package/dist/WorldRuntime.d.ts +15 -5
  17. package/dist/addons/AddonsRuntime.d.ts +37 -0
  18. package/dist/addons/builtin-libs.d.ts +20 -0
  19. package/dist/addons/lib-registry.d.ts +60 -0
  20. package/dist/addons/required-libs.d.ts +9 -0
  21. package/dist/addons/types.d.ts +28 -0
  22. package/dist/behaviours/physics/BallCollider.d.ts +1 -1
  23. package/dist/behaviours/physics/CapsuleCollider.d.ts +1 -1
  24. package/dist/behaviours/physics/ConeCollider.d.ts +1 -1
  25. package/dist/behaviours/physics/ConvexMeshCollider.d.ts +1 -1
  26. package/dist/behaviours/physics/CuboidCollider.d.ts +1 -1
  27. package/dist/behaviours/physics/CylinderCollider.d.ts +1 -1
  28. package/dist/behaviours/physics/HalfspaceCollider.d.ts +1 -1
  29. package/dist/behaviours/physics/_required-patch.d.ts +10 -8
  30. package/dist/behaviours/registry.d.ts +9 -2
  31. package/dist/chat.module-descriptor-BTsep-Kq.js +107 -0
  32. package/dist/{get-scale-from-obj-QKv3Fig5.js → get-scale-from-obj-B7JlVPni.js} +1 -1
  33. package/dist/index.d.ts +26 -14
  34. package/dist/index.js +594 -202
  35. package/dist/lib-registry-D1VyVPBg.js +142 -0
  36. package/dist/modules/HtmlHudModule.d.ts +5 -1
  37. package/dist/modules/InputSystemModule.d.ts +14 -32
  38. package/dist/modules/chat/ChatDom.d.ts +100 -0
  39. package/dist/modules/chat/ChatModule.d.ts +36 -0
  40. package/dist/modules/chat/chat.module-descriptor.d.ts +43 -0
  41. package/dist/modules/contracts/addons.module-api.d.ts +15 -0
  42. package/dist/modules/contracts/chat.module-api.d.ts +52 -0
  43. package/dist/modules/contracts/controls.module-api.d.ts +58 -0
  44. package/dist/modules/contracts/html.module-api.d.ts +22 -2
  45. package/dist/modules/contracts/input.module-api.d.ts +11 -11
  46. package/dist/modules/contracts/module-events.d.ts +17 -0
  47. package/dist/modules/contracts/rapier.module-api.d.ts +13 -1
  48. package/dist/modules/controls/ControlsModule.d.ts +45 -0
  49. package/dist/modules/controls/KeyboardControlsDriver.d.ts +12 -0
  50. package/dist/modules/index.d.ts +30 -3
  51. package/dist/modules/optional-module-descriptor.d.ts +39 -0
  52. package/dist/modules/optional-modules.d.ts +11 -0
  53. package/dist/modules/physics/RapierDebugRenderer.d.ts +19 -0
  54. package/dist/{behaviours/physics/rapier-addon-core → modules/physics}/RapierPhysics.d.ts +13 -3
  55. package/dist/modules/physics/physics.module-descriptor.d.ts +16 -0
  56. package/dist/modules/registry.d.ts +7 -3
  57. package/dist/rendering/SceneBloom.d.ts +1 -1
  58. package/dist/rendering/renderer-apply.d.ts +1 -1
  59. package/dist/{settings → rendering}/types.d.ts +1 -12
  60. package/dist/types.d.ts +10 -3
  61. package/dist/utils/lazy.d.ts +7 -0
  62. package/dist/version.gen.d.ts +1 -1
  63. package/dist/world-stores.d.ts +39 -0
  64. package/package.json +10 -1
  65. package/dist/RapierDebugRenderer-sURZDLP4.js +0 -55
  66. package/dist/behaviours/physics/rapier-addon-core/RapierDebugRenderer.d.ts +0 -17
  67. package/dist/modules/contracts/rapierDebug.module-api.d.ts +0 -5
  68. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  69. package/dist/settings/AddonsRuntime.d.ts +0 -19
  70. package/dist/settings/lib-loaders.d.ts +0 -7
  71. package/dist/settings/lib-registry.d.ts +0 -24
  72. package/dist/settings/module-registry.d.ts +0 -25
  73. /package/dist/{settings → addons}/externals.d.ts +0 -0
  74. /package/dist/behaviours/physics/{rapier-addon-core/Collider.d.ts → Collider.d.ts} +0 -0
  75. /package/dist/behaviours/physics/{rapier-addon-core/get-scale-from-obj.d.ts → get-scale-from-obj.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { t as RapierPhysics } from "./RapierPhysics-C_3b7QOn.js";
2
- import { n as getRadiusScale, t as getHalfheightRadiusScale } from "./get-scale-from-obj-QKv3Fig5.js";
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";
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";
5
6
  import * as ThreeStartNS from "three-start";
@@ -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";
10
- import EventEmitter from "eventemitter3";
11
+ import * as howler from "howler";
11
12
  import { Howl, Howler } from "howler";
12
13
  import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
13
14
  import { clone } from "three/examples/jsm/utils/SkeletonUtils.js";
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)
@@ -40,12 +44,16 @@ var World = class extends ThreeStart {
40
44
  super(options);
41
45
  this._runtime = _runtime;
42
46
  }
43
- /** Asset URL by project path — same resolving as `getUrl` in scripts. */
44
- getUrl = (path) => this._runtime.getUrl(path);
47
+ /** Asset URL by project path or resource — same resolving as `getUrl` in scripts. */
48
+ getUrl = (source) => this._runtime.getUrl(source);
45
49
  /** World lifecycle event bus — the same one as `world.events` in scripts. */
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;
@@ -268,6 +296,248 @@ function finiteOr(value, fallback) {
268
296
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
269
297
  }
270
298
  //#endregion
299
+ //#region packages/world-runtime/src/modules/controls/KeyboardControlsDriver.ts
300
+ /** A diagonal must not be faster than a straight line. */
301
+ var DIAGONAL = Math.SQRT1_2;
302
+ /**
303
+ * Keyboard driver for `controls`: WASD/arrows, shift to run, Space to jump.
304
+ * Reads key state from the `input` module and binds no listeners of its own.
305
+ */
306
+ var KeyboardControlsDriver = class {
307
+ _input;
308
+ kind = "keyboard";
309
+ constructor(_input) {
310
+ this._input = _input;
311
+ }
312
+ read(state) {
313
+ const key = this._input.isKeyDown;
314
+ let x = 0;
315
+ let y = 0;
316
+ if (key("KeyW") || key("ArrowUp")) y += 1;
317
+ if (key("KeyS") || key("ArrowDown")) y -= 1;
318
+ if (key("KeyD") || key("ArrowRight")) x += 1;
319
+ if (key("KeyA") || key("ArrowLeft")) x -= 1;
320
+ if (x !== 0 && y !== 0) {
321
+ x *= DIAGONAL;
322
+ y *= DIAGONAL;
323
+ }
324
+ state.move.set(x, y);
325
+ state.force = x !== 0 || y !== 0 ? 1 : 0;
326
+ state.run = this._input.shiftKey;
327
+ state.jump = key("Space");
328
+ }
329
+ };
330
+ //#endregion
331
+ //#region packages/world-runtime/src/modules/controls/ControlsModule.ts
332
+ /**
333
+ * The player's movement intent — what they want to do, whatever the hardware is.
334
+ * Input sources register as drivers (`addDriver`); the module polls them once a
335
+ * frame and decides which one is driving. Details — `./README.md`.
336
+ */
337
+ var ControlsModule = class extends ContextModule {
338
+ /** x — strafe, y — forward. Reused vector: copy it before storing. */
339
+ move = new THREE.Vector2();
340
+ _drivers = [];
341
+ _state = {
342
+ move: new THREE.Vector2(),
343
+ force: 0,
344
+ run: false,
345
+ jump: false
346
+ };
347
+ _keyboard = null;
348
+ _active = null;
349
+ _source = "keyboard";
350
+ _force = 0;
351
+ _run = false;
352
+ _jump = false;
353
+ onAwake() {
354
+ this._keyboard = new KeyboardControlsDriver(this.modules.input);
355
+ this.addDriver(this._keyboard);
356
+ }
357
+ /** Speed multiplier: 1 is a normal walk, higher means a stronger stick tilt. */
358
+ get force() {
359
+ return this._force;
360
+ }
361
+ /** Run modifier: shift, a stick pushed past its ring, a UI button. */
362
+ get run() {
363
+ return this._run;
364
+ }
365
+ /** Kind of the device driving right now. */
366
+ get source() {
367
+ return this._source;
368
+ }
369
+ get drivers() {
370
+ return this._drivers;
371
+ }
372
+ /** The built-in keyboard driver — detach it with `removeDriver`. */
373
+ get keyboard() {
374
+ if (!this._keyboard) throw new Error("controls: keyboard driver is not ready yet");
375
+ return this._keyboard;
376
+ }
377
+ addDriver(driver) {
378
+ if (!this._drivers.includes(driver)) this._drivers.push(driver);
379
+ return () => this.removeDriver(driver);
380
+ }
381
+ removeDriver(driver) {
382
+ const index = this._drivers.indexOf(driver);
383
+ if (index < 0) return;
384
+ this._drivers.splice(index, 1);
385
+ if (this._active === driver) this._active = null;
386
+ }
387
+ jump() {
388
+ this.emit("jump");
389
+ }
390
+ onUpdate() {
391
+ const driver = this._readActiveDriver();
392
+ this._apply(driver);
393
+ }
394
+ /**
395
+ * The active driver keeps the channel while it produces input — otherwise the
396
+ * keyboard and a stick would steal control from each other every frame. Once it
397
+ * goes quiet, the first driver with input takes over. The state is left filled
398
+ * by the winner, so nobody is read twice.
399
+ */
400
+ _readActiveDriver() {
401
+ if (this._active && this._readDriver(this._active)) return this._active;
402
+ for (const driver of this._drivers) {
403
+ if (driver === this._active) continue;
404
+ if (this._readDriver(driver)) return driver;
405
+ }
406
+ this._resetState();
407
+ return null;
408
+ }
409
+ _readDriver(driver) {
410
+ this._resetState();
411
+ driver.read(this._state);
412
+ const { move, force, jump } = this._state;
413
+ return move.x !== 0 || move.y !== 0 || force > 0 || jump;
414
+ }
415
+ _resetState() {
416
+ const state = this._state;
417
+ state.move.set(0, 0);
418
+ state.force = 0;
419
+ state.run = false;
420
+ state.jump = false;
421
+ }
422
+ _apply(driver) {
423
+ const state = this._state;
424
+ this._active = driver ?? this._active;
425
+ const source = this._active?.kind ?? this._source;
426
+ if (source !== this._source) {
427
+ this._source = source;
428
+ this.emit("sourcechanged", source);
429
+ }
430
+ if (!this.move.equals(state.move)) {
431
+ this.move.copy(state.move);
432
+ this.emit("movechanged", this.move);
433
+ }
434
+ if (this._force !== state.force) {
435
+ this._force = state.force;
436
+ this.emit("forcechanged", state.force);
437
+ }
438
+ this._run = state.run;
439
+ if (state.jump && !this._jump) this.jump();
440
+ this._jump = state.jump;
441
+ }
442
+ };
443
+ //#endregion
444
+ //#region packages/world-runtime/src/behaviours/physics/_required-patch.ts
445
+ var lazyRapierPhysicsModule = lazy(async () => (await import("./RapierPhysics-CU6X--c7.js")).default);
446
+ /** The only place the physics module is created — called by the registry
447
+ * descriptor. The module itself takes rapier from `ctx.modules.addons` on awake. */
448
+ async function createRapierPhysicsModule(config) {
449
+ return new (await (lazyRapierPhysicsModule.get()))({ debug: config.debug });
450
+ }
451
+ /**
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.
463
+ */
464
+ async function applyRapierPhysicsPatch(starter) {
465
+ if (starter.ctx.modules.rapier) return;
466
+ const descriptor = physicsModuleDescriptor;
467
+ const module = await descriptor.create(descriptor.defaultConfig());
468
+ starter.addModules({ rapier: module });
469
+ }
470
+ //#endregion
471
+ //#region packages/world-runtime/src/modules/physics/physics.module-descriptor.ts
472
+ var PHYSICS_ENGINES = ["rapier"];
473
+ /** Physics defaults: the rapier engine, debug off. */
474
+ function normalizePhysics(value) {
475
+ if (!value) return null;
476
+ if (typeof value === "string") return {
477
+ engine: value,
478
+ debug: false
479
+ };
480
+ return {
481
+ engine: value.engine,
482
+ debug: value.debug ?? false
483
+ };
484
+ }
485
+ var physicsModuleDescriptor = {
486
+ id: OptionalModuleId.Physics,
487
+ normalize: (raw) => normalizePhysics(raw),
488
+ defaultConfig: () => ({
489
+ engine: "rapier",
490
+ debug: false
491
+ }),
492
+ create: (config) => createRapierPhysicsModule(config),
493
+ requiredLibs: [LibId.Rapier],
494
+ schema: {
495
+ description: "Physics engine module. false | \"rapier\" | { engine, debug }.",
496
+ oneOf: [
497
+ {
498
+ type: "boolean",
499
+ enum: [false]
500
+ },
501
+ {
502
+ type: "string",
503
+ enum: [...PHYSICS_ENGINES]
504
+ },
505
+ {
506
+ type: "object",
507
+ additionalProperties: false,
508
+ required: ["engine"],
509
+ properties: {
510
+ engine: {
511
+ type: "string",
512
+ enum: [...PHYSICS_ENGINES]
513
+ },
514
+ debug: {
515
+ type: "boolean",
516
+ description: "Draw collider debug lines from the start (the renderer is loaded on demand)."
517
+ }
518
+ }
519
+ }
520
+ ]
521
+ }
522
+ };
523
+ //#endregion
524
+ //#region packages/world-runtime/src/modules/optional-modules.ts
525
+ /**
526
+ * Table of contents for the world's pluggable modules: what can be enabled from
527
+ * `settings.addons.json → modules`. The descriptors themselves live next to
528
+ * their module code; this is only the list.
529
+ *
530
+ * Add a descriptor here and the module shows up both in the runtime and in the
531
+ * config's JSON schema (the platform builds it from `descriptor.schema`).
532
+ */
533
+ var OPTIONAL_MODULES = [physicsModuleDescriptor, chatModuleDescriptor];
534
+ var DESCRIPTOR_BY_ID = new Map(OPTIONAL_MODULES.map((m) => [m.id, m]));
535
+ function getOptionalModuleDescriptor(id) {
536
+ const descriptor = DESCRIPTOR_BY_ID.get(id);
537
+ if (!descriptor) throw new Error(`Unknown world module "${id}"`);
538
+ return descriptor;
539
+ }
540
+ //#endregion
271
541
  //#region packages/world-runtime/src/modules/is-mobile-device.ts
272
542
  function isMobileDevice() {
273
543
  if (typeof navigator === "undefined" || typeof window === "undefined") return false;
@@ -296,19 +566,25 @@ var EnvModule = class extends ContextModule {
296
566
  //#endregion
297
567
  //#region packages/world-runtime/src/modules/HtmlHudModule.ts
298
568
  var { div } = van.tags;
569
+ var LAYER_STYLE = "position:fixed;inset:0;pointer-events:none;";
299
570
  var HtmlHudModule = class extends ContextModule {
300
- root;
571
+ overlay;
572
+ underlay;
301
573
  crosshair;
302
574
  constructor() {
303
575
  super();
304
576
  this.crosshair = div({
305
- id: "kvyverse-game-gui-crosshair",
577
+ id: "kvy-crosshair",
306
578
  style: "position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);"
307
579
  });
308
- this.root = div({
309
- id: "kvyverse-game-gui-root",
310
- style: "position:fixed;inset:0;pointer-events:none;"
580
+ this.overlay = div({
581
+ id: "kvy-overlay",
582
+ style: LAYER_STYLE
311
583
  }, this.crosshair);
584
+ this.underlay = div({
585
+ id: "kvy-underlay",
586
+ style: LAYER_STYLE
587
+ });
312
588
  }
313
589
  onAwake() {
314
590
  const ctx = this.ctx;
@@ -316,8 +592,13 @@ var HtmlHudModule = class extends ContextModule {
316
592
  ctx.on(ThreeContextEvents.Mount, this.onMount);
317
593
  ctx.on(ThreeContextEvents.Unmount, this.onUnmount);
318
594
  }
595
+ /** Appends nodes over the canvas (the HUD). */
319
596
  append(...nodes) {
320
- 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);
321
602
  }
322
603
  setCrosshair(innerHtml) {
323
604
  this.crosshair.innerHTML = innerHtml;
@@ -328,54 +609,18 @@ var HtmlHudModule = class extends ContextModule {
328
609
  this.ctx.off(ThreeContextEvents.Unmount, this.onUnmount);
329
610
  }
330
611
  onMount = (container) => {
331
- 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";
332
616
  };
333
617
  onUnmount = () => {
334
- this.root.remove();
618
+ this.overlay.remove();
619
+ this.underlay.remove();
335
620
  };
336
621
  };
337
622
  //#endregion
338
623
  //#region packages/world-runtime/src/modules/InputSystemModule.ts
339
- var PlayerInput = class extends EventEmitter {
340
- _forward = 0;
341
- _lateral = 0;
342
- _force = 1;
343
- /**
344
- * @default 0
345
- */
346
- get forward() {
347
- return this._forward;
348
- }
349
- /**
350
- * @default 0
351
- */
352
- get lateral() {
353
- return this._lateral;
354
- }
355
- /**
356
- * @default 1
357
- */
358
- get force() {
359
- return this._force;
360
- }
361
- setDir(forward, lateral) {
362
- this._forward = forward;
363
- this._lateral = lateral;
364
- this.emit("dirchanged", forward, lateral);
365
- }
366
- setForce(force) {
367
- this._force = force;
368
- this.emit("forcechanged", force);
369
- }
370
- jump() {
371
- this.emit("jump");
372
- }
373
- /**
374
- * 0 is keyboard, 1 is joystick
375
- * @default 0
376
- */
377
- mode = 0;
378
- };
379
624
  var InputSystemModule = class extends ContextModule {
380
625
  _dom;
381
626
  _keys = /* @__PURE__ */ new Set();
@@ -421,12 +666,30 @@ var InputSystemModule = class extends ContextModule {
421
666
  isKeyDown = (key) => {
422
667
  return this._keys.has(key);
423
668
  };
424
- player = new PlayerInput();
669
+ _captureKeyboard = true;
670
+ /**
671
+ * The game listens to the keyboard on the whole document, not just the canvas
672
+ * (otherwise any HTML over the world would swallow input). `false` hands the
673
+ * keyboard entirely to the UI: the game sees no keys and held ones are reset.
674
+ * @default true
675
+ */
676
+ get captureKeyboard() {
677
+ return this._captureKeyboard;
678
+ }
679
+ set captureKeyboard(value) {
680
+ if (this._captureKeyboard === value) return;
681
+ this._captureKeyboard = value;
682
+ if (!value) {
683
+ this.resetKeys();
684
+ this.emit("keyschanged");
685
+ }
686
+ }
425
687
  onAwake() {
426
688
  const ctx = this.ctx;
427
689
  this._dom = ctx.renderer.domElement;
428
690
  window.addEventListener("resize", this.onWindowResize);
429
691
  window.addEventListener("scroll", this.onWindowScroll);
692
+ window.addEventListener("blur", this.onBlur);
430
693
  ctx.on(ThreeContextEvents.Resized, this.onDomResize);
431
694
  const on = this._dom.addEventListener.bind(this._dom);
432
695
  on("focus", this.onFocus);
@@ -436,13 +699,15 @@ var InputSystemModule = class extends ContextModule {
436
699
  on("pointermove", this.onPointerMove);
437
700
  on("pointerup", this.onPointerUp);
438
701
  on("pointerleave", this.onPointerLeave);
439
- on("keydown", this.onKeyDown);
440
- on("keyup", this.onKeyUp);
702
+ document.addEventListener("keydown", this.onKeyDown);
703
+ document.addEventListener("keyup", this.onKeyUp);
704
+ document.addEventListener("focusin", this.onDocumentFocusIn);
441
705
  }
442
706
  /** three-start `ContextModule` has no `onDestroy`; teardown is explicit. */
443
707
  dispose() {
444
708
  window.removeEventListener("resize", this.onWindowResize);
445
709
  window.removeEventListener("scroll", this.onWindowScroll);
710
+ window.removeEventListener("blur", this.onBlur);
446
711
  this.ctx.off(ThreeContextEvents.Resized, this.onDomResize);
447
712
  const off = this._dom.removeEventListener.bind(this._dom);
448
713
  off("focus", this.onFocus);
@@ -452,8 +717,9 @@ var InputSystemModule = class extends ContextModule {
452
717
  off("pointermove", this.onPointerMove);
453
718
  off("pointerup", this.onPointerUp);
454
719
  off("pointerleave", this.onPointerLeave);
455
- off("keydown", this.onKeyDown);
456
- off("keyup", this.onKeyUp);
720
+ document.removeEventListener("keydown", this.onKeyDown);
721
+ document.removeEventListener("keyup", this.onKeyUp);
722
+ document.removeEventListener("focusin", this.onDocumentFocusIn);
457
723
  }
458
724
  onWindowResize = () => {
459
725
  this.updateDomBoundingClientRect();
@@ -472,15 +738,28 @@ var InputSystemModule = class extends ContextModule {
472
738
  this.resetKeys();
473
739
  this.resetPointers();
474
740
  };
741
+ onDocumentFocusIn = (event) => {
742
+ if (!isUiKeyboardTarget(event.target)) return;
743
+ this.resetKeys();
744
+ this.emit("keyschanged");
745
+ };
746
+ /** The key belongs to the game, not to the UI. */
747
+ acceptsKey(event) {
748
+ return this._captureKeyboard && !isUiKeyboardTarget(event.target);
749
+ }
475
750
  onKeyDown = (event) => {
751
+ if (!this.acceptsKey(event)) return;
476
752
  this.updFnKeys(event);
477
753
  this._keys.add(event.code);
478
754
  this.emit("keyschanged");
755
+ this.emit("keydown", event);
479
756
  };
480
757
  onKeyUp = (event) => {
758
+ if (!this.acceptsKey(event)) return;
481
759
  this.updFnKeys(event);
482
760
  this._keys.delete(event.code);
483
761
  this.emit("keyschanged");
762
+ this.emit("keyup", event);
484
763
  };
485
764
  onPointerEnter = (_) => {};
486
765
  onPointerDown = (event) => {
@@ -577,6 +856,21 @@ var InputSystemModule = class extends ContextModule {
577
856
  return this._domBoundingClientRect;
578
857
  }
579
858
  };
859
+ /**
860
+ * Focus is in a text field (or contenteditable) — the keyboard belongs to the UI
861
+ * and the game does not see it. Engine plumbing: nobody has to think about it,
862
+ * `isKeyDown` simply stays quiet while the player types.
863
+ */
864
+ function isUiKeyboardTarget(target) {
865
+ if (!(target instanceof HTMLElement)) return false;
866
+ if (target.isContentEditable) return true;
867
+ switch (target.tagName) {
868
+ case "INPUT":
869
+ case "TEXTAREA":
870
+ case "SELECT": return true;
871
+ default: return false;
872
+ }
873
+ }
580
874
  //#endregion
581
875
  //#region packages/world-runtime/src/modules/SoundsModule.ts
582
876
  var SoundsModule = class extends ContextModule {
@@ -599,39 +893,36 @@ var UtilsModule = class extends ContextModule {
599
893
  };
600
894
  //#endregion
601
895
  //#region packages/world-runtime/src/modules/index.ts
602
- /** World ctx modules. Registered before `start()` — three-start forbids it later. */
603
- function createWorldModules(mode = "prod") {
604
- return {
896
+ /**
897
+ * Assembles the world's ctx modules: core ones always, pluggable ones per config.
898
+ * Pluggable classes are pulled dynamically, so a disabled module never reaches
899
+ * the bundle. Call strictly before `start()` and with addons already loaded: the
900
+ * physics module takes rapier from `ctx.modules.addons`.
901
+ *
902
+ * A module the scene needs without a line in the config (physics when a physics
903
+ * behaviour is present) is added separately — through the behaviour's
904
+ * `meta.requiredPatch`, with the same descriptor and idempotently.
905
+ */
906
+ async function createWorldModules(mode = "prod", options) {
907
+ const modules = {
908
+ addons: options.addons,
909
+ controls: new ControlsModule(),
605
910
  env: new EnvModule(mode),
606
911
  html: new HtmlHudModule(),
607
912
  input: new InputSystemModule(),
608
913
  sounds: new SoundsModule(),
609
914
  utils: new UtilsModule()
610
915
  };
611
- }
612
- //#endregion
613
- //#region packages/world-runtime/src/behaviours/physics/_required-patch.ts
614
- /** rapier loads lazily: the wasm is pulled only in worlds with physics. */
615
- var lazyRapier = lazy(async () => {
616
- const rapier = await import("@dimforge/rapier3d-compat");
617
- await rapier.init();
618
- return rapier;
619
- });
620
- var lazyRapierPhysicsModule = lazy(async () => (await import("./RapierPhysics-C_3b7QOn.js").then((n) => n.n)).default);
621
- var lazyRapierDebugModule = lazy(async () => (await import("./RapierDebugRenderer-sURZDLP4.js")).default);
622
- /**
623
- * Registers rapier modules before bootstrap: three-start forbids `addModules`
624
- * after `start()`. Idempotent — a repeated call sees the module in ctx and exits.
625
- */
626
- async function applyRapierPhysicsPatch(starter) {
627
- const RAPIER = await lazyRapier.get();
628
- const RapierPhysicsModule = await lazyRapierPhysicsModule.get();
629
- const RapierDebugRenderer = await lazyRapierDebugModule.get();
630
- if (RapierPhysics.findInCtx(starter.ctx)) return;
631
- starter.addModules({
632
- rapier: new RapierPhysicsModule(RAPIER),
633
- rapierDebug: new RapierDebugRenderer()
634
- });
916
+ for (const descriptor of OPTIONAL_MODULES) {
917
+ const config = descriptor.normalize(options.modules?.[descriptor.id]);
918
+ if (!config) continue;
919
+ try {
920
+ modules[descriptor.id] = await descriptor.create(config);
921
+ } catch (error) {
922
+ console.error(`[modules] failed to create "${descriptor.id}"`, error);
923
+ }
924
+ }
925
+ return modules;
635
926
  }
636
927
  //#endregion
637
928
  //#region packages/world-runtime/src/behaviours/registry.ts
@@ -664,7 +955,7 @@ var entries = [
664
955
  {
665
956
  uuid: "fpscameracontroller",
666
957
  name: "FpsCameraController",
667
- load: () => loadDefault(import("./FpsCameraController-BE2EirRj.js"))
958
+ load: () => loadDefault(import("./FpsCameraController-2GNWJM5D.js"))
668
959
  },
669
960
  {
670
961
  uuid: "sightinteractablearea",
@@ -679,56 +970,56 @@ var entries = [
679
970
  {
680
971
  uuid: "rrb",
681
972
  name: "Rigidbody",
682
- load: () => loadDefault(import("./Rigidbody-b7QNF4ev.js").then((n) => n.n)),
683
- requiredPatch: applyRapierPhysicsPatch
973
+ load: () => loadDefault(import("./Rigidbody-DLhN8ddv.js").then((n) => n.n)),
974
+ requiredPatch: rapierPhysicsPatch
684
975
  },
685
976
  {
686
977
  uuid: "rapierballcol",
687
978
  name: "BallCollider",
688
- load: () => loadDefault(import("./BallCollider-8ZrxReEd.js")),
689
- requiredPatch: applyRapierPhysicsPatch
979
+ load: () => loadDefault(import("./BallCollider-3L5vOrcL.js")),
980
+ requiredPatch: rapierPhysicsPatch
690
981
  },
691
982
  {
692
983
  uuid: "rapiercapscol",
693
984
  name: "CapsuleCollider",
694
- load: () => loadDefault(import("./CapsuleCollider-bynyf6Mc.js")),
695
- requiredPatch: applyRapierPhysicsPatch
985
+ load: () => loadDefault(import("./CapsuleCollider-CBTsR-Ys.js")),
986
+ requiredPatch: rapierPhysicsPatch
696
987
  },
697
988
  {
698
989
  uuid: "rapierconecol",
699
990
  name: "ConeCollider",
700
- load: () => loadDefault(import("./ConeCollider-DtRZmgK8.js")),
701
- requiredPatch: applyRapierPhysicsPatch
991
+ load: () => loadDefault(import("./ConeCollider-C3XU3Y9X.js")),
992
+ requiredPatch: rapierPhysicsPatch
702
993
  },
703
994
  {
704
995
  uuid: "rapierconvxcol",
705
996
  name: "ConvexMeshCollider",
706
- load: () => loadDefault(import("./ConvexMeshCollider-BSmDM4o3.js")),
707
- requiredPatch: applyRapierPhysicsPatch
997
+ load: () => loadDefault(import("./ConvexMeshCollider-DzNV4HTO.js")),
998
+ requiredPatch: rapierPhysicsPatch
708
999
  },
709
1000
  {
710
1001
  uuid: "rapiercubcol",
711
1002
  name: "CuboidCollider",
712
- load: () => loadDefault(import("./CuboidCollider-CJvq7--s.js")),
713
- requiredPatch: applyRapierPhysicsPatch
1003
+ load: () => loadDefault(import("./CuboidCollider-Dp1DZoFM.js")),
1004
+ requiredPatch: rapierPhysicsPatch
714
1005
  },
715
1006
  {
716
1007
  uuid: "rapiercylcol",
717
1008
  name: "CylinderCollider",
718
- load: () => loadDefault(import("./CylinderCollider-CQPSh4tu.js")),
719
- requiredPatch: applyRapierPhysicsPatch
1009
+ load: () => loadDefault(import("./CylinderCollider-C0h_nhB5.js")),
1010
+ requiredPatch: rapierPhysicsPatch
720
1011
  },
721
1012
  {
722
1013
  uuid: "rapiertricol",
723
1014
  name: "TrimeshCollider",
724
1015
  load: () => loadDefault(import("./TrimeshCollider-7dIs_xtZ.js")),
725
- requiredPatch: applyRapierPhysicsPatch
1016
+ requiredPatch: rapierPhysicsPatch
726
1017
  },
727
1018
  {
728
1019
  uuid: "rapierfpsplayercontroller",
729
1020
  name: "RapierFpsPlayerController",
730
- load: () => loadDefault(import("./RapierFpsPlayerController-VsNfQl6v.js")),
731
- requiredPatch: applyRapierPhysicsPatch
1021
+ load: () => loadDefault(import("./RapierFpsPlayerController-DthD8dw9.js")),
1022
+ requiredPatch: rapierPhysicsPatch
732
1023
  }
733
1024
  ];
734
1025
  /** Registry of built-in behaviours by uuid — source of truth for both game and export. */
@@ -803,81 +1094,124 @@ function createFont(url, family, mime, name) {
803
1094
  return source;
804
1095
  }
805
1096
  //#endregion
806
- //#region packages/world-runtime/src/settings/lib-registry.ts
1097
+ //#region packages/world-runtime/src/addons/builtin-libs.ts
807
1098
  /**
808
- * Registry of built-in libraries enabled via `settings.addons.json → libs`.
809
- *
810
- * Separates two keys: `id` (short config key under `libs`) and `requireKey`
811
- * (what the user writes in `require(...)` in scripts). Single source for:
812
- * - game: dynamic loading (`lib-loaders`),
813
- * - editor: JSON schema (`libs` props) + require typings (`AddonsRequireTypings`).
814
- *
815
- * Adding a lib = add an entry here (+ loader in game, + d.ts loader in editor).
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.
816
1103
  */
817
- var LibId = /* @__PURE__ */ function(LibId) {
818
- LibId["Tween"] = "tween";
819
- LibId["Anime"] = "anime";
820
- return LibId;
821
- }({});
822
- var LIBS = {
823
- ["tween"]: {
824
- id: "tween",
825
- requireKey: "@tweenjs/tween.js",
826
- label: "Tween.js"
827
- },
828
- ["anime"]: {
829
- id: "anime",
830
- requireKey: "animejs/animation",
831
- label: "Anime.js (animate)"
832
- }
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
833
1112
  };
834
- var LIB_IDS = Object.values(LibId);
835
- var LIB_DESCRIPTORS = LIB_IDS.map((id) => LIBS[id]);
836
- function libByRequireKey(requireKey) {
837
- return LIB_DESCRIPTORS.find((l) => l.requireKey === requireKey);
838
- }
839
- //#endregion
840
- //#region packages/world-runtime/src/settings/lib-loaders.ts
841
1113
  /**
842
- * Dynamic loaders of built-in libs (game-only). Key — `LibId` from the registry;
843
- * the value resolves for the user via `require("<requireKey>")`. Imports are lazy —
844
- * bundled only when the lib is enabled in `settings.addons.json`.
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.
845
1117
  */
846
- var LIB_LOADERS = {
847
- [LibId.Tween]: () => import("@tweenjs/tween.js"),
848
- [LibId.Anime]: () => import("animejs/animation")
1118
+ var BUILTIN_GLOBALS = {
1119
+ THREE,
1120
+ TSL,
1121
+ van
849
1122
  };
850
1123
  //#endregion
851
- //#region packages/world-runtime/src/settings/AddonsRuntime.ts
1124
+ //#region packages/world-runtime/src/addons/required-libs.ts
852
1125
  /**
853
- * Runtime loading of addons (game-only): enabled built-in libs + external
854
- * scripts (externals). Exposes them to user scripts via `resolve(key)`
855
- * (used in `ScriptsRuntime.require`). Replaces the old
856
- * `GameLoader.loadExternalsAsync` / `getExternalLib`.
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.
857
1130
  */
858
- var AddonsRuntime = class {
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];
1145
+ }
1146
+ //#endregion
1147
+ //#region packages/world-runtime/src/addons/AddonsRuntime.ts
1148
+ /**
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>")`.
1154
+ *
1155
+ * This is the `addons` ctx module, but it also works without a ctx: launcher
1156
+ * worlds have no 3d context while `require` still has to work, so there the
1157
+ * instance is used directly. How to import optional packages is up to the
1158
+ * runtime owner (see `WorldImports`).
1159
+ */
1160
+ var AddonsRuntime = class extends ContextModule {
1161
+ _imports;
859
1162
  /** libs — by requireKey (`@tweenjs/tween.js`), externals — by accessor (`Tone`). */
860
1163
  _byKey = /* @__PURE__ */ new Map();
1164
+ _byLibId = /* @__PURE__ */ new Map();
861
1165
  _scriptTags = [];
862
- async load(addons) {
863
- await this._loadLibs(addons.libs);
1166
+ constructor(_imports = {}) {
1167
+ super();
1168
+ this._imports = _imports;
1169
+ this._registerBuiltins();
1170
+ }
1171
+ /** `usedBuiltinUuids` — built-in behaviours on the scene; they may require libs. */
1172
+ async load(addons, usedBuiltinUuids = []) {
1173
+ await this._loadLibs(collectRequiredLibs(addons, usedBuiltinUuids));
864
1174
  await this._loadExternals(addons.externals);
865
1175
  }
866
- /** Resolves a require key: enabled lib or external accessor; undefined otherwise. */
1176
+ /** Resolves a require key: a lib or an external accessor; undefined otherwise. */
867
1177
  resolve(key) {
868
1178
  return this._byKey.get(key);
869
1179
  }
870
- async _loadLibs(libs) {
871
- if (!libs) return;
872
- for (const id of LIB_IDS) {
873
- if (!libs[id]) continue;
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
+ }
1194
+ }
1195
+ async _loadLibs(ids) {
1196
+ for (const id of ids) {
1197
+ const descriptor = LIBS[id];
1198
+ const load = this._imports.libs?.[id];
1199
+ if (!load) {
1200
+ console.error(`[addons] lib "${id}" is needed but has no importer`);
1201
+ continue;
1202
+ }
874
1203
  try {
875
- this._byKey.set(LIBS[id].requireKey, await LIB_LOADERS[id]());
1204
+ const module = await load();
1205
+ this._register(id, unwrapLib(id, await (descriptor.init?.(module) ?? module)));
876
1206
  } catch (e) {
877
1207
  console.error(`[addons] failed to load lib "${id}"`, e);
878
1208
  }
879
1209
  }
880
1210
  }
1211
+ _register(id, value) {
1212
+ this._byLibId.set(id, value);
1213
+ this._byKey.set(LIBS[id].requireKey, value);
1214
+ }
881
1215
  async _loadExternals(externals) {
882
1216
  if (!externals) return;
883
1217
  for (const ext of externals) try {
@@ -907,37 +1241,76 @@ var AddonsRuntime = class {
907
1241
  for (const tag of this._scriptTags) tag.remove();
908
1242
  this._scriptTags.length = 0;
909
1243
  this._byKey.clear();
1244
+ this._byLibId.clear();
910
1245
  }
911
1246
  };
1247
+ /** A package like nipplejs exposes its API as the default export — that is what
1248
+ * `require` should hand back. */
1249
+ function unwrapLib(id, module) {
1250
+ if (!LIBS[id].defaultExport) return module;
1251
+ return module.default ?? module;
1252
+ }
912
1253
  //#endregion
913
- //#region packages/world-runtime/src/settings/module-registry.ts
1254
+ //#region packages/world-runtime/src/world-stores.ts
914
1255
  /**
915
- * Registry of pluggable engine modules (`settings.addons.json → modules`).
916
- * These are three-start modules that must be registered before `start()`.
917
- * Feature toggles (chat/joystick/debugPerf) are NOT here — they live in
918
- * `settings.json` (see `../features/feature-registry`).
919
- *
920
- * The only module so far is physics.
921
- * Installation lives in game (`game/settings/module-installers`).
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()`.
922
1260
  */
923
- var ModuleId = /* @__PURE__ */ function(ModuleId) {
924
- ModuleId["Physics"] = "physics";
925
- return ModuleId;
926
- }({});
927
- var PHYSICS_ENGINES = ["rapier"];
928
- var MODULE_IDS = Object.values(ModuleId);
929
- /** Normalizes the physics config to an object (or null if disabled). */
930
- function normalizePhysics(value) {
931
- if (!value) return null;
932
- if (typeof value === "string") return {
933
- engine: value,
934
- debug: false
935
- };
936
- return {
937
- engine: value.engine,
938
- debug: value.debug ?? false
939
- };
940
- }
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
+ };
941
1314
  //#endregion
942
1315
  //#region packages/world-runtime/src/WorldRuntime.ts
943
1316
  /**
@@ -949,17 +1322,26 @@ var WorldRuntime = class {
949
1322
  options;
950
1323
  /** World lifecycle event bus (`world.events` in scripts). */
951
1324
  emitter = new EventEmitter();
952
- _addons = new AddonsRuntime();
1325
+ /** Shared reactive state of the world (`world.stores` in scripts). */
1326
+ stores = new WorldStores();
1327
+ _addons;
953
1328
  _executedScripts = /* @__PURE__ */ new Map();
954
1329
  _builtins = /* @__PURE__ */ new Map();
955
1330
  _styleElements = [];
956
1331
  _resources = {};
1332
+ /** Reverse index for `getUrl(resource)` — play mode resolves objects too. */
1333
+ _pathByResource = /* @__PURE__ */ new Map();
957
1334
  _ctx = null;
958
1335
  _bloom = null;
959
1336
  _scriptContext = null;
960
1337
  constructor(definition, options) {
961
1338
  this.definition = definition;
962
1339
  this.options = options;
1340
+ this._addons = new AddonsRuntime(definition.imports);
1341
+ }
1342
+ /** The world's external dependencies; also the `addons` ctx module. */
1343
+ get addons() {
1344
+ return this._addons;
963
1345
  }
964
1346
  /** ctx appears at the 3d-context creation step; unavailable to scripts before that. */
965
1347
  get ctx() {
@@ -974,14 +1356,18 @@ var WorldRuntime = class {
974
1356
  setBloom(bloom) {
975
1357
  this._bloom = bloom;
976
1358
  }
977
- /** @internal Built-in libs and external scripts from `settings.addons.json`. */
1359
+ /** @internal Libs (config + required by modules/behaviours) and externals. */
978
1360
  async loadAddons() {
979
- const addons = this.definition.addons;
980
- if (addons) await this._addons.load(addons);
1361
+ await this._addons.load(this.definition.addons ?? {}, this.definition.usedBuiltins ?? []);
981
1362
  }
982
1363
  /** @internal World resources by asset path — result of `loadAssets`. */
983
1364
  setResources(resources) {
984
1365
  this._resources = resources;
1366
+ this._pathByResource.clear();
1367
+ for (const path in resources) {
1368
+ const resource = resources[path];
1369
+ if (resource !== null && typeof resource === "object") this._pathByResource.set(resource, path);
1370
+ }
985
1371
  }
986
1372
  reportProgress(stage, details) {
987
1373
  const progress = {
@@ -990,9 +1376,11 @@ var WorldRuntime = class {
990
1376
  };
991
1377
  this.options.onProgress?.(progress);
992
1378
  }
993
- getUrl = (path) => {
994
- const url = this.definition.assets[path];
995
- if (url === void 0) throw new Error(`World runtime: unknown asset "${path}"`);
1379
+ /** Accepts an asset path or a resource returned by `require` — like play mode. */
1380
+ getUrl = (source) => {
1381
+ const path = typeof source === "string" ? source : this._pathByResource.get(source);
1382
+ const url = path === void 0 ? void 0 : this.definition.assets[path];
1383
+ if (url === void 0) throw new Error(`World runtime: unknown asset "${typeof source === "string" ? source : path ?? "<unknown resource>"}"`);
996
1384
  if (isAbsoluteUrl(url)) return url;
997
1385
  return `${this.options.assetsBaseUrl ?? ""}${url}`;
998
1386
  };
@@ -1027,16 +1415,11 @@ var WorldRuntime = class {
1027
1415
  const patch = getBuiltinBehaviour(uuid)?.requiredPatch;
1028
1416
  if (!patch) continue;
1029
1417
  try {
1030
- await patch(starter);
1418
+ await patch.apply(starter);
1031
1419
  } catch (error) {
1032
1420
  console.error(`Required patch of "${uuid}" failed`, error);
1033
1421
  }
1034
1422
  }
1035
- if (normalizePhysics(this.definition.addons?.modules?.physics)) try {
1036
- await applyRapierPhysicsPatch(starter);
1037
- } catch (error) {
1038
- console.error("Physics module patch failed", error);
1039
- }
1040
1423
  }
1041
1424
  /** Addon (built-in lib / external) → script by path → asset resource. Same
1042
1425
  * resolve order as in play mode. */
@@ -1050,7 +1433,8 @@ var WorldRuntime = class {
1050
1433
  };
1051
1434
  /**
1052
1435
  * Runtime spawn: a prefab (build-time factory) is assembled anew, a regular
1053
- * object is cloned. Returns a detached object — the caller adds it.
1436
+ * object is cloned. The instance is only created and returned — adding it to
1437
+ * the scene hierarchy is up to the caller (`ctx.scene.add(obj)`).
1054
1438
  */
1055
1439
  instantiate = (source, options) => {
1056
1440
  const object = typeof source === "function" ? source() : clone(source);
@@ -1105,6 +1489,7 @@ var WorldRuntime = class {
1105
1489
  this._styleElements.length = 0;
1106
1490
  this._executedScripts.clear();
1107
1491
  this.emitter.removeAllListeners();
1492
+ this.stores.clear();
1108
1493
  }
1109
1494
  /** Narrow bloom facade for scripts: mode is read-only, params are live
1110
1495
  * (uniforms, can be animated per frame). `null` — bloom is disabled. */
@@ -1144,18 +1529,22 @@ var WorldRuntime = class {
1144
1529
  get ctx() {
1145
1530
  return runtime.ctx;
1146
1531
  },
1532
+ /** Short path to the chat module, same as in play mode. */
1533
+ get chat() {
1534
+ return runtime.ctx.modules.chat;
1535
+ },
1147
1536
  events: this.emitter,
1148
1537
  emitter: this.emitter,
1538
+ stores: this.stores,
1149
1539
  rendering: { get bloom() {
1150
1540
  return runtime.bloomApi;
1151
1541
  } }
1152
1542
  },
1543
+ modules: lazyProxy(() => runtime.ctx.modules),
1153
1544
  require: this.require,
1154
1545
  instantiate: this.instantiate,
1155
1546
  getUrl: this.getUrl,
1156
- THREE,
1157
- TSL,
1158
- van
1547
+ ...BUILTIN_GLOBALS
1159
1548
  };
1160
1549
  return this._scriptContext;
1161
1550
  }
@@ -1177,7 +1566,7 @@ function isAbsoluteUrl(url) {
1177
1566
  }
1178
1567
  //#endregion
1179
1568
  //#region packages/world-runtime/src/version.gen.ts
1180
- var RUNTIME_VERSION = "0.1.0";
1569
+ var RUNTIME_VERSION = "0.4.2";
1181
1570
  //#endregion
1182
1571
  //#region packages/world-runtime/src/version.ts
1183
1572
  /**
@@ -1243,7 +1632,10 @@ async function createWorld(definition, runtime) {
1243
1632
  scene: new THREE.Scene(),
1244
1633
  autoInitRenderer: false
1245
1634
  }, runtime);
1246
- world.addModules(createWorldModules("prod"));
1635
+ world.addModules(await createWorldModules("prod", {
1636
+ addons: runtime.addons,
1637
+ modules: runtime.definition.addons?.modules
1638
+ }));
1247
1639
  applyRendererConfig(renderer, config);
1248
1640
  if (config.bloom?.enabled) {
1249
1641
  if (config.bloom.mode === "emissive") enableSceneEmissiveMrt(world.ctx.scenePass);
@@ -1678,7 +2070,7 @@ var UNIFORM_DEFAULTS = {
1678
2070
  texture: () => getDefaultResource(DefaultResourceId.Texture)
1679
2071
  };
1680
2072
  //#endregion
1681
- //#region packages/world-runtime/src/settings/externals.ts
2073
+ //#region packages/world-runtime/src/addons/externals.ts
1682
2074
  var EXTERNAL_TYPES = ["umd", "module"];
1683
2075
  //#endregion
1684
2076
  //#region packages/world-runtime/src/rendering/renderer-options.ts
@@ -1704,4 +2096,4 @@ var TONE_MAPPING_NAMES = [
1704
2096
  "Neutral"
1705
2097
  ];
1706
2098
  //#endregion
1707
- export { AddonsRuntime, AssetLoaders, AudioSource, BLOOM_MODES, BUILTIN_BEHAVIOURS, BUILTIN_BEHAVIOUR_LIST, DEFAULT_RESOURCE_IDS, DefaultResourceId, EXTERNAL_TYPES, EnvModule, FontSource, HtmlHudModule, InputSystemModule, LIBS, LIB_DESCRIPTORS, LIB_IDS, LOADING_MANAGER, LibId, MODULE_IDS, ModuleId, 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, getRadiusScale, isDefaultResourceId, isMobileDevice, isUniformType, lazyRapier, libByRequireKey, loadGeometry, loadHdr, loadModel, loadTexture, 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 };