@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,370 @@
1
+ import { isTouchDevice } from '../controls/touch.js';
2
+ const STYLE_ID = 'worldmesh-overlay-style';
3
+ const CSS = `
4
+ .wm-overlay {
5
+ position: fixed;
6
+ inset: 0;
7
+ pointer-events: none;
8
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
9
+ color: #f2f6fb;
10
+ z-index: 2147483000;
11
+ }
12
+ .wm-overlay * { box-sizing: border-box; }
13
+ .wm-crosshair {
14
+ position: absolute;
15
+ left: 50%;
16
+ top: 50%;
17
+ width: 6px;
18
+ height: 6px;
19
+ margin: -3px 0 0 -3px;
20
+ border-radius: 50%;
21
+ background: rgba(255,255,255,0.85);
22
+ box-shadow: 0 0 0 1.5px rgba(0,0,0,0.45);
23
+ opacity: 0;
24
+ transition: opacity .15s ease;
25
+ }
26
+ .wm-overlay[data-locked="true"] .wm-crosshair { opacity: 1; }
27
+ .wm-lock {
28
+ position: absolute;
29
+ inset: 0;
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ justify-content: flex-start;
34
+ overflow-y: auto;
35
+ overflow-x: hidden;
36
+ -webkit-overflow-scrolling: touch;
37
+ background: rgba(8,11,16,0.72);
38
+ backdrop-filter: blur(6px);
39
+ pointer-events: auto;
40
+ cursor: pointer;
41
+ text-align: center;
42
+ 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));
43
+ box-sizing: border-box;
44
+ -webkit-tap-highlight-color: transparent;
45
+ }
46
+ .wm-overlay[data-locked="true"] .wm-lock { display: none; }
47
+ .wm-lock-content {
48
+ margin: auto 0;
49
+ width: 100%;
50
+ max-width: 440px;
51
+ display: flex;
52
+ flex-direction: column;
53
+ align-items: center;
54
+ gap: 16px;
55
+ padding: 8px 0;
56
+ box-sizing: border-box;
57
+ }
58
+ .wm-lock-title {
59
+ font-size: clamp(22px, 5vw, 34px);
60
+ font-weight: 600;
61
+ letter-spacing: .01em;
62
+ margin: 0;
63
+ }
64
+ .wm-lock-cta {
65
+ font-size: 13.5px;
66
+ letter-spacing: .14em;
67
+ text-transform: uppercase;
68
+ padding: 12px 28px;
69
+ border: 1px solid rgba(255,255,255,0.35);
70
+ border-radius: 999px;
71
+ background: rgba(255,255,255,0.1);
72
+ color: #fff;
73
+ cursor: pointer;
74
+ font-family: inherit;
75
+ font-weight: 500;
76
+ display: inline-flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ gap: 8px;
80
+ touch-action: manipulation;
81
+ -webkit-tap-highlight-color: transparent;
82
+ transition: all .15s ease;
83
+ }
84
+ .wm-lock-cta:hover, .wm-lock-cta:active {
85
+ background: rgba(255,255,255,0.22);
86
+ border-color: rgba(255,255,255,0.6);
87
+ transform: scale(1.02);
88
+ }
89
+ .wm-keys {
90
+ display: grid;
91
+ grid-template-columns: repeat(2, minmax(0, 1fr));
92
+ gap: 8px 14px;
93
+ width: 100%;
94
+ max-width: 420px;
95
+ font-size: 13px;
96
+ opacity: .9;
97
+ box-sizing: border-box;
98
+ padding: 0 4px;
99
+ }
100
+ .wm-keys-item {
101
+ display: flex;
102
+ gap: 7px;
103
+ align-items: center;
104
+ justify-content: flex-start;
105
+ min-width: 0;
106
+ }
107
+ .wm-key {
108
+ flex-shrink: 0;
109
+ min-width: 20px;
110
+ padding: 2.5px 6px;
111
+ border-radius: 5px;
112
+ border: 1px solid rgba(255,255,255,0.28);
113
+ background: rgba(255,255,255,0.08);
114
+ font-size: 11px;
115
+ font-weight: 600;
116
+ text-align: center;
117
+ white-space: nowrap;
118
+ line-height: 1.25;
119
+ }
120
+ .wm-key-desc {
121
+ min-width: 0;
122
+ overflow: hidden;
123
+ text-overflow: ellipsis;
124
+ white-space: nowrap;
125
+ font-size: 12px;
126
+ color: rgba(255,255,255,0.85);
127
+ }
128
+ .wm-prompt {
129
+ position: absolute;
130
+ left: 50%;
131
+ top: 58%;
132
+ transform: translateX(-50%);
133
+ padding: 10px 18px;
134
+ border-radius: 999px;
135
+ background: rgba(8,11,16,0.85);
136
+ border: 1px solid rgba(255,255,255,0.3);
137
+ box-shadow: 0 4px 16px rgba(0,0,0,0.5);
138
+ font-size: 14px;
139
+ display: none;
140
+ white-space: nowrap;
141
+ pointer-events: auto;
142
+ cursor: pointer;
143
+ -webkit-tap-highlight-color: transparent;
144
+ touch-action: manipulation;
145
+ transition: transform .12s ease, border-color .12s ease;
146
+ }
147
+ .wm-prompt:hover, .wm-prompt:active {
148
+ transform: translateX(-50%) scale(1.04);
149
+ border-color: rgba(255,255,255,0.6);
150
+ }
151
+ .wm-overlay[data-locked="true"] .wm-prompt[data-visible="true"] { display: block; }
152
+ .wm-badge {
153
+ position: absolute;
154
+ left: max(14px, env(safe-area-inset-left));
155
+ bottom: max(12px, env(safe-area-inset-bottom));
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 8px;
159
+ font-size: 11px;
160
+ letter-spacing: .18em;
161
+ text-transform: uppercase;
162
+ color: rgba(255,255,255,0.72);
163
+ text-decoration: none;
164
+ pointer-events: auto;
165
+ padding: 6px 10px;
166
+ border-radius: 7px;
167
+ background: rgba(8,11,16,0.45);
168
+ border: 1px solid rgba(255,255,255,0.12);
169
+ }
170
+ .wm-badge:hover { color: #fff; border-color: rgba(255,255,255,0.3); }
171
+ .wm-badge-dot {
172
+ width: 7px;
173
+ height: 7px;
174
+ border-radius: 50%;
175
+ background: #6cf0ff;
176
+ box-shadow: 0 0 8px #6cf0ff;
177
+ }
178
+ .wm-hint {
179
+ position: absolute;
180
+ right: max(14px, env(safe-area-inset-right));
181
+ bottom: max(12px, env(safe-area-inset-bottom));
182
+ font-size: 11px;
183
+ letter-spacing: .1em;
184
+ color: rgba(255,255,255,0.5);
185
+ opacity: 0;
186
+ transition: opacity .2s ease;
187
+ }
188
+ .wm-overlay[data-locked="true"] .wm-hint { opacity: 1; }
189
+
190
+ @media (max-width: 400px) {
191
+ .wm-lock {
192
+ 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));
193
+ }
194
+ .wm-lock-content {
195
+ gap: 12px;
196
+ }
197
+ .wm-lock-title {
198
+ font-size: 20px;
199
+ }
200
+ .wm-lock-cta {
201
+ padding: 10px 22px;
202
+ font-size: 12px;
203
+ }
204
+ .wm-keys {
205
+ gap: 6px 8px;
206
+ }
207
+ .wm-keys-item {
208
+ gap: 5px;
209
+ }
210
+ .wm-key {
211
+ font-size: 10px;
212
+ padding: 2px 5px;
213
+ }
214
+ .wm-key-desc {
215
+ font-size: 11px;
216
+ }
217
+ }
218
+
219
+ @media (max-height: 520px) {
220
+ .wm-lock {
221
+ 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));
222
+ }
223
+ .wm-lock-content {
224
+ gap: 10px;
225
+ }
226
+ .wm-lock-title {
227
+ font-size: 18px;
228
+ }
229
+ .wm-lock-cta {
230
+ padding: 8px 20px;
231
+ font-size: 12px;
232
+ }
233
+ .wm-keys {
234
+ gap: 4px 10px;
235
+ }
236
+ .wm-key {
237
+ font-size: 9.5px;
238
+ padding: 2px 4px;
239
+ }
240
+ .wm-key-desc {
241
+ font-size: 11px;
242
+ }
243
+ }
244
+ `;
245
+ const KEY_LEGEND = [
246
+ ['WASD / ↑←↓→', 'Move'],
247
+ ['Mouse / Drag', 'Look'],
248
+ ['Space / ▲', 'Jump'],
249
+ ['Shift', 'Sprint'],
250
+ ['E', 'Interact'],
251
+ ['V', 'Camera'],
252
+ ['1 – 0', 'Face'],
253
+ ['Esc / ⏸', 'Menu / Pause'],
254
+ ];
255
+ /**
256
+ * The shared chrome of a WorldMesh world. Every world shows the same
257
+ * click/tap-to-enter panel, the same control legend and the same interact prompt,
258
+ * which is what makes an unfamiliar world navigable on sight across desktop and mobile.
259
+ */
260
+ export class Overlay {
261
+ root;
262
+ prompt;
263
+ onEnter;
264
+ constructor(options) {
265
+ injectStyles();
266
+ this.onEnter = options.onEnter;
267
+ const isTouch = isTouchDevice();
268
+ this.root = document.createElement('div');
269
+ this.root.className = 'wm-overlay';
270
+ this.root.dataset.locked = 'false';
271
+ if (options.crosshair !== false) {
272
+ const crosshair = document.createElement('div');
273
+ crosshair.className = 'wm-crosshair';
274
+ this.root.appendChild(crosshair);
275
+ }
276
+ const lock = document.createElement('div');
277
+ lock.className = 'wm-lock';
278
+ const content = document.createElement('div');
279
+ content.className = 'wm-lock-content';
280
+ const title = document.createElement('h1');
281
+ title.className = 'wm-lock-title';
282
+ title.textContent = options.title ?? 'WorldMesh';
283
+ content.appendChild(title);
284
+ const cta = document.createElement('button');
285
+ cta.type = 'button';
286
+ cta.className = 'wm-lock-cta';
287
+ cta.textContent = isTouch ? 'Tap to enter' : 'Click to enter';
288
+ content.appendChild(cta);
289
+ const handleEnter = (e) => {
290
+ e.preventDefault();
291
+ e.stopPropagation();
292
+ const touchInitiated = ('pointerType' in e && e.pointerType === 'touch') ||
293
+ e.type === 'touchend' ||
294
+ isTouch;
295
+ this.onEnter(touchInitiated);
296
+ };
297
+ cta.addEventListener('click', handleEnter);
298
+ cta.addEventListener('pointerup', handleEnter);
299
+ lock.addEventListener('click', handleEnter);
300
+ if (options.controlsHint !== false) {
301
+ const keys = document.createElement('div');
302
+ keys.className = 'wm-keys';
303
+ const legend = isTouch
304
+ ? [...KEY_LEGEND.filter(([key]) => key !== '1 – 0'), ['Touch', 'On-screen controls']]
305
+ : KEY_LEGEND;
306
+ for (const [key, label] of legend) {
307
+ const row = document.createElement('div');
308
+ row.className = 'wm-keys-item';
309
+ const badge = document.createElement('span');
310
+ badge.className = 'wm-key';
311
+ badge.textContent = key;
312
+ const text = document.createElement('span');
313
+ text.className = 'wm-key-desc';
314
+ text.textContent = label;
315
+ row.append(badge, text);
316
+ keys.appendChild(row);
317
+ }
318
+ content.appendChild(keys);
319
+ }
320
+ lock.appendChild(content);
321
+ this.root.appendChild(lock);
322
+ this.prompt = document.createElement('div');
323
+ this.prompt.className = 'wm-prompt';
324
+ this.prompt.dataset.visible = 'false';
325
+ this.prompt.addEventListener('click', (e) => {
326
+ e.stopPropagation();
327
+ options.onInteract?.();
328
+ });
329
+ this.root.appendChild(this.prompt);
330
+ if (options.badge !== false) {
331
+ const badge = document.createElement('a');
332
+ badge.className = 'wm-badge';
333
+ badge.href = options.hubUrl ?? 'https://worldmesh.net';
334
+ const dot = document.createElement('span');
335
+ dot.className = 'wm-badge-dot';
336
+ const label = document.createElement('span');
337
+ label.textContent = 'WorldMesh';
338
+ badge.append(dot, label);
339
+ this.root.appendChild(badge);
340
+ }
341
+ const hint = document.createElement('div');
342
+ hint.className = 'wm-hint';
343
+ hint.textContent = isTouch ? 'Tap ⏸ to pause' : 'ESC to release cursor';
344
+ this.root.appendChild(hint);
345
+ document.body.appendChild(this.root);
346
+ }
347
+ setLocked(locked) {
348
+ this.root.dataset.locked = String(locked);
349
+ }
350
+ setPrompt(text) {
351
+ if (!text) {
352
+ this.prompt.dataset.visible = 'false';
353
+ return;
354
+ }
355
+ this.prompt.textContent = text;
356
+ this.prompt.dataset.visible = 'true';
357
+ }
358
+ dispose() {
359
+ this.root.remove();
360
+ }
361
+ }
362
+ function injectStyles() {
363
+ if (document.getElementById(STYLE_ID))
364
+ return;
365
+ const style = document.createElement('style');
366
+ style.id = STYLE_ID;
367
+ style.textContent = CSS;
368
+ document.head.appendChild(style);
369
+ }
370
+ //# sourceMappingURL=overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../src/ui/overlay.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@worldmesh/runtime",
3
+ "version": "0.1.0",
4
+ "description": "The shared WorldMesh navigation runtime: controls, movement, camera, player, abilities and portals.",
5
+ "keywords": [
6
+ "worldmesh",
7
+ "three",
8
+ "threejs",
9
+ "3d",
10
+ "webgl",
11
+ "controls",
12
+ "movement",
13
+ "first-person",
14
+ "third-person",
15
+ "portals"
16
+ ],
17
+ "homepage": "https://github.com/eliaswillnat/worldmesh/tree/main/packages/runtime#readme",
18
+ "bugs": "https://github.com/eliaswillnat/worldmesh/issues",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/eliaswillnat/worldmesh.git",
22
+ "directory": "packages/runtime"
23
+ },
24
+ "author": "Elias Willnat",
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "./dist/index.js",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "clean": "rm -rf dist",
50
+ "prebuild": "npm run clean && node scripts/check-version.mjs",
51
+ "build": "tsc -p tsconfig.build.json",
52
+ "watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
53
+ "typecheck": "tsc -p tsconfig.json",
54
+ "prepare": "npm run build"
55
+ },
56
+ "peerDependencies": {
57
+ "three": ">=0.160.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/three": "^0.169.0",
61
+ "three": "^0.169.0"
62
+ }
63
+ }
@@ -0,0 +1,30 @@
1
+ import type { Abilities } from '../types.js';
2
+
3
+ export const DEFAULT_ABILITIES: Abilities = {
4
+ doubleJump: false,
5
+ flying: false,
6
+ crouching: false,
7
+ dash: false,
8
+ climbing: false,
9
+ swimming: false,
10
+ vehicles: false,
11
+ };
12
+
13
+ const RESERVED: (keyof Abilities)[] = ['climbing', 'swimming', 'vehicles'];
14
+
15
+ /**
16
+ * Merge a world's declared abilities over the defaults. Abilities are pure
17
+ * configuration: the shared controller reads these flags, so a world never
18
+ * has to fork the movement code to add one.
19
+ */
20
+ export function resolveAbilities(declared: Partial<Abilities> = {}): Abilities {
21
+ const abilities: Abilities = { ...DEFAULT_ABILITIES, ...declared };
22
+ for (const key of RESERVED) {
23
+ if (abilities[key]) {
24
+ console.warn(
25
+ `[worldmesh] ability "${key}" is reserved in the standard but not implemented yet; it will have no effect.`,
26
+ );
27
+ }
28
+ }
29
+ return abilities;
30
+ }
@@ -0,0 +1,106 @@
1
+ import { Euler, PerspectiveCamera, Vector3, type Camera } from 'three';
2
+ import type { CollisionWorld } from '../movement/collision.js';
3
+ import type { ViewMode, ViewOptions } from '../types.js';
4
+
5
+ const HALF_PI = Math.PI / 2;
6
+ const PITCH_LIMIT = HALF_PI - 0.01;
7
+
8
+ export interface CameraRigOptions extends ViewOptions {
9
+ camera: Camera;
10
+ collision: CollisionWorld;
11
+ }
12
+
13
+ /**
14
+ * Owns yaw/pitch and both camera modes. Worlds get first- and third-person
15
+ * for free, with the same toggle key everywhere, because this lives in the
16
+ * runtime rather than in each world's main.ts.
17
+ */
18
+ export class CameraRig {
19
+ yaw: number;
20
+ pitch: number;
21
+ mode: ViewMode;
22
+ distance: number;
23
+ sensitivity: number;
24
+
25
+ private camera: Camera;
26
+ private collision: CollisionWorld;
27
+ private minDistance: number;
28
+ private maxDistance: number;
29
+ private euler = new Euler(0, 0, 0, 'YXZ');
30
+ private target = new Vector3();
31
+ private offset = new Vector3();
32
+ /** Smoothed boom length so the camera does not pop when it clears a wall. */
33
+ private currentBoom: number;
34
+
35
+ constructor(options: CameraRigOptions) {
36
+ this.camera = options.camera;
37
+ this.collision = options.collision;
38
+ this.mode = options.mode ?? 'first';
39
+ this.distance = options.distance ?? 5;
40
+ this.currentBoom = this.distance;
41
+ this.minDistance = options.minDistance ?? 2;
42
+ this.maxDistance = options.maxDistance ?? 12;
43
+ this.sensitivity = options.sensitivity ?? 0.0022;
44
+ this.yaw = options.yaw ?? 0;
45
+ this.pitch = options.pitch ?? 0;
46
+ }
47
+
48
+ look(dx: number, dy: number): void {
49
+ this.yaw -= dx * this.sensitivity;
50
+ this.pitch -= dy * this.sensitivity;
51
+ this.pitch = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, this.pitch));
52
+ }
53
+
54
+ zoom(wheel: number): void {
55
+ if (this.mode !== 'third' || wheel === 0) return;
56
+ this.distance = clamp(this.distance + wheel * 0.01, this.minDistance, this.maxDistance);
57
+ }
58
+
59
+ setMode(mode: ViewMode): void {
60
+ this.mode = mode;
61
+ if (mode === 'third') this.currentBoom = this.distance;
62
+ }
63
+
64
+ toggleMode(): ViewMode {
65
+ this.setMode(this.mode === 'first' ? 'third' : 'first');
66
+ return this.mode;
67
+ }
68
+
69
+ /** Point the camera at the player. `feet` is the player's base position. */
70
+ update(dt: number, feet: Vector3, eyeHeight: number): void {
71
+ this.euler.set(this.pitch, this.yaw, 0);
72
+ this.camera.quaternion.setFromEuler(this.euler);
73
+ this.target.set(feet.x, feet.y + eyeHeight, feet.z);
74
+
75
+ if (this.mode === 'first') {
76
+ this.camera.position.copy(this.target);
77
+ this.currentBoom = this.distance;
78
+ return;
79
+ }
80
+
81
+ // Boom backwards along the view direction, pulled in by anything in the way.
82
+ this.offset.set(0, 0, 1).applyQuaternion(this.camera.quaternion);
83
+ const wanted = this.collision.castDistance(this.target, this.offset, this.distance + 0.3) - 0.3;
84
+ const boom = clamp(wanted, 0.4, this.distance);
85
+ // Snap in fast, ease out slowly: popping into geometry is worse than lag.
86
+ const rate = boom < this.currentBoom ? 1 : 1 - Math.exp(-8 * dt);
87
+ this.currentBoom += (boom - this.currentBoom) * rate;
88
+ this.camera.position.copy(this.target).addScaledVector(this.offset, this.currentBoom);
89
+ }
90
+
91
+ /** Horizontal forward vector, for spawn orientation helpers. */
92
+ getForward(out = new Vector3()): Vector3 {
93
+ return out.set(-Math.sin(this.yaw), 0, -Math.cos(this.yaw));
94
+ }
95
+
96
+ resize(width: number, height: number): void {
97
+ if (this.camera instanceof PerspectiveCamera) {
98
+ this.camera.aspect = width / height;
99
+ this.camera.updateProjectionMatrix();
100
+ }
101
+ }
102
+ }
103
+
104
+ function clamp(value: number, min: number, max: number): number {
105
+ return Math.max(min, Math.min(max, value));
106
+ }