@worldmesh/runtime 0.1.0

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/dist/abilities/abilities.d.ts +9 -0
  4. package/dist/abilities/abilities.d.ts.map +1 -0
  5. package/dist/abilities/abilities.js +25 -0
  6. package/dist/abilities/abilities.js.map +1 -0
  7. package/dist/camera/cameraRig.d.ts +39 -0
  8. package/dist/camera/cameraRig.d.ts.map +1 -0
  9. package/dist/camera/cameraRig.js +88 -0
  10. package/dist/camera/cameraRig.js.map +1 -0
  11. package/dist/controls/input.d.ts +64 -0
  12. package/dist/controls/input.d.ts.map +1 -0
  13. package/dist/controls/input.js +236 -0
  14. package/dist/controls/input.js.map +1 -0
  15. package/dist/controls/keymap.d.ts +8 -0
  16. package/dist/controls/keymap.d.ts.map +1 -0
  17. package/dist/controls/keymap.js +21 -0
  18. package/dist/controls/keymap.js.map +1 -0
  19. package/dist/controls/touch.d.ts +43 -0
  20. package/dist/controls/touch.d.ts.map +1 -0
  21. package/dist/controls/touch.js +329 -0
  22. package/dist/controls/touch.js.map +1 -0
  23. package/dist/core/events.d.ts +9 -0
  24. package/dist/core/events.d.ts.map +1 -0
  25. package/dist/core/events.js +27 -0
  26. package/dist/core/events.js.map +1 -0
  27. package/dist/core/worldmesh.d.ts +10 -0
  28. package/dist/core/worldmesh.d.ts.map +1 -0
  29. package/dist/core/worldmesh.js +283 -0
  30. package/dist/core/worldmesh.js.map +1 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +18 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/movement/collision.d.ts +39 -0
  36. package/dist/movement/collision.d.ts.map +1 -0
  37. package/dist/movement/collision.js +93 -0
  38. package/dist/movement/collision.js.map +1 -0
  39. package/dist/movement/controller.d.ts +53 -0
  40. package/dist/movement/controller.d.ts.map +1 -0
  41. package/dist/movement/controller.js +200 -0
  42. package/dist/movement/controller.js.map +1 -0
  43. package/dist/net/adapter.d.ts +22 -0
  44. package/dist/net/adapter.d.ts.map +1 -0
  45. package/dist/net/adapter.js +8 -0
  46. package/dist/net/adapter.js.map +1 -0
  47. package/dist/player/avatar.d.ts +29 -0
  48. package/dist/player/avatar.d.ts.map +1 -0
  49. package/dist/player/avatar.js +420 -0
  50. package/dist/player/avatar.js.map +1 -0
  51. package/dist/player/player.d.ts +30 -0
  52. package/dist/player/player.d.ts.map +1 -0
  53. package/dist/player/player.js +76 -0
  54. package/dist/player/player.js.map +1 -0
  55. package/dist/portals/portals.d.ts +34 -0
  56. package/dist/portals/portals.d.ts.map +1 -0
  57. package/dist/portals/portals.js +132 -0
  58. package/dist/portals/portals.js.map +1 -0
  59. package/dist/types.d.ts +228 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +2 -0
  62. package/dist/types.js.map +1 -0
  63. package/dist/ui/overlay.d.ts +19 -0
  64. package/dist/ui/overlay.d.ts.map +1 -0
  65. package/dist/ui/overlay.js +370 -0
  66. package/dist/ui/overlay.js.map +1 -0
  67. package/package.json +63 -0
  68. package/src/abilities/abilities.ts +30 -0
  69. package/src/camera/cameraRig.ts +106 -0
  70. package/src/controls/input.ts +252 -0
  71. package/src/controls/keymap.ts +23 -0
  72. package/src/controls/touch.ts +378 -0
  73. package/src/core/events.ts +31 -0
  74. package/src/core/worldmesh.ts +313 -0
  75. package/src/index.ts +46 -0
  76. package/src/movement/collision.ts +109 -0
  77. package/src/movement/controller.ts +248 -0
  78. package/src/net/adapter.ts +28 -0
  79. package/src/player/avatar.ts +495 -0
  80. package/src/player/player.ts +91 -0
  81. package/src/portals/portals.ts +173 -0
  82. package/src/types.ts +230 -0
  83. package/src/ui/overlay.ts +398 -0
