@kvyverse/world-runtime 0.1.0 → 0.3.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 +78 -60
- package/dist/{BallCollider-8ZrxReEd.js → BallCollider-3L5vOrcL.js} +2 -2
- package/dist/{CapsuleCollider-bynyf6Mc.js → CapsuleCollider-CBTsR-Ys.js} +2 -2
- package/dist/ChatModule-2tiWS_Yr.js +411 -0
- package/dist/{Collider-4_rCTHVA.js → Collider-CPUCG3RA.js} +2 -2
- package/dist/{ConeCollider-DtRZmgK8.js → ConeCollider-C3XU3Y9X.js} +2 -2
- package/dist/{ConvexMeshCollider-BSmDM4o3.js → ConvexMeshCollider-DzNV4HTO.js} +1 -1
- package/dist/{CuboidCollider-CJvq7--s.js → CuboidCollider-Dp1DZoFM.js} +1 -1
- package/dist/{CylinderCollider-CQPSh4tu.js → CylinderCollider-C0h_nhB5.js} +2 -2
- package/dist/{FpsCameraController-BE2EirRj.js → FpsCameraController-2GNWJM5D.js} +1 -1
- package/dist/RapierDebugRenderer-BQy_m-3C.js +73 -0
- package/dist/{RapierFpsPlayerController-VsNfQl6v.js → RapierFpsPlayerController-DthD8dw9.js} +5 -22
- package/dist/{RapierPhysics-C_3b7QOn.js → RapierPhysics-zLwVZpwF.js} +33 -15
- package/dist/{Rigidbody-b7QNF4ev.js → Rigidbody-DLhN8ddv.js} +12 -1
- package/dist/World.d.ts +2 -2
- package/dist/WorldRuntime.d.ts +13 -4
- package/dist/addons/AddonsRuntime.d.ts +35 -0
- package/dist/addons/lib-registry.d.ts +34 -0
- package/dist/addons/types.d.ts +28 -0
- package/dist/behaviours/physics/BallCollider.d.ts +1 -1
- package/dist/behaviours/physics/CapsuleCollider.d.ts +1 -1
- package/dist/behaviours/physics/ConeCollider.d.ts +1 -1
- package/dist/behaviours/physics/ConvexMeshCollider.d.ts +1 -1
- package/dist/behaviours/physics/CuboidCollider.d.ts +1 -1
- package/dist/behaviours/physics/CylinderCollider.d.ts +1 -1
- package/dist/behaviours/physics/HalfspaceCollider.d.ts +1 -1
- package/dist/behaviours/physics/_required-patch.d.ts +10 -6
- package/dist/chat.module-descriptor-BTsep-Kq.js +107 -0
- package/dist/{get-scale-from-obj-QKv3Fig5.js → get-scale-from-obj-B7JlVPni.js} +1 -1
- package/dist/index.d.ts +21 -13
- package/dist/index.js +441 -155
- package/dist/modules/InputSystemModule.d.ts +14 -32
- package/dist/modules/chat/ChatDom.d.ts +100 -0
- package/dist/modules/chat/ChatModule.d.ts +36 -0
- package/dist/modules/chat/chat.module-descriptor.d.ts +43 -0
- package/dist/modules/contracts/addons.module-api.d.ts +15 -0
- package/dist/modules/contracts/chat.module-api.d.ts +52 -0
- package/dist/modules/contracts/controls.module-api.d.ts +58 -0
- package/dist/modules/contracts/html.module-api.d.ts +9 -1
- package/dist/modules/contracts/input.module-api.d.ts +11 -11
- package/dist/modules/contracts/module-events.d.ts +17 -0
- package/dist/modules/contracts/rapier.module-api.d.ts +13 -1
- package/dist/modules/controls/ControlsModule.d.ts +45 -0
- package/dist/modules/controls/KeyboardControlsDriver.d.ts +12 -0
- package/dist/modules/index.d.ts +30 -3
- package/dist/modules/optional-module-descriptor.d.ts +36 -0
- package/dist/modules/optional-modules.d.ts +11 -0
- package/dist/modules/physics/RapierDebugRenderer.d.ts +19 -0
- package/dist/{behaviours/physics/rapier-addon-core → modules/physics}/RapierPhysics.d.ts +13 -3
- package/dist/modules/physics/physics.module-descriptor.d.ts +16 -0
- package/dist/modules/registry.d.ts +7 -3
- package/dist/rendering/SceneBloom.d.ts +1 -1
- package/dist/rendering/renderer-apply.d.ts +1 -1
- package/dist/{settings → rendering}/types.d.ts +1 -12
- package/dist/types.d.ts +10 -3
- package/dist/version.gen.d.ts +1 -1
- package/package.json +9 -1
- package/dist/RapierDebugRenderer-sURZDLP4.js +0 -55
- package/dist/behaviours/physics/rapier-addon-core/RapierDebugRenderer.d.ts +0 -17
- package/dist/modules/contracts/rapierDebug.module-api.d.ts +0 -5
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/settings/AddonsRuntime.d.ts +0 -19
- package/dist/settings/lib-loaders.d.ts +0 -7
- package/dist/settings/lib-registry.d.ts +0 -24
- package/dist/settings/module-registry.d.ts +0 -25
- /package/dist/{settings → addons}/externals.d.ts +0 -0
- /package/dist/behaviours/physics/{rapier-addon-core/Collider.d.ts → Collider.d.ts} +0 -0
- /package/dist/behaviours/physics/{rapier-addon-core/get-scale-from-obj.d.ts → get-scale-from-obj.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,87 +1,105 @@
|
|
|
1
1
|
# @kvyverse/world-runtime
|
|
2
2
|
|
|
3
3
|
Runtime for worlds exported from [Kvyverse](https://kvyverse.com) — a web-based
|
|
4
|
-
3D world editor.
|
|
5
|
-
module; this package is the engine that module runs on.
|
|
4
|
+
3D world editor. When you export a world for self-hosting, Kvyverse gives you a
|
|
5
|
+
generated JS module; this package is the engine that module runs on.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
useful together with a world module built by the Kvyverse editor
|
|
9
|
-
(`Export → Build for self-hosting`).
|
|
7
|
+
Install it only together with such a module: on its own it does nothing.
|
|
10
8
|
|
|
11
|
-
##
|
|
9
|
+
## 1. Export the world from Kvyverse
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
In the editor: **burger menu → Export → Build for self-hosting**. You get a
|
|
12
|
+
`<world>.zip` with `index.js` (the world) and `index.d.ts` (its types), plus a
|
|
13
|
+
dialog with two ready-to-copy snippets — the install command and the import.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
`vanjs-core`, `eventemitter3`) are auto-installed by npm 7+, while optional
|
|
19
|
-
ones (`@dimforge/rapier3d-compat` for physics, `@tweenjs/tween.js`, `animejs`)
|
|
20
|
-
are only needed when the world uses them — the export dialog shows the exact
|
|
21
|
-
install command for your world.
|
|
15
|
+
## 2. Put the world into your project
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
Unzip the folder into your sources, for example `src/my-world/`. Nothing else
|
|
18
|
+
from the archive is needed: your bundler picks the module up like any local file.
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
Asset files (models, textures, audio, fonts) are **not** in the archive — they
|
|
21
|
+
stay on the Kvyverse CDN, and the module carries a table of their URLs.
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
import { loadWorld } from "./my-world-3f9a1c02.js"; // file built by Kvyverse
|
|
23
|
+
## 3. Install the dependencies
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
world
|
|
33
|
-
|
|
25
|
+
Copy the command from the export dialog — it already lists exactly what your
|
|
26
|
+
world needs and pins the runtime version the world was built with, for example:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
npm i @kvyverse/world-runtime@<version> three three-start @dimforge/rapier3d-compat
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
Everything the runtime needs is a **peer dependency**, so nothing is duplicated
|
|
33
|
+
in your `node_modules`:
|
|
34
|
+
|
|
35
|
+
| Package | When |
|
|
36
|
+
| --- | --- |
|
|
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 |
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
The optional ones are listed in the dialog only when the world actually uses
|
|
43
|
+
them. Skip one it does need and the build fails with a clear
|
|
44
|
+
`failed to resolve import` — install it and rebuild.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
Requirements for your project: ES modules, a bundler (Vite, webpack, Rollup, …),
|
|
47
|
+
and a browser with WebGPU (the world renders through `three/webgpu`).
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## 4. Run it
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
redistribution; bundling the unmodified package into your app build is allowed.
|
|
51
|
+
```js
|
|
52
|
+
import { loadWorld } from "./my-world/index.js";
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
const world = await loadWorld({
|
|
55
|
+
onProgress: (p) => console.log(p.stage), // optional loading screen
|
|
56
|
+
});
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
world.mount(document.getElementById("app")); // creates the canvas, starts the render loop
|
|
59
|
+
world.start(); // activates the world's behaviours
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`mount` and `start` are separate on purpose: you can show your own intro or wait
|
|
63
|
+
for a click before the world comes alive. If the world plays audio or locks the
|
|
64
|
+
pointer, call `start()` from a user gesture — browsers require it.
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
`@kvyverse/world-runtime` → `packages/world-runtime/src`; no build needed
|
|
61
|
-
locally. `npm run build:world-runtime` from the repo root produces
|
|
62
|
-
`dist/index.js` + `.d.ts`. The package is covered by the root `tsc -b` and
|
|
63
|
-
`npm run lint`.
|
|
66
|
+
### `loadWorld(options)`
|
|
64
67
|
|
|
65
|
-
|
|
|
68
|
+
| Option | What it does |
|
|
66
69
|
| --- | --- |
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `rendering/` | `SceneBloom` (+ MRT contracts), renderer config application, TSL node/uniform factories (`createTslNode`, `createUniformNode`). |
|
|
71
|
-
| `settings/` | World config types (`RendererConfig`, `AddonsConfig`), lib/module registries, `AddonsRuntime`. |
|
|
72
|
-
| `bootstrap.ts` | The world assembly order — the only place it is defined. |
|
|
73
|
-
| `World.ts` / `WorldRuntime.ts` | `ThreeStart` subclass for the host and the runtime state owner (resources by path, `require`, script execution, styles/fonts, `world.events`). |
|
|
74
|
-
| `world-events.ts` | `WorldEvent` lifecycle enum, shared with the game. |
|
|
70
|
+
| `onProgress` | `({ stage, details }) => void` while the world loads — for your loading screen |
|
|
71
|
+
| `dracoPath` | Folder with your own Draco decoder for compressed models; defaults to Google's public one |
|
|
72
|
+
| `assetsBaseUrl` | Prefix for relative asset paths (only for worlds built that way) |
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
maintenance rules: [`/docs/self-hosted-export-maintenance.md`](../../docs/self-hosted-export-maintenance.md).
|
|
74
|
+
### The `world` object
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
`World` extends `ThreeStart` from `three-start`, so the usual lifecycle and
|
|
77
|
+
context are yours:
|
|
80
78
|
|
|
79
|
+
```js
|
|
80
|
+
world.ctx.scene, world.ctx.camera, world.ctx.renderer
|
|
81
|
+
world.unmount(), world.runLoop(), world.stopLoop(), world.dispose()
|
|
81
82
|
```
|
|
82
|
-
npm run build:world-runtime
|
|
83
|
-
cd packages/world-runtime && npm publish # publishConfig.access = public
|
|
84
|
-
```
|
|
85
83
|
|
|
86
|
-
|
|
87
|
-
|
|
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).
|
|
87
|
+
|
|
88
|
+
## Good to know
|
|
89
|
+
|
|
90
|
+
- **Scripts, behaviours, materials and shaders** written in the editor are baked
|
|
91
|
+
into the module — no Kvyverse code runs at load time to interpret them.
|
|
92
|
+
- **Re-exporting** a world overwrites the same folder with the same file names,
|
|
93
|
+
so your import never changes.
|
|
94
|
+
- **Runtime version.** The module records the runtime version it was built with
|
|
95
|
+
and the world format version, and refuses to load on an incompatible runtime.
|
|
96
|
+
Re-export the world after a major runtime update.
|
|
97
|
+
- **Not everything crosses over.** Platform features (multiplayer server, user
|
|
98
|
+
state, launcher worlds, the editor's debug overlay) stay in Kvyverse; the
|
|
99
|
+
export report lists whatever your particular world loses.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
Proprietary, see [LICENSE.md](./LICENSE.md). In short: free to use (including
|
|
104
|
+
commercially) **only to run worlds exported from Kvyverse**; no modification or
|
|
105
|
+
redistribution; bundling the unmodified package into your app build is allowed.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as getRadiusScale } from "./get-scale-from-obj-
|
|
2
|
-
import { t as Collider } from "./Collider-
|
|
1
|
+
import { n as getRadiusScale } from "./get-scale-from-obj-B7JlVPni.js";
|
|
2
|
+
import { t as Collider } from "./Collider-CPUCG3RA.js";
|
|
3
3
|
//#region packages/world-runtime/src/behaviours/physics/BallCollider.ts
|
|
4
4
|
var BallCollider = class extends Collider {
|
|
5
5
|
type = "Ball";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as getHalfheightRadiusScale } from "./get-scale-from-obj-
|
|
2
|
-
import { t as Collider } from "./Collider-
|
|
1
|
+
import { t as getHalfheightRadiusScale } from "./get-scale-from-obj-B7JlVPni.js";
|
|
2
|
+
import { t as Collider } from "./Collider-CPUCG3RA.js";
|
|
3
3
|
//#region packages/world-runtime/src/behaviours/physics/CapsuleCollider.ts
|
|
4
4
|
var CapsuleCollider = class extends Collider {
|
|
5
5
|
type = "Capsule";
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { t as CHAT_COMMAND_KEY } from "./chat.module-descriptor-BTsep-Kq.js";
|
|
2
|
+
import { ContextModule, ThreeContextEvents } from "three-start";
|
|
3
|
+
import van from "vanjs-core";
|
|
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";
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region packages/world-runtime/src/modules/chat/ChatDom.ts
|
|
8
|
+
var { button, div, form, input } = van.tags;
|
|
9
|
+
var STYLE_ID = "kvyverse-chat-styles";
|
|
10
|
+
/**
|
|
11
|
+
* Chat DOM: the message feed plus the input field.
|
|
12
|
+
*
|
|
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`.
|
|
17
|
+
*/
|
|
18
|
+
var ChatDom = class ChatDom {
|
|
19
|
+
root;
|
|
20
|
+
feed;
|
|
21
|
+
form;
|
|
22
|
+
input;
|
|
23
|
+
_options;
|
|
24
|
+
_lines = [];
|
|
25
|
+
_isComposing = false;
|
|
26
|
+
_viewportRaf = 0;
|
|
27
|
+
_mounted = false;
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this._options = options;
|
|
30
|
+
this.input = input({
|
|
31
|
+
class: "kvy-chat__input",
|
|
32
|
+
type: "text",
|
|
33
|
+
enterkeyhint: "send",
|
|
34
|
+
inputmode: "text",
|
|
35
|
+
autocomplete: "off",
|
|
36
|
+
"aria-label": "Chat message",
|
|
37
|
+
placeholder: options.placeholder ?? "",
|
|
38
|
+
...options.maxLength ? { maxlength: String(options.maxLength) } : {}
|
|
39
|
+
});
|
|
40
|
+
this.input.spellcheck = false;
|
|
41
|
+
this.input.setAttribute("autocorrect", "off");
|
|
42
|
+
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
|
+
this.feed = div({ class: "kvy-chat__feed" });
|
|
52
|
+
this.root = div({ class: "kvy-chat" }, this.feed, this.form);
|
|
53
|
+
}
|
|
54
|
+
/** The focus policy is only known at runtime (mobile/desktop, canvas). */
|
|
55
|
+
setFocusPolicy(policy) {
|
|
56
|
+
this._options.keepFocusAfterSubmit = policy.keepFocusAfterSubmit;
|
|
57
|
+
this._options.returnFocusTo = policy.returnFocusTo;
|
|
58
|
+
}
|
|
59
|
+
get isMounted() {
|
|
60
|
+
return this._mounted;
|
|
61
|
+
}
|
|
62
|
+
/** Whether to hide the input while unfocused (`toggle` mode). */
|
|
63
|
+
setHideWhenIdle(hide) {
|
|
64
|
+
this._options.hideWhenIdle = hide;
|
|
65
|
+
this._syncVisibility();
|
|
66
|
+
}
|
|
67
|
+
get isFocused() {
|
|
68
|
+
return document.activeElement === this.input;
|
|
69
|
+
}
|
|
70
|
+
get value() {
|
|
71
|
+
return this.input.value;
|
|
72
|
+
}
|
|
73
|
+
set value(next) {
|
|
74
|
+
this.input.value = next;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Syncs the feed with the history: new lines are appended at the bottom, lines
|
|
78
|
+
* gone from the history are removed, extra ones are cut from the top. There is
|
|
79
|
+
* no scrolling — what does not fit is not shown.
|
|
80
|
+
*/
|
|
81
|
+
renderMessages(messages) {
|
|
82
|
+
const byId = new Map(this._lines.map((line) => [line.id, line]));
|
|
83
|
+
const next = [];
|
|
84
|
+
for (const message of messages.slice(-this._maxLines)) {
|
|
85
|
+
const existing = byId.get(message.id);
|
|
86
|
+
if (existing) {
|
|
87
|
+
byId.delete(message.id);
|
|
88
|
+
next.push(existing);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
next.push(this._createLine(message));
|
|
92
|
+
}
|
|
93
|
+
for (const stale of byId.values()) this._removeLine(stale);
|
|
94
|
+
this.feed.replaceChildren(...next.map((line) => line.el));
|
|
95
|
+
this._lines = next;
|
|
96
|
+
this._syncLines();
|
|
97
|
+
}
|
|
98
|
+
mount(container) {
|
|
99
|
+
if (this._mounted) return;
|
|
100
|
+
ChatDom._ensureStyles();
|
|
101
|
+
container.append(this.root);
|
|
102
|
+
this._bind();
|
|
103
|
+
this._mounted = true;
|
|
104
|
+
this._syncVisibility();
|
|
105
|
+
this._syncKeyboardInset();
|
|
106
|
+
}
|
|
107
|
+
unmount() {
|
|
108
|
+
if (!this._mounted) return;
|
|
109
|
+
this._unbind();
|
|
110
|
+
this.root.remove();
|
|
111
|
+
this._mounted = false;
|
|
112
|
+
}
|
|
113
|
+
focus() {
|
|
114
|
+
this.input.focus({ preventScroll: true });
|
|
115
|
+
const end = this.input.value.length;
|
|
116
|
+
this.input.setSelectionRange(end, end);
|
|
117
|
+
}
|
|
118
|
+
blur() {
|
|
119
|
+
this.input.blur();
|
|
120
|
+
}
|
|
121
|
+
clear() {
|
|
122
|
+
this.input.value = "";
|
|
123
|
+
}
|
|
124
|
+
dispose() {
|
|
125
|
+
this.unmount();
|
|
126
|
+
for (const line of this._lines) this._removeLine(line);
|
|
127
|
+
this._lines = [];
|
|
128
|
+
}
|
|
129
|
+
/** Keep in the DOM as much as an opened chat could need. */
|
|
130
|
+
get _maxLines() {
|
|
131
|
+
return Math.max(this._options.visibleLines ?? 10, this._options.openedLines ?? 20);
|
|
132
|
+
}
|
|
133
|
+
_createLine(message) {
|
|
134
|
+
const fadeAfter = this._options.fadeAfter ?? 1e4;
|
|
135
|
+
return {
|
|
136
|
+
id: message.id,
|
|
137
|
+
el: div({ class: "kvy-chat__line" }, message.text),
|
|
138
|
+
deadline: fadeAfter > 0 ? message.timestamp + fadeAfter : Infinity,
|
|
139
|
+
timer: 0,
|
|
140
|
+
faded: false
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
_removeLine(line) {
|
|
144
|
+
if (line.timer) clearTimeout(line.timer);
|
|
145
|
+
line.el.remove();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* While the input is focused more lines are shown, including expired ones (an
|
|
149
|
+
* opened chat is meant to be read). Once closed, every line lives by its own
|
|
150
|
+
* deadline again: expired ones fade at once, the rest sit out their remainder.
|
|
151
|
+
*/
|
|
152
|
+
_syncLines() {
|
|
153
|
+
const opened = this.isFocused;
|
|
154
|
+
const visible = opened ? this._options.openedLines ?? 20 : this._options.visibleLines ?? 10;
|
|
155
|
+
const offscreen = this._lines.length - visible;
|
|
156
|
+
this._lines.forEach((line, index) => {
|
|
157
|
+
line.el.style.display = index < offscreen ? "none" : "";
|
|
158
|
+
});
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
for (const line of this._lines) {
|
|
161
|
+
if (line.timer) {
|
|
162
|
+
clearTimeout(line.timer);
|
|
163
|
+
line.timer = 0;
|
|
164
|
+
}
|
|
165
|
+
if (opened) {
|
|
166
|
+
this._setFaded(line, false);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const remaining = line.deadline - now;
|
|
170
|
+
if (remaining <= 0) {
|
|
171
|
+
this._setFaded(line, true);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
this._setFaded(line, false);
|
|
175
|
+
if (remaining === Infinity) continue;
|
|
176
|
+
line.timer = window.setTimeout(() => {
|
|
177
|
+
line.timer = 0;
|
|
178
|
+
this._setFaded(line, true);
|
|
179
|
+
}, remaining);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
_setFaded(line, faded) {
|
|
183
|
+
if (line.faded === faded) return;
|
|
184
|
+
line.faded = faded;
|
|
185
|
+
line.el.classList.toggle("kvy-chat__line--faded", faded);
|
|
186
|
+
}
|
|
187
|
+
_bind() {
|
|
188
|
+
this.form.addEventListener("submit", this._onSubmit);
|
|
189
|
+
this.input.addEventListener("compositionstart", this._onCompositionStart);
|
|
190
|
+
this.input.addEventListener("compositionend", this._onCompositionEnd);
|
|
191
|
+
this.input.addEventListener("beforeinput", this._onBeforeInput);
|
|
192
|
+
this.input.addEventListener("keydown", this._onKeyDown);
|
|
193
|
+
this.input.addEventListener("keyup", this._stopPropagation);
|
|
194
|
+
this.input.addEventListener("focus", this._onFocus);
|
|
195
|
+
this.input.addEventListener("blur", this._onBlur);
|
|
196
|
+
const vv = window.visualViewport;
|
|
197
|
+
if (vv) {
|
|
198
|
+
vv.addEventListener("resize", this._onViewportChange);
|
|
199
|
+
vv.addEventListener("scroll", this._onViewportChange);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
_unbind() {
|
|
203
|
+
this.form.removeEventListener("submit", this._onSubmit);
|
|
204
|
+
this.input.removeEventListener("compositionstart", this._onCompositionStart);
|
|
205
|
+
this.input.removeEventListener("compositionend", this._onCompositionEnd);
|
|
206
|
+
this.input.removeEventListener("beforeinput", this._onBeforeInput);
|
|
207
|
+
this.input.removeEventListener("keydown", this._onKeyDown);
|
|
208
|
+
this.input.removeEventListener("keyup", this._stopPropagation);
|
|
209
|
+
this.input.removeEventListener("focus", this._onFocus);
|
|
210
|
+
this.input.removeEventListener("blur", this._onBlur);
|
|
211
|
+
const vv = window.visualViewport;
|
|
212
|
+
if (vv) {
|
|
213
|
+
vv.removeEventListener("resize", this._onViewportChange);
|
|
214
|
+
vv.removeEventListener("scroll", this._onViewportChange);
|
|
215
|
+
}
|
|
216
|
+
if (this._viewportRaf) {
|
|
217
|
+
cancelAnimationFrame(this._viewportRaf);
|
|
218
|
+
this._viewportRaf = 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
_onSubmit = (e) => {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
if (this._isComposing) return;
|
|
224
|
+
const text = this.input.value.trim();
|
|
225
|
+
this.clear();
|
|
226
|
+
if (!text) return;
|
|
227
|
+
this._options.onSubmit?.(text);
|
|
228
|
+
if (this._options.keepFocusAfterSubmit) {
|
|
229
|
+
this.focus();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.blur();
|
|
233
|
+
this._options.returnFocusTo?.focus({ preventScroll: true });
|
|
234
|
+
};
|
|
235
|
+
_onCompositionStart = () => {
|
|
236
|
+
this._isComposing = true;
|
|
237
|
+
};
|
|
238
|
+
_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();
|
|
246
|
+
};
|
|
247
|
+
_onKeyDown = (e) => {
|
|
248
|
+
e.stopPropagation();
|
|
249
|
+
if (e.key !== "Escape") return;
|
|
250
|
+
this.blur();
|
|
251
|
+
this._options.returnFocusTo?.focus({ preventScroll: true });
|
|
252
|
+
};
|
|
253
|
+
_stopPropagation = (e) => {
|
|
254
|
+
e.stopPropagation();
|
|
255
|
+
};
|
|
256
|
+
_onFocus = () => {
|
|
257
|
+
this._options.onFocusChange?.(true);
|
|
258
|
+
this._syncVisibility();
|
|
259
|
+
this._syncLines();
|
|
260
|
+
this._syncKeyboardInset();
|
|
261
|
+
};
|
|
262
|
+
_onBlur = () => {
|
|
263
|
+
this._options.onFocusChange?.(false);
|
|
264
|
+
this._syncVisibility();
|
|
265
|
+
this._syncLines();
|
|
266
|
+
this._syncKeyboardInset();
|
|
267
|
+
};
|
|
268
|
+
/** In `toggle` mode a hidden input must catch neither clicks nor tab focus. */
|
|
269
|
+
_syncVisibility() {
|
|
270
|
+
const hidden = !!this._options.hideWhenIdle && !this.isFocused;
|
|
271
|
+
this.root.classList.toggle("kvy-chat--idle-hidden", hidden);
|
|
272
|
+
this.input.tabIndex = hidden ? -1 : 0;
|
|
273
|
+
}
|
|
274
|
+
_onViewportChange = () => {
|
|
275
|
+
if (this._viewportRaf) return;
|
|
276
|
+
this._viewportRaf = requestAnimationFrame(() => {
|
|
277
|
+
this._viewportRaf = 0;
|
|
278
|
+
this._syncKeyboardInset();
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* The keyboard changes the layout viewport neither on iOS nor (by default) on
|
|
283
|
+
* Android — anything fixed to the bottom ends up underneath it. We measure the
|
|
284
|
+
* overlap through `visualViewport` and lift the chat by that much.
|
|
285
|
+
*/
|
|
286
|
+
_syncKeyboardInset() {
|
|
287
|
+
const vv = window.visualViewport;
|
|
288
|
+
if (!vv) return;
|
|
289
|
+
const overlap = Math.max(0, window.innerHeight - (vv.height + vv.offsetTop));
|
|
290
|
+
this.root.style.setProperty("--kvy-chat-keyboard-inset", `${Math.round(overlap)}px`);
|
|
291
|
+
}
|
|
292
|
+
/** Styles are inlined once per document: the package ships no separate css file. */
|
|
293
|
+
static _ensureStyles() {
|
|
294
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
295
|
+
const style = document.createElement("style");
|
|
296
|
+
style.id = STYLE_ID;
|
|
297
|
+
style.textContent = chat_default;
|
|
298
|
+
document.head.append(style);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region packages/world-runtime/src/modules/chat/ChatModule.ts
|
|
303
|
+
/**
|
|
304
|
+
* The world chat as a pluggable ctx module: message history plus its own DOM
|
|
305
|
+
* input (`dom`), no React. Enabled from `settings.addons.json → modules`; it
|
|
306
|
+
* mounts its DOM itself. Details and mobile quirks — `./README.md`.
|
|
307
|
+
*/
|
|
308
|
+
var ChatModule = class extends ContextModule {
|
|
309
|
+
dom;
|
|
310
|
+
_maxMessages;
|
|
311
|
+
_openKeys;
|
|
312
|
+
_messages = [];
|
|
313
|
+
_seq = 0;
|
|
314
|
+
constructor(config) {
|
|
315
|
+
super();
|
|
316
|
+
this._maxMessages = config.maxMessages;
|
|
317
|
+
this._openKeys = config.openKeys;
|
|
318
|
+
this.dom = new ChatDom({
|
|
319
|
+
placeholder: config.placeholder,
|
|
320
|
+
maxLength: config.maxLength,
|
|
321
|
+
hideWhenIdle: config.input === "toggle",
|
|
322
|
+
visibleLines: config.visibleLines,
|
|
323
|
+
openedLines: config.openedLines,
|
|
324
|
+
fadeAfter: config.fadeAfter,
|
|
325
|
+
onSubmit: (text) => this.send(text),
|
|
326
|
+
onFocusChange: (focused) => this.emit("inputfocuschanged", focused)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
get messages() {
|
|
330
|
+
return this._messages;
|
|
331
|
+
}
|
|
332
|
+
onAwake() {
|
|
333
|
+
const ctx = this.ctx;
|
|
334
|
+
const isMobile = ctx.modules.env.isMobile;
|
|
335
|
+
this.dom.setFocusPolicy({
|
|
336
|
+
keepFocusAfterSubmit: isMobile,
|
|
337
|
+
returnFocusTo: ctx.renderer.domElement
|
|
338
|
+
});
|
|
339
|
+
if (isMobile) this.dom.setHideWhenIdle(false);
|
|
340
|
+
ctx.modules.input.on("keydown", this._onKeyDown);
|
|
341
|
+
if (ctx.canvasContainer) this._onMount(ctx.canvasContainer);
|
|
342
|
+
ctx.on(ThreeContextEvents.Mount, this._onMount);
|
|
343
|
+
ctx.on(ThreeContextEvents.Unmount, this._onUnmount);
|
|
344
|
+
}
|
|
345
|
+
/** Print a line into the feed as is. */
|
|
346
|
+
print(text) {
|
|
347
|
+
this._messages.push(this._createMessage(text));
|
|
348
|
+
if (this._messages.length > this._maxMessages) this._messages = this._messages.slice(-this._maxMessages);
|
|
349
|
+
this._emitChanged();
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Send a line the way a player does: commands are handled here, everything else
|
|
353
|
+
* goes out as `sent`. It prints nothing by itself — what to show is up to the world.
|
|
354
|
+
*/
|
|
355
|
+
send(text) {
|
|
356
|
+
const value = text.trim();
|
|
357
|
+
if (!value) return;
|
|
358
|
+
if (value === "/clear") {
|
|
359
|
+
this.clear();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.emit("sent", value);
|
|
363
|
+
}
|
|
364
|
+
/** Redraw the whole feed — animation frames, tables, ASCII art. */
|
|
365
|
+
setMessages(texts) {
|
|
366
|
+
const tail = texts.length > this._maxMessages ? texts.slice(-this._maxMessages) : texts;
|
|
367
|
+
this._messages = tail.map((text) => this._createMessage(text));
|
|
368
|
+
this._emitChanged();
|
|
369
|
+
}
|
|
370
|
+
clear() {
|
|
371
|
+
this._messages = [];
|
|
372
|
+
this._emitChanged();
|
|
373
|
+
}
|
|
374
|
+
_emitChanged() {
|
|
375
|
+
this.dom.renderMessages(this._messages);
|
|
376
|
+
this.emit("changed", this._messages);
|
|
377
|
+
}
|
|
378
|
+
_createMessage(text) {
|
|
379
|
+
return {
|
|
380
|
+
id: `m${++this._seq}`,
|
|
381
|
+
text: text.trim(),
|
|
382
|
+
timestamp: Date.now()
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** three-start ContextModule has no onDestroy; teardown is explicit. */
|
|
386
|
+
dispose() {
|
|
387
|
+
const ctx = this.ctx;
|
|
388
|
+
ctx.modules.input.off("keydown", this._onKeyDown);
|
|
389
|
+
ctx.off(ThreeContextEvents.Mount, this._onMount);
|
|
390
|
+
ctx.off(ThreeContextEvents.Unmount, this._onUnmount);
|
|
391
|
+
this.dom.dispose();
|
|
392
|
+
}
|
|
393
|
+
_onKeyDown = (e) => {
|
|
394
|
+
if (e.repeat) return;
|
|
395
|
+
if (!this.dom.isMounted || this.dom.isFocused) return;
|
|
396
|
+
const isCommandKey = e.code === CHAT_COMMAND_KEY;
|
|
397
|
+
if (!isCommandKey && !this._openKeys.includes(e.code)) return;
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
if (isCommandKey) this.dom.value = "/";
|
|
400
|
+
this.dom.focus();
|
|
401
|
+
};
|
|
402
|
+
_onMount = (container) => {
|
|
403
|
+
this.dom.mount(container);
|
|
404
|
+
this.dom.renderMessages(this._messages);
|
|
405
|
+
};
|
|
406
|
+
_onUnmount = () => {
|
|
407
|
+
this.dom.unmount();
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
//#endregion
|
|
411
|
+
export { ChatModule };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { t as Rigidbody } from "./Rigidbody-
|
|
1
|
+
import { t as Rigidbody } from "./Rigidbody-DLhN8ddv.js";
|
|
2
2
|
import * as THREE from "three/webgpu";
|
|
3
3
|
import { Object3DBehaviour, assertDefined, getComponents } from "three-start";
|
|
4
|
-
//#region packages/world-runtime/src/behaviours/physics/
|
|
4
|
+
//#region packages/world-runtime/src/behaviours/physics/Collider.ts
|
|
5
5
|
/** Walk the parent chain (ancestors only); stop when `cb` returns `false`. */
|
|
6
6
|
function traverseUp(obj, cb) {
|
|
7
7
|
let cur = obj.parent;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as getHalfheightRadiusScale } from "./get-scale-from-obj-
|
|
2
|
-
import { t as Collider } from "./Collider-
|
|
1
|
+
import { t as getHalfheightRadiusScale } from "./get-scale-from-obj-B7JlVPni.js";
|
|
2
|
+
import { t as Collider } from "./Collider-CPUCG3RA.js";
|
|
3
3
|
//#region packages/world-runtime/src/behaviours/physics/ConeCollider.ts
|
|
4
4
|
var ConeCollider = class extends Collider {
|
|
5
5
|
type = "Cone";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as getHalfheightRadiusScale } from "./get-scale-from-obj-
|
|
2
|
-
import { t as Collider } from "./Collider-
|
|
1
|
+
import { t as getHalfheightRadiusScale } from "./get-scale-from-obj-B7JlVPni.js";
|
|
2
|
+
import { t as Collider } from "./Collider-CPUCG3RA.js";
|
|
3
3
|
//#region packages/world-runtime/src/behaviours/physics/CylinderCollider.ts
|
|
4
4
|
var CylinderCollider = class extends Collider {
|
|
5
5
|
type = "Cylinder";
|
|
@@ -11,7 +11,7 @@ var FpsCameraController = class extends Object3DBehaviour {
|
|
|
11
11
|
let isDragging = false;
|
|
12
12
|
let prevX = 0;
|
|
13
13
|
let prevY = 0;
|
|
14
|
-
const isMobile =
|
|
14
|
+
const isMobile = this.modules.env.isMobile;
|
|
15
15
|
const sensitivity = isMobile ? this.mobileSensitivity : this.sensitivity;
|
|
16
16
|
const inverted = isMobile && this.useInvertedOnMobile ? true : this.inverted;
|
|
17
17
|
const onMouseDown = (e) => {
|