@kvyverse/world-runtime 0.6.3 → 0.6.8

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
@@ -102,8 +102,8 @@ world.stores.set("difficulty", "hard");
102
102
  and the world format version, and refuses to load on an incompatible runtime.
103
103
  Re-export the world after a major runtime update.
104
104
  - **Not everything crosses over.** Platform features (multiplayer server, user
105
- state, launcher worlds, the editor's debug overlay) stay in Kvyverse; the
106
- export report lists whatever your particular world loses.
105
+ state, switching the page to another world, the editor's debug overlay) stay in
106
+ Kvyverse; the export report lists whatever your particular world loses.
107
107
 
108
108
  ## License
109
109
 
@@ -1,28 +1,29 @@
1
- import { t as CHAT_COMMAND_KEY } from "./chat.module-descriptor-BTsep-Kq.js";
1
+ import { t as CHAT_COMMAND_KEY } from "./chat.module-descriptor-CGeOaq16.js";
2
2
  import { ContextModule, ThreeContextEvents } from "three-start";
3
3
  import van from "vanjs-core";
4
4
  //#region packages/world-runtime/src/modules/chat/chat.css?inline
5
- var chat_default = "/* Neutral base for the chat input: only what it breaks without on real devices.\n Looks are left to the world. */\n\n.kvy-chat {\n position: absolute;\n left: 0;\n bottom: 0;\n width: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n /* On-screen keyboard: ChatDom writes the overlap height here. */\n padding-bottom: var(--kvy-chat-keyboard-inset, 0px);\n /* Click-through like the HUD around it: the root stretches over the whole feed,\n so catching events here would kill clicks on the world behind the messages.\n Only the input takes events back. */\n pointer-events: none;\n}\n\n.kvy-chat__form {\n display: flex;\n margin: 0;\n}\n\n.kvy-chat__input {\n flex: 1 1 auto;\n min-width: 0;\n /* 16px — below that iOS Safari zooms the page on focus. */\n font: inherit;\n font-size: 16px;\n line-height: 1.25;\n color: inherit;\n background: transparent;\n border: 0;\n outline: none;\n padding: 4px 6px;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 0;\n /* The one interactive part of the chat. */\n pointer-events: auto;\n /* No 300ms delay and no double-tap zoom on the input. */\n touch-action: manipulation;\n}\n\n/* The message feed is a TTL queue, not a scroll container. It sticks to the\n bottom and extra lines are cut from the top — no scrollbar here by design. */\n.kvy-chat__feed {\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n max-height: 40vh;\n overflow: hidden;\n /* Belt and braces: the root is already click-through, but a world restyling it\n must not accidentally make messages catch clicks. */\n pointer-events: none;\n}\n\n.kvy-chat__line {\n opacity: 1;\n transition: opacity 300ms ease;\n overflow-wrap: anywhere;\n}\n\n/* The line has outlived its time: it fades but stays in the DOM — opening the\n chat shows it again instead of rebuilding the feed. */\n.kvy-chat__line--faded {\n opacity: 0;\n}\n\n/* Hidden submit button: backs up implicit submission in old webviews. */\n.kvy-chat__submit {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n\n/* Toggle mode: the input is hidden until entered (Enter on the canvas).\n Opacity, not visibility/display: an element hidden by those cannot be focused\n programmatically, leaving nothing to open the chat with. */\n.kvy-chat--idle-hidden .kvy-chat__input {\n opacity: 0;\n pointer-events: none;\n}\n";
5
+ var chat_default = "/* Neutral base for the chat input: only what it breaks without on real devices.\n Looks are left to the world. */\n\n.kvy-chat {\n position: absolute;\n left: 0;\n bottom: 0;\n width: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n /* On-screen keyboard: ChatDom writes the overlap height here. */\n padding-bottom: var(--kvy-chat-keyboard-inset, 0px);\n /* Click-through like the HUD around it: the root stretches over the whole feed,\n so catching events here would kill clicks on the world behind the messages.\n Only the input takes events back. */\n pointer-events: none;\n}\n\n.kvy-chat__form {\n display: flex;\n}\n\n.kvy-chat__input {\n flex: 1 1 auto;\n min-width: 0;\n /* 16px — below that iOS Safari zooms the page on focus. */\n font: inherit;\n font-size: 16px;\n line-height: 1.25;\n color: inherit;\n background: transparent;\n border: 0;\n outline: none;\n padding: 4px 6px;\n -webkit-appearance: none;\n appearance: none;\n border-radius: 0;\n /* The one interactive part of the chat. */\n pointer-events: auto;\n /* No 300ms delay and no double-tap zoom on the input. */\n touch-action: manipulation;\n}\n\n/* The message feed is a TTL queue, not a scroll container. It sticks to the\n bottom and extra lines are cut from the top — no scrollbar here by design. */\n.kvy-chat__feed {\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n max-height: 40vh;\n overflow: hidden;\n /* Belt and braces: the root is already click-through, but a world restyling it\n must not accidentally make messages catch clicks. */\n pointer-events: none;\n}\n\n.kvy-chat__line {\n opacity: 1;\n transition: opacity 300ms ease;\n overflow-wrap: anywhere;\n}\n\n/* The line has outlived its time: it fades but stays in the DOM — opening the\n chat shows it again instead of rebuilding the feed. */\n.kvy-chat__line--faded {\n opacity: 0;\n}\n\n/* Toggle mode: the input is hidden until entered (Enter on the canvas).\n Opacity, not visibility/display: an element hidden by those cannot be focused\n programmatically, leaving nothing to open the chat with. */\n.kvy-chat--idle-hidden .kvy-chat__input {\n opacity: 0;\n pointer-events: none;\n}\n";
6
6
  //#endregion
7
7
  //#region packages/world-runtime/src/modules/chat/ChatDom.ts
8
- var { button, div, form, input } = van.tags;
8
+ var { div, input } = van.tags;
9
9
  var STYLE_ID = "kvyverse-chat-styles";
10
+ /** How long a send waits for the keyboard to commit the typed word (ms). */
11
+ var COMPOSITION_COMMIT_TIMEOUT = 150;
10
12
  /**
11
13
  * Chat DOM: the message feed plus the input field.
12
14
  *
13
- * Built to survive real devices: submit through a `<form>` (Enter/Go/Send on any
14
- * mobile keyboard), an IME composition guard, and lifting above the on-screen
15
- * keyboard via `visualViewport`. Details —
16
- * `./README.md`.
15
+ * Built to survive real devices: sending on the Enter keydown (no `<form>`: a
16
+ * sandboxed host may block form submission), an IME composition guard, and
17
+ * lifting above the on-screen keyboard via `visualViewport`. Details — `./README.md`.
17
18
  */
18
19
  var ChatDom = class ChatDom {
19
20
  root;
20
21
  feed;
21
- form;
22
22
  input;
23
23
  _options;
24
24
  _lines = [];
25
- _isComposing = false;
25
+ /** Timer of a send waiting for the keyboard to commit its composition. */
26
+ _pendingSubmit = 0;
26
27
  _viewportRaf = 0;
27
28
  _mounted = false;
28
29
  constructor(options = {}) {
@@ -40,16 +41,8 @@ var ChatDom = class ChatDom {
40
41
  this.input.spellcheck = false;
41
42
  this.input.setAttribute("autocorrect", "off");
42
43
  this.input.setAttribute("autocapitalize", "sentences");
43
- this.form = form({
44
- class: "kvy-chat__form",
45
- novalidate: true
46
- }, this.input, button({
47
- class: "kvy-chat__submit",
48
- type: "submit",
49
- tabindex: "-1"
50
- }, "Send"));
51
44
  this.feed = div({ class: "kvy-chat__feed" });
52
- this.root = div({ class: "kvy-chat" }, this.feed, this.form);
45
+ this.root = div({ class: "kvy-chat" }, this.feed, div({ class: "kvy-chat__form" }, this.input));
53
46
  }
54
47
  /** The focus policy is only known at runtime (mobile/desktop, canvas). */
55
48
  setFocusPolicy(policy) {
@@ -185,10 +178,8 @@ var ChatDom = class ChatDom {
185
178
  line.el.classList.toggle("kvy-chat__line--faded", faded);
186
179
  }
187
180
  _bind() {
188
- this.form.addEventListener("submit", this._onSubmit);
189
- this.input.addEventListener("compositionstart", this._onCompositionStart);
190
- this.input.addEventListener("compositionend", this._onCompositionEnd);
191
181
  this.input.addEventListener("beforeinput", this._onBeforeInput);
182
+ this.input.addEventListener("compositionend", this._onCompositionEnd);
192
183
  this.input.addEventListener("keydown", this._onKeyDown);
193
184
  this.input.addEventListener("keyup", this._stopPropagation);
194
185
  this.input.addEventListener("focus", this._onFocus);
@@ -200,10 +191,9 @@ var ChatDom = class ChatDom {
200
191
  }
201
192
  }
202
193
  _unbind() {
203
- this.form.removeEventListener("submit", this._onSubmit);
204
- this.input.removeEventListener("compositionstart", this._onCompositionStart);
205
- this.input.removeEventListener("compositionend", this._onCompositionEnd);
194
+ this._cancelPendingSubmit();
206
195
  this.input.removeEventListener("beforeinput", this._onBeforeInput);
196
+ this.input.removeEventListener("compositionend", this._onCompositionEnd);
207
197
  this.input.removeEventListener("keydown", this._onKeyDown);
208
198
  this.input.removeEventListener("keyup", this._stopPropagation);
209
199
  this.input.removeEventListener("focus", this._onFocus);
@@ -218,9 +208,7 @@ var ChatDom = class ChatDom {
218
208
  this._viewportRaf = 0;
219
209
  }
220
210
  }
221
- _onSubmit = (e) => {
222
- e.preventDefault();
223
- if (this._isComposing) return;
211
+ _submit() {
224
212
  const text = this.input.value.trim();
225
213
  this.clear();
226
214
  if (!text) return;
@@ -231,25 +219,54 @@ var ChatDom = class ChatDom {
231
219
  }
232
220
  this.blur();
233
221
  this._options.returnFocusTo?.focus({ preventScroll: true });
234
- };
235
- _onCompositionStart = () => {
236
- this._isComposing = true;
222
+ }
223
+ /**
224
+ * Sends now or, while the keyboard still holds the typed word as a composition
225
+ * (Android), once it commits the word: clearing the field under a live
226
+ * composition can bring the word back into it.
227
+ */
228
+ _requestSubmit(isComposing) {
229
+ if (!isComposing) {
230
+ this._cancelPendingSubmit();
231
+ this._submit();
232
+ return;
233
+ }
234
+ if (this._pendingSubmit) return;
235
+ this._pendingSubmit = window.setTimeout(this._flushSubmit, COMPOSITION_COMMIT_TIMEOUT);
236
+ }
237
+ _cancelPendingSubmit() {
238
+ if (!this._pendingSubmit) return;
239
+ clearTimeout(this._pendingSubmit);
240
+ this._pendingSubmit = 0;
241
+ }
242
+ _flushSubmit = () => {
243
+ this._pendingSubmit = 0;
244
+ this._submit();
237
245
  };
238
246
  _onCompositionEnd = () => {
239
- this._isComposing = false;
240
- };
241
- /** Some Android keyboards send no Enter keydown, only `insertLineBreak`. */
242
- _onBeforeInput = (e) => {
243
- if (e.inputType !== "insertLineBreak") return;
244
- e.preventDefault();
245
- this.form.requestSubmit();
247
+ if (!this._pendingSubmit) return;
248
+ clearTimeout(this._pendingSubmit);
249
+ this._pendingSubmit = window.setTimeout(this._flushSubmit, 0);
246
250
  };
247
251
  _onKeyDown = (e) => {
248
252
  e.stopPropagation();
253
+ if (isImeProcessedKey(e)) return;
254
+ if (e.key === "Enter") {
255
+ e.preventDefault();
256
+ this._requestSubmit(e.isComposing);
257
+ return;
258
+ }
249
259
  if (e.key !== "Escape") return;
250
260
  this.blur();
251
261
  this._options.returnFocusTo?.focus({ preventScroll: true });
252
262
  };
263
+ /** Fallback for keyboards that insert a line break instead of sending Enter. */
264
+ _onBeforeInput = (e) => {
265
+ const { inputType, data, isComposing } = e;
266
+ if (!(inputType === "insertLineBreak" || inputType === "insertParagraph" || inputType === "insertText" && data === "\n")) return;
267
+ e.preventDefault();
268
+ this._requestSubmit(isComposing);
269
+ };
253
270
  _stopPropagation = (e) => {
254
271
  e.stopPropagation();
255
272
  };
@@ -298,6 +315,14 @@ var ChatDom = class ChatDom {
298
315
  document.head.append(style);
299
316
  }
300
317
  };
318
+ /**
319
+ * The key went to the IME, not the page: every engine marks it with `keyCode 229`
320
+ * (Safari even after `compositionend`). Not `isComposing`: on Android the typed word
321
+ * is always a composition, and an Enter that reaches the page there means "send".
322
+ */
323
+ function isImeProcessedKey(e) {
324
+ return e.keyCode === 229;
325
+ }
301
326
  //#endregion
302
327
  //#region packages/world-runtime/src/modules/chat/ChatModule.ts
303
328
  /**
@@ -18,7 +18,7 @@ var SightInteractableArea = class SightInteractableArea extends Object3DBehaviou
18
18
  this.object.getWorldPosition(this._initWorldPos);
19
19
  }
20
20
  onDisable() {
21
- this._deactivate();
21
+ this._hidePrompt();
22
22
  }
23
23
  setRadius(r) {
24
24
  this.radius = r;
@@ -38,10 +38,10 @@ var SightInteractableArea = class SightInteractableArea extends Object3DBehaviou
38
38
  if (this._active === active) return;
39
39
  this._active = active;
40
40
  this.emit("activechanged", active);
41
- if (active) this._activate();
42
- else this._deactivate();
41
+ if (active) this._showPrompt();
42
+ else this._hidePrompt();
43
43
  }
44
- _activate() {
44
+ _showPrompt() {
45
45
  const modules = this.modules;
46
46
  SightInteractableArea._CURRENT = this.object.id;
47
47
  modules.html.setCrosshair(this.hint);
@@ -50,7 +50,7 @@ var SightInteractableArea = class SightInteractableArea extends Object3DBehaviou
50
50
  modules.html.crosshair.addEventListener("pointerdown", this._interact);
51
51
  } else this.ctx.renderer.domElement.addEventListener("mousedown", this._interact);
52
52
  }
53
- _deactivate() {
53
+ _hidePrompt() {
54
54
  const modules = this.modules;
55
55
  if (SightInteractableArea._CURRENT === this.object.id) {
56
56
  SightInteractableArea._CURRENT = null;
@@ -66,7 +66,7 @@ var SightInteractableArea = class SightInteractableArea extends Object3DBehaviou
66
66
  const current = SightInteractableArea._CURRENT;
67
67
  if (!(current === this.object.id || current === null)) return;
68
68
  this.interactEnabled = false;
69
- this._deactivate();
69
+ this._hidePrompt();
70
70
  this._active = false;
71
71
  this.emit("interaction");
72
72
  console.log("interaction with ", this.object.name);
@@ -9,10 +9,9 @@ import type { AddonsConfig, WorldImports } from "./types";
9
9
  * by a module/behaviour through `requiredLibs`. Scripts reach all of them by
10
10
  * `require("<key>")`.
11
11
  *
12
- * This is the `addons` ctx module, but it also works without a ctx: launcher
13
- * worlds have no 3d context while `require` still has to work, so there the
14
- * instance is used directly. How to import optional packages is up to the
15
- * runtime owner (see `WorldImports`).
12
+ * This is the `addons` ctx module, but it is loaded before the ctx exists: a
13
+ * module may need its lib (physics needs rapier) the moment the ctx is built.
14
+ * How to import optional packages is up to the runtime owner (see `WorldImports`).
16
15
  */
17
16
  export declare class AddonsRuntime extends ContextModule implements AddonsModuleApi {
18
17
  private readonly _imports;
@@ -17,7 +17,7 @@ export default class SightInteractableArea extends Object3DBehaviour<{
17
17
  onDisable(): void;
18
18
  setRadius(r: number): void;
19
19
  onBeforeRender(): void;
20
- private _activate;
21
- private _deactivate;
20
+ private _showPrompt;
21
+ private _hidePrompt;
22
22
  private _interact;
23
23
  }
@@ -49,7 +49,7 @@ var chatModuleDescriptor = {
49
49
  normalize: (raw) => normalizeChat(raw),
50
50
  defaultConfig: () => ({ ...DEFAULTS }),
51
51
  create: async (config) => {
52
- const { ChatModule } = await import("./ChatModule-2tiWS_Yr.js");
52
+ const { ChatModule } = await import("./ChatModule-BFNVVLA8.js");
53
53
  return new ChatModule(config);
54
54
  },
55
55
  schema: {
package/dist/index.d.ts CHANGED
@@ -7,8 +7,9 @@ export { WorldEvent } from "./world-events";
7
7
  export { lazyProxy } from "./utils/lazy";
8
8
  export { WorldStores } from "./world-stores";
9
9
  export type { WorldStore } from "./world-stores";
10
- export { createWorldModules, EnvModule, HtmlHudModule, InputSystemModule, isMobileDevice, SoundsModule, UtilsModule, } from "./modules";
10
+ export { createWorldModules, EnvModule, HtmlHudModule, InputSystemModule, isMobileDevice, NullPlatformModule, SoundsModule, UtilsModule, } from "./modules";
11
11
  export type { EnvMode, WorldModules } from "./modules";
12
+ export type { PlatformModuleApi, PlatformPlayerApi, PlatformPlayerStorageApi, WorldName, WorldNamesRegister, WorldOpenParams, } from "./modules/contracts/platform.module-api";
12
13
  export { DEFAULT_RESOURCE_IDS, DefaultResourceId, getDefaultResource, isDefaultResourceId, } from "./resources/default-resources";
13
14
  export type { DefaultResourceIdValue, DefaultResourceTypes, } from "./resources/default-resources";
14
15
  export { AssetLoaders, configureAssetLoaders, LOADING_MANAGER } from "./resources/loaders";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
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";
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-CGeOaq16.js";
2
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-CPwXvdYs.js";
3
3
  import { n as getRadiusScale, t as getHalfheightRadiusScale } from "./get-scale-from-obj-B7JlVPni.js";
4
4
  import * as THREE from "three/webgpu";
@@ -875,6 +875,36 @@ function isUiKeyboardTarget(target) {
875
875
  }
876
876
  }
877
877
  //#endregion
878
+ //#region packages/world-runtime/src/modules/platform/NullPlatformModule.ts
879
+ var NO_PARAMS = Object.freeze({});
880
+ var NULL_PLAYER = {
881
+ username: null,
882
+ storage: {
883
+ get: () => Promise.resolve(null),
884
+ save: () => Promise.resolve()
885
+ }
886
+ };
887
+ /**
888
+ * `world.platform` без платформы: self-hosted экспорт, мир без моста. Все вызовы
889
+ * безопасны — чтения пустые, записи ничего не делают. Мир на платформе получает
890
+ * вместо него реализацию хоста, переданную в `createWorldModules({ platform })`.
891
+ */
892
+ var NullPlatformModule = class extends ContextModule {
893
+ available = false;
894
+ player = NULL_PLAYER;
895
+ openedWith = NO_PARAMS;
896
+ referrer = null;
897
+ openWorld() {
898
+ return Promise.resolve(false);
899
+ }
900
+ resolveWorld() {
901
+ return Promise.resolve(null);
902
+ }
903
+ openLink() {
904
+ return Promise.resolve(false);
905
+ }
906
+ };
907
+ //#endregion
878
908
  //#region packages/world-runtime/src/modules/SoundsModule.ts
879
909
  var SoundsModule = class extends ContextModule {
880
910
  global = Howler;
@@ -913,6 +943,7 @@ async function createWorldModules(mode = "prod", options) {
913
943
  env: new EnvModule(mode),
914
944
  html: new HtmlHudModule(),
915
945
  input: new InputSystemModule(),
946
+ platform: options.platform ?? new NullPlatformModule(),
916
947
  sounds: new SoundsModule(),
917
948
  utils: new UtilsModule()
918
949
  };
@@ -963,7 +994,7 @@ var entries = [
963
994
  {
964
995
  uuid: "sightinteractablearea",
965
996
  name: "SightInteractableArea",
966
- load: () => loadDefault(import("./SightInteractableArea-DrqyB41W.js"))
997
+ load: () => loadDefault(import("./SightInteractableArea-BK8IXNIl.js"))
967
998
  },
968
999
  {
969
1000
  uuid: "meshinstansing",
@@ -1155,10 +1186,9 @@ function collectRequiredLibs(addons, usedBuiltinUuids = []) {
1155
1186
  * by a module/behaviour through `requiredLibs`. Scripts reach all of them by
1156
1187
  * `require("<key>")`.
1157
1188
  *
1158
- * This is the `addons` ctx module, but it also works without a ctx: launcher
1159
- * worlds have no 3d context while `require` still has to work, so there the
1160
- * instance is used directly. How to import optional packages is up to the
1161
- * runtime owner (see `WorldImports`).
1189
+ * This is the `addons` ctx module, but it is loaded before the ctx exists: a
1190
+ * module may need its lib (physics needs rapier) the moment the ctx is built.
1191
+ * How to import optional packages is up to the runtime owner (see `WorldImports`).
1162
1192
  */
1163
1193
  var AddonsRuntime = class extends ContextModule {
1164
1194
  _imports;
@@ -1578,7 +1608,7 @@ function isAbsoluteUrl(url) {
1578
1608
  }
1579
1609
  //#endregion
1580
1610
  //#region packages/world-runtime/src/version.gen.ts
1581
- var RUNTIME_VERSION = "0.6.3";
1611
+ var RUNTIME_VERSION = "0.6.8";
1582
1612
  //#endregion
1583
1613
  //#region packages/world-runtime/src/version.ts
1584
1614
  /**
@@ -2109,4 +2139,4 @@ var TONE_MAPPING_NAMES = [
2109
2139
  "Neutral"
2110
2140
  ];
2111
2141
  //#endregion
2112
- 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 };
2142
+ 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, NullPlatformModule, 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 };
@@ -28,19 +28,18 @@ export interface ChatDomOptions {
28
28
  /**
29
29
  * Chat DOM: the message feed plus the input field.
30
30
  *
31
- * Built to survive real devices: submit through a `<form>` (Enter/Go/Send on any
32
- * mobile keyboard), an IME composition guard, and lifting above the on-screen
33
- * keyboard via `visualViewport`. Details —
34
- * `./README.md`.
31
+ * Built to survive real devices: sending on the Enter keydown (no `<form>`: a
32
+ * sandboxed host may block form submission), an IME composition guard, and
33
+ * lifting above the on-screen keyboard via `visualViewport`. Details — `./README.md`.
35
34
  */
36
35
  export declare class ChatDom {
37
36
  readonly root: HTMLDivElement;
38
37
  readonly feed: HTMLDivElement;
39
- readonly form: HTMLFormElement;
40
38
  readonly input: HTMLInputElement;
41
39
  private readonly _options;
42
40
  private _lines;
43
- private _isComposing;
41
+ /** Timer of a send waiting for the keyboard to commit its composition. */
42
+ private _pendingSubmit;
44
43
  private _viewportRaf;
45
44
  private _mounted;
46
45
  constructor(options?: ChatDomOptions);
@@ -77,12 +76,19 @@ export declare class ChatDom {
77
76
  private _setFaded;
78
77
  private _bind;
79
78
  private _unbind;
80
- private readonly _onSubmit;
81
- private readonly _onCompositionStart;
79
+ private _submit;
80
+ /**
81
+ * Sends now or, while the keyboard still holds the typed word as a composition
82
+ * (Android), once it commits the word: clearing the field under a live
83
+ * composition can bring the word back into it.
84
+ */
85
+ private _requestSubmit;
86
+ private _cancelPendingSubmit;
87
+ private readonly _flushSubmit;
82
88
  private readonly _onCompositionEnd;
83
- /** Some Android keyboards send no Enter keydown, only `insertLineBreak`. */
84
- private readonly _onBeforeInput;
85
89
  private readonly _onKeyDown;
90
+ /** Fallback for keyboards that insert a line break instead of sending Enter. */
91
+ private readonly _onBeforeInput;
86
92
  private readonly _stopPropagation;
87
93
  private readonly _onFocus;
88
94
  private readonly _onBlur;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Public API of the `platform` module — the world's link to the Kvyverse
3
+ * platform. Off the platform (self-hosted export) every call is a safe no-op:
4
+ * nothing throws, reads resolve to empty values.
5
+ */
6
+ export interface PlatformModuleApi {
7
+ /** The world runs on the platform and the link is established. */
8
+ readonly available: boolean;
9
+ readonly player: PlatformPlayerApi;
10
+ /** Parameters this world was opened with; empty when opened from its own page. */
11
+ readonly openedWith: Readonly<WorldOpenParams>;
12
+ /**
13
+ * The world that switched to this one with `openWorld`; `null` when the
14
+ * player came straight to this page (a reload included).
15
+ */
16
+ readonly referrer: WorldName | null;
17
+ /**
18
+ * Asks the platform to put another world of the same author on this page,
19
+ * in place of this one. `state` reaches it as its `openedWith`. Resolves to
20
+ * `false` when the platform refuses (another author, no such world) or off
21
+ * the platform; on success this world is torn down.
22
+ */
23
+ openWorld: (world: WorldName, state?: WorldOpenParams) => Promise<boolean>;
24
+ /**
25
+ * Checks a name that came at runtime (a chat command, player input) against
26
+ * the author's worlds: resolves to its url-name, typed as `WorldName`, or
27
+ * `null` when there is no such world of this author, or off the platform.
28
+ * Takes a url-name or an id.
29
+ */
30
+ resolveWorld: (world: string) => Promise<WorldName | null>;
31
+ /**
32
+ * Asks the platform to open an external link. The platform shows the address
33
+ * and asks the player; resolves to `false` when they decline, when the
34
+ * address is not http(s), or off the platform.
35
+ */
36
+ openLink: (url: string) => Promise<boolean>;
37
+ }
38
+ /** Parameters a world is opened with. Values are strings. */
39
+ export type WorldOpenParams = Record<string, string>;
40
+ /**
41
+ * Filled in by the Kvyverse editor for the author's own world: `worlds` lists
42
+ * the author's worlds. Not filled (anywhere else) — any string.
43
+ */
44
+ export interface WorldNamesRegister {
45
+ }
46
+ /** Url-name of one of the author's worlds: what `openWorld` takes and `referrer` holds. */
47
+ export type WorldName = WorldNamesRegister extends {
48
+ worlds: infer TWorld extends string;
49
+ } ? TWorld : string;
50
+ export interface PlatformPlayerApi {
51
+ /** Display name of the current player; `null` for a guest or off-platform. */
52
+ readonly username: string | null;
53
+ /** Server-side storage for this world + player pair. */
54
+ readonly storage: PlatformPlayerStorageApi;
55
+ }
56
+ /** Storage of one world for one player. Never throws: no platform — no value. */
57
+ export interface PlatformPlayerStorageApi {
58
+ get: () => Promise<string | null>;
59
+ save: (data: string) => Promise<void>;
60
+ }
@@ -7,13 +7,16 @@ import { UtilsModule } from "./UtilsModule";
7
7
  import type { AddonsRuntime } from "../addons/AddonsRuntime";
8
8
  import type { AddonsConfig } from "../addons/types";
9
9
  import type { EnvMode } from "./EnvModule";
10
+ import type { PlatformModuleApi } from "./contracts/platform.module-api";
10
11
  import type { KvyverseModules } from "./registry";
12
+ import type { ContextModule } from "three-start";
11
13
  export type { ChatModule } from "./chat/ChatModule";
12
14
  export type { ChatDom, ChatDomOptions } from "./chat/ChatDom";
13
15
  export { ControlsModule } from "./controls/ControlsModule";
14
16
  export { EnvModule } from "./EnvModule";
15
17
  export { HtmlHudModule } from "./HtmlHudModule";
16
18
  export { InputSystemModule } from "./InputSystemModule";
19
+ export { NullPlatformModule } from "./platform/NullPlatformModule";
17
20
  export { SoundsModule } from "./SoundsModule";
18
21
  export { UtilsModule } from "./UtilsModule";
19
22
  export { isMobileDevice } from "./is-mobile-device";
@@ -25,6 +28,7 @@ export type CoreWorldModules = {
25
28
  env: EnvModule;
26
29
  html: HtmlHudModule;
27
30
  input: InputSystemModule;
31
+ platform: ContextModule & PlatformModuleApi;
28
32
  sounds: SoundsModule;
29
33
  utils: UtilsModule;
30
34
  };
@@ -35,6 +39,8 @@ export interface CreateWorldModulesOptions {
35
39
  addons: AddonsRuntime;
36
40
  /** `settings.addons.json → modules`: which pluggable modules are on, and how. */
37
41
  modules?: AddonsConfig["modules"];
42
+ /** `world.platform`: the host's implementation; omitted off the platform, and it turns no-op. */
43
+ platform?: ContextModule & PlatformModuleApi;
38
44
  }
39
45
  /**
40
46
  * Assembles the world's ctx modules: core ones always, pluggable ones per config.
@@ -0,0 +1,16 @@
1
+ import { ContextModule } from "three-start";
2
+ import type { PlatformModuleApi, PlatformPlayerApi, WorldOpenParams } from "../contracts/platform.module-api";
3
+ /**
4
+ * `world.platform` без платформы: self-hosted экспорт, мир без моста. Все вызовы
5
+ * безопасны — чтения пустые, записи ничего не делают. Мир на платформе получает
6
+ * вместо него реализацию хоста, переданную в `createWorldModules({ platform })`.
7
+ */
8
+ export declare class NullPlatformModule extends ContextModule implements PlatformModuleApi {
9
+ readonly available = false;
10
+ readonly player: PlatformPlayerApi;
11
+ readonly openedWith: Readonly<WorldOpenParams>;
12
+ readonly referrer: null;
13
+ openWorld(): Promise<boolean>;
14
+ resolveWorld(): Promise<null>;
15
+ openLink(): Promise<boolean>;
16
+ }
@@ -1,3 +1,4 @@
1
+ import type { ContextModule } from "three-start";
1
2
  import type { RapierPhysics } from "./physics/RapierPhysics";
2
3
  import type { AddonsRuntime } from "../addons/AddonsRuntime";
3
4
  import type { ChatModule } from "./chat/ChatModule";
@@ -5,6 +6,7 @@ import type { ControlsModule } from "./controls/ControlsModule";
5
6
  import type { EnvModule } from "./EnvModule";
6
7
  import type { HtmlHudModule } from "./HtmlHudModule";
7
8
  import type { InputSystemModule } from "./InputSystemModule";
9
+ import type { PlatformModuleApi } from "./contracts/platform.module-api";
8
10
  import type { SoundsModule } from "./SoundsModule";
9
11
  import type { UtilsModule } from "./UtilsModule";
10
12
  /**
@@ -23,6 +25,8 @@ export interface KvyverseModules {
23
25
  controls: ControlsModule;
24
26
  env: EnvModule;
25
27
  html: HtmlHudModule;
28
+ /** Реализацию даёт тот, кто собирает мир: хост на платформе или пустая. */
29
+ platform: ContextModule & PlatformModuleApi;
26
30
  sounds: SoundsModule;
27
31
  utils: UtilsModule;
28
32
  rapier: RapierPhysics;
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.6.3";
1
+ export declare const RUNTIME_VERSION = "0.6.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvyverse/world-runtime",
3
- "version": "0.6.3",
3
+ "version": "0.6.8",
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": "Kvyverse, Inc.",