@@ -0,0 +1,398 @@
1
+ import type { UiOptions } from '../types.js';
2
+ import { isTouchDevice } from '../controls/touch.js';
3
+
4
+ const STYLE_ID = 'worldmesh-overlay-style';
5
+
6
+ const CSS = `
7
+ .wm-overlay {
8
+ position: fixed;
9
+ inset: 0;
10
+ pointer-events: none;
11
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
12
+ color: #f2f6fb;
13
+ z-index: 2147483000;
14
+ }
15
+ .wm-overlay * { box-sizing: border-box; }
16
+ .wm-crosshair {
17
+ position: absolute;
18
+ left: 50%;
19
+ top: 50%;
20
+ width: 6px;
21
+ height: 6px;
22
+ margin: -3px 0 0 -3px;
23
+ border-radius: 50%;
24
+ background: rgba(255,255,255,0.85);
25
+ box-shadow: 0 0 0 1.5px rgba(0,0,0,0.45);
26
+ opacity: 0;
27
+ transition: opacity .15s ease;
28
+ }
29
+ .wm-overlay[data-locked="true"] .wm-crosshair { opacity: 1; }
30
+ .wm-lock {
31
+ position: absolute;
32
+ inset: 0;
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ justify-content: flex-start;
37
+ overflow-y: auto;
38
+ overflow-x: hidden;
39
+ -webkit-overflow-scrolling: touch;
40
+ background: rgba(8,11,16,0.72);
41
+ backdrop-filter: blur(6px);
42
+ pointer-events: auto;
43
+ cursor: pointer;
44
+ text-align: center;
45
+ padding: max(20px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
46
+ box-sizing: border-box;
47
+ -webkit-tap-highlight-color: transparent;
48
+ }
49
+ .wm-overlay[data-locked="true"] .wm-lock { display: none; }
50
+ .wm-lock-content {
51
+ margin: auto 0;
52
+ width: 100%;
53
+ max-width: 440px;
54
+ display: flex;
55
+ flex-direction: column;
56
+ align-items: center;
57
+ gap: 16px;
58
+ padding: 8px 0;
59
+ box-sizing: border-box;
60
+ }
61
+ .wm-lock-title {
62
+ font-size: clamp(22px, 5vw, 34px);
63
+ font-weight: 600;
64
+ letter-spacing: .01em;
65
+ margin: 0;
66
+ }
67
+ .wm-lock-cta {
68
+ font-size: 13.5px;
69
+ letter-spacing: .14em;
70
+ text-transform: uppercase;
71
+ padding: 12px 28px;
72
+ border: 1px solid rgba(255,255,255,0.35);
73
+ border-radius: 999px;
74
+ background: rgba(255,255,255,0.1);
75
+ color: #fff;
76
+ cursor: pointer;
77
+ font-family: inherit;
78
+ font-weight: 500;
79
+ display: inline-flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ gap: 8px;
83
+ touch-action: manipulation;
84
+ -webkit-tap-highlight-color: transparent;
85
+ transition: all .15s ease;
86
+ }
87
+ .wm-lock-cta:hover, .wm-lock-cta:active {
88
+ background: rgba(255,255,255,0.22);
89
+ border-color: rgba(255,255,255,0.6);
90
+ transform: scale(1.02);
91
+ }
92
+ .wm-keys {
93
+ display: grid;
94
+ grid-template-columns: repeat(2, minmax(0, 1fr));
95
+ gap: 8px 14px;
96
+ width: 100%;
97
+ max-width: 420px;
98
+ font-size: 13px;
99
+ opacity: .9;
100
+ box-sizing: border-box;
101
+ padding: 0 4px;
102
+ }
103
+ .wm-keys-item {
104
+ display: flex;
105
+ gap: 7px;
106
+ align-items: center;
107
+ justify-content: flex-start;
108
+ min-width: 0;
109
+ }
110
+ .wm-key {
111
+ flex-shrink: 0;
112
+ min-width: 20px;
113
+ padding: 2.5px 6px;
114
+ border-radius: 5px;
115
+ border: 1px solid rgba(255,255,255,0.28);
116
+ background: rgba(255,255,255,0.08);
117
+ font-size: 11px;
118
+ font-weight: 600;
119
+ text-align: center;
120
+ white-space: nowrap;
121
+ line-height: 1.25;
122
+ }
123
+ .wm-key-desc {
124
+ min-width: 0;
125
+ overflow: hidden;
126
+ text-overflow: ellipsis;
127
+ white-space: nowrap;
128
+ font-size: 12px;
129
+ color: rgba(255,255,255,0.85);
130
+ }
131
+ .wm-prompt {
132
+ position: absolute;
133
+ left: 50%;
134
+ top: 58%;
135
+ transform: translateX(-50%);
136
+ padding: 10px 18px;
137
+ border-radius: 999px;
138
+ background: rgba(8,11,16,0.85);
139
+ border: 1px solid rgba(255,255,255,0.3);
140
+ box-shadow: 0 4px 16px rgba(0,0,0,0.5);
141
+ font-size: 14px;
142
+ display: none;
143
+ white-space: nowrap;
144
+ pointer-events: auto;
145
+ cursor: pointer;
146
+ -webkit-tap-highlight-color: transparent;
147
+ touch-action: manipulation;
148
+ transition: transform .12s ease, border-color .12s ease;
149
+ }
150
+ .wm-prompt:hover, .wm-prompt:active {
151
+ transform: translateX(-50%) scale(1.04);
152
+ border-color: rgba(255,255,255,0.6);
153
+ }
154
+ .wm-overlay[data-locked="true"] .wm-prompt[data-visible="true"] { display: block; }
155
+ .wm-badge {
156
+ position: absolute;
157
+ left: max(14px, env(safe-area-inset-left));
158
+ bottom: max(12px, env(safe-area-inset-bottom));
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 8px;
162
+ font-size: 11px;
163
+ letter-spacing: .18em;
164
+ text-transform: uppercase;
165
+ color: rgba(255,255,255,0.72);
166
+ text-decoration: none;
167
+ pointer-events: auto;
168
+ padding: 6px 10px;
169
+ border-radius: 7px;
170
+ background: rgba(8,11,16,0.45);
171
+ border: 1px solid rgba(255,255,255,0.12);
172
+ }
173
+ .wm-badge:hover { color: #fff; border-color: rgba(255,255,255,0.3); }
174
+ .wm-badge-dot {
175
+ width: 7px;
176
+ height: 7px;
177
+ border-radius: 50%;
178
+ background: #6cf0ff;
179
+ box-shadow: 0 0 8px #6cf0ff;
180
+ }
181
+ .wm-hint {
182
+ position: absolute;
183
+ right: max(14px, env(safe-area-inset-right));
184
+ bottom: max(12px, env(safe-area-inset-bottom));
185
+ font-size: 11px;
186
+ letter-spacing: .1em;
187
+ color: rgba(255,255,255,0.5);
188
+ opacity: 0;
189
+ transition: opacity .2s ease;
190
+ }
191
+ .wm-overlay[data-locked="true"] .wm-hint { opacity: 1; }
192
+
193
+ @media (max-width: 400px) {
194
+ .wm-lock {
195
+ padding: max(14px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(18px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
196
+ }
197
+ .wm-lock-content {
198
+ gap: 12px;
199
+ }
200
+ .wm-lock-title {
201
+ font-size: 20px;
202
+ }
203
+ .wm-lock-cta {
204
+ padding: 10px 22px;
205
+ font-size: 12px;
206
+ }
207
+ .wm-keys {
208
+ gap: 6px 8px;
209
+ }
210
+ .wm-keys-item {
211
+ gap: 5px;
212
+ }
213
+ .wm-key {
214
+ font-size: 10px;
215
+ padding: 2px 5px;
216
+ }
217
+ .wm-key-desc {
218
+ font-size: 11px;
219
+ }
220
+ }
221
+
222
+ @media (max-height: 520px) {
223
+ .wm-lock {
224
+ padding: max(8px, env(safe-area-inset-top)) max(14px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
225
+ }
226
+ .wm-lock-content {
227
+ gap: 10px;
228
+ }
229
+ .wm-lock-title {
230
+ font-size: 18px;
231
+ }
232
+ .wm-lock-cta {
233
+ padding: 8px 20px;
234
+ font-size: 12px;
235
+ }
236
+ .wm-keys {
237
+ gap: 4px 10px;
238
+ }
239
+ .wm-key {
240
+ font-size: 9.5px;
241
+ padding: 2px 4px;
242
+ }
243
+ .wm-key-desc {
244
+ font-size: 11px;
245
+ }
246
+ }
247
+ `;
248
+
249
+ const KEY_LEGEND: [string, string][] = [
250
+ ['WASD / ↑←↓→', 'Move'],
251
+ ['Mouse / Drag', 'Look'],
252
+ ['Space / ▲', 'Jump'],
253
+ ['Shift', 'Sprint'],
254
+ ['E', 'Interact'],
255
+ ['V', 'Camera'],
256
+ ['1 – 0', 'Face'],
257
+ ['Esc / ⏸', 'Menu / Pause'],
258
+ ];
259
+
260
+ /**
261
+ * The shared chrome of a WorldMesh world. Every world shows the same
262
+ * click/tap-to-enter panel, the same control legend and the same interact prompt,
263
+ * which is what makes an unfamiliar world navigable on sight across desktop and mobile.
264
+ */
265
+ export class Overlay {
266
+ readonly root: HTMLDivElement;
267
+ private prompt: HTMLDivElement;
268
+ private onEnter: (touch?: boolean) => void;
269
+
270
+ constructor(
271
+ options: UiOptions & {
272
+ onEnter: (touch?: boolean) => void;
273
+ onInteract?: () => void;
274
+ },
275
+ ) {
276
+ injectStyles();
277
+ this.onEnter = options.onEnter;
278
+
279
+ const isTouch = isTouchDevice();
280
+
281
+ this.root = document.createElement('div');
282
+ this.root.className = 'wm-overlay';
283
+ this.root.dataset.locked = 'false';
284
+
285
+ if (options.crosshair !== false) {
286
+ const crosshair = document.createElement('div');
287
+ crosshair.className = 'wm-crosshair';
288
+ this.root.appendChild(crosshair);
289
+ }
290
+
291
+ const lock = document.createElement('div');
292
+ lock.className = 'wm-lock';
293
+
294
+ const content = document.createElement('div');
295
+ content.className = 'wm-lock-content';
296
+
297
+ const title = document.createElement('h1');
298
+ title.className = 'wm-lock-title';
299
+ title.textContent = options.title ?? 'WorldMesh';
300
+ content.appendChild(title);
301
+
302
+ const cta = document.createElement('button');
303
+ cta.type = 'button';
304
+ cta.className = 'wm-lock-cta';
305
+ cta.textContent = isTouch ? 'Tap to enter' : 'Click to enter';
306
+ content.appendChild(cta);
307
+
308
+ const handleEnter = (e: Event) => {
309
+ e.preventDefault();
310
+ e.stopPropagation();
311
+ const touchInitiated =
312
+ ('pointerType' in e && (e as PointerEvent).pointerType === 'touch') ||
313
+ e.type === 'touchend' ||
314
+ isTouch;
315
+ this.onEnter(touchInitiated);
316
+ };
317
+
318
+ cta.addEventListener('click', handleEnter);
319
+ cta.addEventListener('pointerup', handleEnter);
320
+ lock.addEventListener('click', handleEnter);
321
+
322
+ if (options.controlsHint !== false) {
323
+ const keys = document.createElement('div');
324
+ keys.className = 'wm-keys';
325
+ const legend = isTouch
326
+ ? [...KEY_LEGEND.filter(([key]) => key !== '1 – 0'), ['Touch', 'On-screen controls'] as [string, string]]
327
+ : KEY_LEGEND;
328
+ for (const [key, label] of legend) {
329
+ const row = document.createElement('div');
330
+ row.className = 'wm-keys-item';
331
+ const badge = document.createElement('span');
332
+ badge.className = 'wm-key';
333
+ badge.textContent = key;
334
+ const text = document.createElement('span');
335
+ text.className = 'wm-key-desc';
336
+ text.textContent = label;
337
+ row.append(badge, text);
338
+ keys.appendChild(row);
339
+ }
340
+ content.appendChild(keys);
341
+ }
342
+ lock.appendChild(content);
343
+ this.root.appendChild(lock);
344
+
345
+ this.prompt = document.createElement('div');
346
+ this.prompt.className = 'wm-prompt';
347
+ this.prompt.dataset.visible = 'false';
348
+ this.prompt.addEventListener('click', (e) => {
349
+ e.stopPropagation();
350
+ options.onInteract?.();
351
+ });
352
+ this.root.appendChild(this.prompt);
353
+
354
+ if (options.badge !== false) {
355
+ const badge = document.createElement('a');
356
+ badge.className = 'wm-badge';
357
+ badge.href = options.hubUrl ?? 'https://worldmesh.net';
358
+ const dot = document.createElement('span');
359
+ dot.className = 'wm-badge-dot';
360
+ const label = document.createElement('span');
361
+ label.textContent = 'WorldMesh';
362
+ badge.append(dot, label);
363
+ this.root.appendChild(badge);
364
+ }
365
+
366
+ const hint = document.createElement('div');
367
+ hint.className = 'wm-hint';
368
+ hint.textContent = isTouch ? 'Tap ⏸ to pause' : 'ESC to release cursor';
369
+ this.root.appendChild(hint);
370
+
371
+ document.body.appendChild(this.root);
372
+ }
373
+
374
+ setLocked(locked: boolean): void {
375
+ this.root.dataset.locked = String(locked);
376
+ }
377
+
378
+ setPrompt(text: string | null): void {
379
+ if (!text) {
380
+ this.prompt.dataset.visible = 'false';
381
+ return;
382
+ }
383
+ this.prompt.textContent = text;
384
+ this.prompt.dataset.visible = 'true';
385
+ }
386
+
387
+ dispose(): void {
388
+ this.root.remove();
389
+ }
390
+ }
391
+
392
+ function injectStyles(): void {
393
+ if (document.getElementById(STYLE_ID)) return;
394
+ const style = document.createElement('style');
395
+ style.id = STYLE_ID;
396
+ style.textContent = CSS;
397
+ document.head.appendChild(style);
398
+ }