@worldmesh/runtime 0.2.0 → 0.2.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 +33 -3
- package/dist/audio/footsteps.d.ts +37 -0
- package/dist/audio/footsteps.d.ts.map +1 -0
- package/dist/audio/footsteps.js +178 -0
- package/dist/audio/footsteps.js.map +1 -0
- package/dist/avatar/descriptor.d.ts +15 -0
- package/dist/avatar/descriptor.d.ts.map +1 -1
- package/dist/avatar/descriptor.js +19 -0
- package/dist/avatar/descriptor.js.map +1 -1
- package/dist/avatar/loader.d.ts +12 -1
- package/dist/avatar/loader.d.ts.map +1 -1
- package/dist/avatar/loader.js +16 -1
- package/dist/avatar/loader.js.map +1 -1
- package/dist/avatar/share.d.ts +30 -0
- package/dist/avatar/share.d.ts.map +1 -0
- package/dist/avatar/share.js +203 -0
- package/dist/avatar/share.js.map +1 -0
- package/dist/camera/cameraRig.d.ts +4 -0
- package/dist/camera/cameraRig.d.ts.map +1 -1
- package/dist/camera/cameraRig.js +10 -1
- package/dist/camera/cameraRig.js.map +1 -1
- package/dist/camera/viewHandoff.d.ts +20 -0
- package/dist/camera/viewHandoff.d.ts.map +1 -0
- package/dist/camera/viewHandoff.js +64 -0
- package/dist/camera/viewHandoff.js.map +1 -0
- package/dist/controls/input.d.ts +29 -0
- package/dist/controls/input.d.ts.map +1 -1
- package/dist/controls/input.js +124 -1
- package/dist/controls/input.js.map +1 -1
- package/dist/controls/touch.d.ts +10 -0
- package/dist/controls/touch.d.ts.map +1 -1
- package/dist/controls/touch.js +57 -1
- package/dist/controls/touch.js.map +1 -1
- package/dist/core/worldmesh.d.ts +9 -0
- package/dist/core/worldmesh.d.ts.map +1 -1
- package/dist/core/worldmesh.js +498 -26
- package/dist/core/worldmesh.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/net/follow.d.ts +7 -0
- package/dist/net/follow.d.ts.map +1 -0
- package/dist/net/follow.js +10 -0
- package/dist/net/follow.js.map +1 -0
- package/dist/net/presence.d.ts +126 -4
- package/dist/net/presence.d.ts.map +1 -1
- package/dist/net/presence.js +472 -25
- package/dist/net/presence.js.map +1 -1
- package/dist/net/roomFull.d.ts +44 -0
- package/dist/net/roomFull.d.ts.map +1 -0
- package/dist/net/roomFull.js +147 -0
- package/dist/net/roomFull.js.map +1 -0
- package/dist/player/avatar.d.ts +17 -4
- package/dist/player/avatar.d.ts.map +1 -1
- package/dist/player/avatar.js +85 -18
- package/dist/player/avatar.js.map +1 -1
- package/dist/player/player.d.ts +6 -2
- package/dist/player/player.d.ts.map +1 -1
- package/dist/player/player.js +13 -3
- package/dist/player/player.js.map +1 -1
- package/dist/player/stroke.d.ts +11 -0
- package/dist/player/stroke.d.ts.map +1 -0
- package/dist/player/stroke.js +96 -0
- package/dist/player/stroke.js.map +1 -0
- package/dist/portals/portals.d.ts +1 -1
- package/dist/portals/portals.d.ts.map +1 -1
- package/dist/portals/portals.js +6 -2
- package/dist/portals/portals.js.map +1 -1
- package/dist/types.d.ts +53 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlay.d.ts +18 -0
- package/dist/ui/overlay.d.ts.map +1 -1
- package/dist/ui/overlay.js +166 -1
- package/dist/ui/overlay.js.map +1 -1
- package/dist/xr/session.d.ts +13 -0
- package/dist/xr/session.d.ts.map +1 -0
- package/dist/xr/session.js +33 -0
- package/dist/xr/session.js.map +1 -0
- package/package.json +1 -1
- package/src/audio/footsteps.ts +185 -0
- package/src/avatar/descriptor.ts +28 -0
- package/src/avatar/loader.ts +19 -2
- package/src/avatar/share.ts +196 -0
- package/src/camera/cameraRig.ts +10 -1
- package/src/camera/viewHandoff.ts +67 -0
- package/src/controls/input.ts +119 -1
- package/src/controls/touch.ts +59 -1
- package/src/core/worldmesh.ts +495 -25
- package/src/index.ts +14 -7
- package/src/net/follow.ts +9 -0
- package/src/net/presence.ts +548 -28
- package/src/net/roomFull.ts +160 -0
- package/src/player/avatar.ts +87 -18
- package/src/player/player.ts +13 -4
- package/src/player/stroke.ts +93 -0
- package/src/portals/portals.ts +5 -2
- package/src/types.ts +52 -9
- package/src/ui/overlay.ts +171 -1
- package/src/xr/session.ts +33 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { parseAvatarShare } from './descriptor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which avatar other players may see, keeping its link fresh, and checking the
|
|
4
|
+
* links other players send.
|
|
5
|
+
*
|
|
6
|
+
* Other players download a model straight from the platform, so a link must
|
|
7
|
+
* never be one a player made up: a server of their own would learn the IP
|
|
8
|
+
* address of everyone in the room. So only links the WorldMesh hub signed
|
|
9
|
+
* (Ed25519) when it resolved the avatar are sent, and a link from someone
|
|
10
|
+
* else is downloaded only if its signature checks out against the hub's
|
|
11
|
+
* public key. The hub signs Sketchfab links and at3d links on Bluesky's hosts;
|
|
12
|
+
* never VRoid Hub (its license needs "Multiplay" approval for other players)
|
|
13
|
+
* or a self-hosted AT Protocol server.
|
|
14
|
+
*
|
|
15
|
+
* Links and signatures expire (Sketchfab's links within minutes), so the owner
|
|
16
|
+
* asks for a fresh one before the old runs out; someone joining later still
|
|
17
|
+
* gets a working link. Players who already loaded the model keep it.
|
|
18
|
+
*/
|
|
19
|
+
/** Never shown to other players, even if a hub signed it. */
|
|
20
|
+
const PRIVATE_PROVIDERS = ['vroid'];
|
|
21
|
+
/** Ask for a new link this long before the old one expires. */
|
|
22
|
+
const REFRESH_EARLY_MS = 45_000;
|
|
23
|
+
const MIN_REFRESH_MS = 30_000;
|
|
24
|
+
const SHARE_CONTEXT = 'worldmesh-avatar-share-v1';
|
|
25
|
+
/** The hub-signed link other players may be shown, or null when there is none. */
|
|
26
|
+
export function shareableAvatar(descriptor) {
|
|
27
|
+
if (!descriptor || PRIVATE_PROVIDERS.includes(descriptor.provider))
|
|
28
|
+
return null;
|
|
29
|
+
return parseAvatarShare(descriptor.share);
|
|
30
|
+
}
|
|
31
|
+
/** Same avatar, possibly a different link. */
|
|
32
|
+
function sameAvatar(a, b) {
|
|
33
|
+
if (a.avatarId || b.avatarId)
|
|
34
|
+
return a.provider === b.provider && a.avatarId === b.avatarId;
|
|
35
|
+
return a.modelUrl === b.modelUrl;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The avatar this visitor shows other players. Set it whenever the local
|
|
39
|
+
* avatar changes; with `resolve` (asks the hub again), it keeps the signed
|
|
40
|
+
* link current, and fetches one if the descriptor came without.
|
|
41
|
+
*/
|
|
42
|
+
export class SharedAvatarLink {
|
|
43
|
+
resolve;
|
|
44
|
+
descriptor = null;
|
|
45
|
+
shared = null;
|
|
46
|
+
timer = 0;
|
|
47
|
+
turn = 0;
|
|
48
|
+
constructor(resolve) {
|
|
49
|
+
this.resolve = resolve;
|
|
50
|
+
}
|
|
51
|
+
set(descriptor) {
|
|
52
|
+
this.turn += 1;
|
|
53
|
+
window.clearTimeout(this.timer);
|
|
54
|
+
this.descriptor = descriptor;
|
|
55
|
+
this.shared = shareableAvatar(descriptor);
|
|
56
|
+
// Handed over without a signed link (e.g. by an older hub): ask for one now.
|
|
57
|
+
if (descriptor && !this.shared && !PRIVATE_PROVIDERS.includes(descriptor.provider) && descriptor.share === undefined && this.resolve) {
|
|
58
|
+
const turn = this.turn;
|
|
59
|
+
this.timer = window.setTimeout(() => void this.refresh(turn), 0);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.schedule();
|
|
63
|
+
}
|
|
64
|
+
get() {
|
|
65
|
+
return this.shared;
|
|
66
|
+
}
|
|
67
|
+
dispose() {
|
|
68
|
+
this.turn += 1;
|
|
69
|
+
window.clearTimeout(this.timer);
|
|
70
|
+
this.resolve = null;
|
|
71
|
+
}
|
|
72
|
+
schedule() {
|
|
73
|
+
const descriptor = this.descriptor;
|
|
74
|
+
const shared = this.shared;
|
|
75
|
+
if (!this.resolve || !shared || !descriptor)
|
|
76
|
+
return;
|
|
77
|
+
const linkExpires = descriptor.expiresAt ? Date.parse(descriptor.expiresAt) : Infinity;
|
|
78
|
+
const expires = Math.min(shared.x * 1000, Number.isFinite(linkExpires) ? linkExpires : Infinity);
|
|
79
|
+
const delay = Math.max(MIN_REFRESH_MS, expires - Date.now() - REFRESH_EARLY_MS);
|
|
80
|
+
const turn = this.turn;
|
|
81
|
+
this.timer = window.setTimeout(() => void this.refresh(turn), delay);
|
|
82
|
+
}
|
|
83
|
+
async refresh(turn) {
|
|
84
|
+
if (turn !== this.turn || !this.resolve)
|
|
85
|
+
return;
|
|
86
|
+
// Nobody new sees a hidden tab's link expire; ask again once it is back.
|
|
87
|
+
if (document.hidden) {
|
|
88
|
+
this.timer = window.setTimeout(() => void this.refresh(turn), MIN_REFRESH_MS);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let fresh = null;
|
|
92
|
+
try {
|
|
93
|
+
fresh = await this.resolve();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// The hub could not be reached just now. Ask again shortly, or the link
|
|
97
|
+
// runs out for anyone joining later.
|
|
98
|
+
if (turn === this.turn)
|
|
99
|
+
this.timer = window.setTimeout(() => void this.refresh(turn), MIN_REFRESH_MS);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (turn !== this.turn || !this.descriptor)
|
|
103
|
+
return;
|
|
104
|
+
// A different avatar (picked on the hub since) is not swapped in here; the
|
|
105
|
+
// body this visitor sees and the one others see stay the same.
|
|
106
|
+
if (!fresh || !sameAvatar(fresh, this.descriptor))
|
|
107
|
+
return;
|
|
108
|
+
this.descriptor = fresh;
|
|
109
|
+
this.shared = shareableAvatar(fresh);
|
|
110
|
+
this.schedule();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** The exact bytes the hub signs (workers/auth/src/avatars/share.ts). */
|
|
114
|
+
function sharePayload(share) {
|
|
115
|
+
return new TextEncoder().encode(`${SHARE_CONTEXT}\n${share.u}\n${share.f}\n${share.i}\n${share.x}`);
|
|
116
|
+
}
|
|
117
|
+
const verifiers = new Map();
|
|
118
|
+
/** After the hub's key failed to load, wait this long before asking for it again. */
|
|
119
|
+
const KEY_RETRY_MS = 30_000;
|
|
120
|
+
/**
|
|
121
|
+
* Checks links other players send against the public key of the hub at
|
|
122
|
+
* `hubUrl` (fetched once from its resolve endpoint). Says no when the hub has
|
|
123
|
+
* no key or the browser cannot check Ed25519, so nothing unchecked is ever
|
|
124
|
+
* downloaded. Throws when the key could not be fetched just now (offline for a
|
|
125
|
+
* moment, the hub mid-deploy): the link may well be fine, so ask again later.
|
|
126
|
+
*/
|
|
127
|
+
export function createShareVerifier(hubUrl) {
|
|
128
|
+
let endpoint;
|
|
129
|
+
try {
|
|
130
|
+
endpoint = new URL('/api/account/avatar/resolve', hubUrl).toString();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return async () => false;
|
|
134
|
+
}
|
|
135
|
+
const existing = verifiers.get(endpoint);
|
|
136
|
+
if (existing)
|
|
137
|
+
return existing;
|
|
138
|
+
// Resolves to null when links can never be checked here; rejects when the key could not be fetched.
|
|
139
|
+
let key = null;
|
|
140
|
+
let failedAt = 0;
|
|
141
|
+
const loadKey = () => {
|
|
142
|
+
// After a failure, fetch again a little later rather than for every link.
|
|
143
|
+
if (key && !(failedAt && Date.now() - failedAt > KEY_RETRY_MS))
|
|
144
|
+
return key;
|
|
145
|
+
failedAt = 0;
|
|
146
|
+
key = (async () => {
|
|
147
|
+
let x;
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(endpoint, { method: 'GET', mode: 'cors', credentials: 'omit', referrerPolicy: 'no-referrer' });
|
|
150
|
+
if (!response.ok)
|
|
151
|
+
throw new Error(`Key request failed (${response.status})`);
|
|
152
|
+
const body = (await response.json());
|
|
153
|
+
const jwk = body.key;
|
|
154
|
+
if (jwk === null)
|
|
155
|
+
x = null;
|
|
156
|
+
else if (!jwk || jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string')
|
|
157
|
+
throw new Error('Unexpected share key');
|
|
158
|
+
else
|
|
159
|
+
x = jwk.x;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
failedAt = Date.now();
|
|
163
|
+
console.warn('[worldmesh] Cannot load the hub’s avatar key yet; showing default bodies for now.', error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
if (!x) {
|
|
167
|
+
console.warn('[worldmesh] This hub does not vouch for avatar links; showing default bodies.');
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return await crypto.subtle.importKey('jwk', { kty: 'OKP', crv: 'Ed25519', x }, { name: 'Ed25519' }, false, ['verify']);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.warn('[worldmesh] This browser cannot check other players’ avatar links; showing default bodies.', error);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
178
|
+
return key;
|
|
179
|
+
};
|
|
180
|
+
const verify = async (share) => {
|
|
181
|
+
// Throws while the key cannot be fetched: the caller tries again later.
|
|
182
|
+
const publicKey = await loadKey();
|
|
183
|
+
if (!publicKey)
|
|
184
|
+
return false;
|
|
185
|
+
try {
|
|
186
|
+
return await crypto.subtle.verify({ name: 'Ed25519' }, publicKey, fromBase64url(share.s), sharePayload(share));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
verifiers.set(endpoint, verify);
|
|
193
|
+
return verify;
|
|
194
|
+
}
|
|
195
|
+
function fromBase64url(text) {
|
|
196
|
+
const base64 = text.replace(/-/g, '+').replace(/_/g, '/');
|
|
197
|
+
const binary = atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4));
|
|
198
|
+
const bytes = new Uint8Array(binary.length);
|
|
199
|
+
for (let i = 0; i < binary.length; i++)
|
|
200
|
+
bytes[i] = binary.charCodeAt(i);
|
|
201
|
+
return bytes;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=share.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.js","sourceRoot":"","sources":["../../src/avatar/share.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,10 +25,14 @@ export declare class CameraRig {
|
|
|
25
25
|
private offset;
|
|
26
26
|
/** Smoothed boom length so the camera does not pop when it clears a wall. */
|
|
27
27
|
private currentBoom;
|
|
28
|
+
/** After snapBoom: ease back out slowly, a pull-back rather than a jump. */
|
|
29
|
+
private gentle;
|
|
28
30
|
constructor(options: CameraRigOptions);
|
|
29
31
|
look(dx: number, dy: number): void;
|
|
30
32
|
zoom(wheel: number): void;
|
|
31
33
|
setMode(mode: ViewMode): void;
|
|
34
|
+
/** Put the third-person camera this far back right now; it eases back to `distance` from there. */
|
|
35
|
+
snapBoom(length: number): void;
|
|
32
36
|
toggleMode(): ViewMode;
|
|
33
37
|
/** Point the camera at the player. `feet` is the player's base position. */
|
|
34
38
|
update(dt: number, feet: Vector3, eyeHeight: number): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cameraRig.d.ts","sourceRoot":"","sources":["../../src/camera/cameraRig.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAKzD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,MAAM,CAAiB;IAC/B,6EAA6E;IAC7E,OAAO,CAAC,WAAW,CAAS;
|
|
1
|
+
{"version":3,"file":"cameraRig.d.ts","sourceRoot":"","sources":["../../src/camera/cameraRig.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAKzD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,MAAM,CAAiB;IAC/B,6EAA6E;IAC7E,OAAO,CAAC,WAAW,CAAS;IAC5B,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,gBAAgB;IAarC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAMlC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAKzB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAK7B,mGAAmG;IACnG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK9B,UAAU,IAAI,QAAQ;IAKtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAsB1D,gEAAgE;IAChE,UAAU,CAAC,GAAG,UAAgB,GAAG,OAAO;IAIxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAM5C"}
|
package/dist/camera/cameraRig.js
CHANGED
|
@@ -21,6 +21,8 @@ export class CameraRig {
|
|
|
21
21
|
offset = new Vector3();
|
|
22
22
|
/** Smoothed boom length so the camera does not pop when it clears a wall. */
|
|
23
23
|
currentBoom;
|
|
24
|
+
/** After snapBoom: ease back out slowly, a pull-back rather than a jump. */
|
|
25
|
+
gentle = false;
|
|
24
26
|
constructor(options) {
|
|
25
27
|
this.camera = options.camera;
|
|
26
28
|
this.collision = options.collision;
|
|
@@ -48,6 +50,11 @@ export class CameraRig {
|
|
|
48
50
|
if (mode === 'third')
|
|
49
51
|
this.currentBoom = this.distance;
|
|
50
52
|
}
|
|
53
|
+
/** Put the third-person camera this far back right now; it eases back to `distance` from there. */
|
|
54
|
+
snapBoom(length) {
|
|
55
|
+
this.currentBoom = clamp(length, 0.4, this.maxDistance);
|
|
56
|
+
this.gentle = true;
|
|
57
|
+
}
|
|
51
58
|
toggleMode() {
|
|
52
59
|
this.setMode(this.mode === 'first' ? 'third' : 'first');
|
|
53
60
|
return this.mode;
|
|
@@ -67,8 +74,10 @@ export class CameraRig {
|
|
|
67
74
|
const wanted = this.collision.castDistance(this.target, this.offset, this.distance + 0.3) - 0.3;
|
|
68
75
|
const boom = clamp(wanted, 0.4, this.distance);
|
|
69
76
|
// Snap in fast, ease out slowly: popping into geometry is worse than lag.
|
|
70
|
-
const rate = boom < this.currentBoom ? 1 : 1 - Math.exp(-8 * dt);
|
|
77
|
+
const rate = boom < this.currentBoom ? 1 : 1 - Math.exp(-(this.gentle ? 1.6 : 8) * dt);
|
|
71
78
|
this.currentBoom += (boom - this.currentBoom) * rate;
|
|
79
|
+
if (this.gentle && (boom < this.currentBoom || this.currentBoom > boom - 0.05))
|
|
80
|
+
this.gentle = false;
|
|
72
81
|
this.camera.position.copy(this.target).addScaledVector(this.offset, this.currentBoom);
|
|
73
82
|
}
|
|
74
83
|
/** Horizontal forward vector, for spawn orientation helpers. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cameraRig.js","sourceRoot":"","sources":["../../src/camera/cameraRig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAe,MAAM,OAAO,CAAC;AAIvE,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;AAC5B,MAAM,WAAW,GAAG,OAAO,GAAG,IAAI,CAAC;AAOnC;;;;GAIG;AACH,MAAM,OAAO,SAAS;IACpB,GAAG,CAAS;IACZ,KAAK,CAAS;IACd,IAAI,CAAW;IACf,QAAQ,CAAS;IACjB,WAAW,CAAS;IAEZ,MAAM,CAAS;IACf,SAAS,CAAiB;IAC1B,WAAW,CAAS;IACpB,WAAW,CAAS;IACpB,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IAClC,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;IACvB,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;IAC/B,6EAA6E;IACrE,WAAW,CAAS;
|
|
1
|
+
{"version":3,"file":"cameraRig.js","sourceRoot":"","sources":["../../src/camera/cameraRig.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ViewMode } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Remembering the visitor's camera view (first or third person) between worlds.
|
|
4
|
+
*
|
|
5
|
+
* Worlds live on different origins, so storage alone cannot carry it: travel
|
|
6
|
+
* URLs add it to the fragment (`#wm-view=third`), and each world also keeps it
|
|
7
|
+
* in localStorage so it holds across reloads and later visits.
|
|
8
|
+
*
|
|
9
|
+
* Only a view the visitor picked (or was handed) is carried. Until then every
|
|
10
|
+
* world opens in its own default.
|
|
11
|
+
*/
|
|
12
|
+
export declare const VIEW_PARAM = "wm-view";
|
|
13
|
+
export declare const VIEW_STORAGE_KEY = "worldmesh.view";
|
|
14
|
+
/** Moves a view from this page's URL fragment into storage, and returns the remembered one. */
|
|
15
|
+
export declare function takeViewHandoff(): ViewMode | null;
|
|
16
|
+
export declare function getRememberedView(): ViewMode | null;
|
|
17
|
+
export declare function rememberView(mode: ViewMode): void;
|
|
18
|
+
/** Adds the remembered view to a destination URL's fragment, next to any other WorldMesh handoff. */
|
|
19
|
+
export declare function withView(url: URL): URL;
|
|
20
|
+
//# sourceMappingURL=viewHandoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewHandoff.d.ts","sourceRoot":"","sources":["../../src/camera/viewHandoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;GASG;AAEH,eAAO,MAAM,UAAU,YAAY,CAAC;AACpC,eAAO,MAAM,gBAAgB,mBAAmB,CAAC;AAQjD,+FAA+F;AAC/F,wBAAgB,eAAe,IAAI,QAAQ,GAAG,IAAI,CAqBjD;AAED,wBAAgB,iBAAiB,IAAI,QAAQ,GAAG,IAAI,CAEnD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAOjD;AAED,qGAAqG;AACrG,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,CAMtC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remembering the visitor's camera view (first or third person) between worlds.
|
|
3
|
+
*
|
|
4
|
+
* Worlds live on different origins, so storage alone cannot carry it: travel
|
|
5
|
+
* URLs add it to the fragment (`#wm-view=third`), and each world also keeps it
|
|
6
|
+
* in localStorage so it holds across reloads and later visits.
|
|
7
|
+
*
|
|
8
|
+
* Only a view the visitor picked (or was handed) is carried. Until then every
|
|
9
|
+
* world opens in its own default.
|
|
10
|
+
*/
|
|
11
|
+
export const VIEW_PARAM = 'wm-view';
|
|
12
|
+
export const VIEW_STORAGE_KEY = 'worldmesh.view';
|
|
13
|
+
let current = null;
|
|
14
|
+
function isViewMode(value) {
|
|
15
|
+
return value === 'first' || value === 'third';
|
|
16
|
+
}
|
|
17
|
+
/** Moves a view from this page's URL fragment into storage, and returns the remembered one. */
|
|
18
|
+
export function takeViewHandoff() {
|
|
19
|
+
const hash = window.location.hash.slice(1);
|
|
20
|
+
if (hash) {
|
|
21
|
+
const params = new URLSearchParams(hash);
|
|
22
|
+
const view = params.get(VIEW_PARAM);
|
|
23
|
+
if (view !== null) {
|
|
24
|
+
params.delete(VIEW_PARAM);
|
|
25
|
+
const rest = params.toString();
|
|
26
|
+
history.replaceState(history.state, '', `${window.location.pathname}${window.location.search}${rest ? `#${rest}` : ''}`);
|
|
27
|
+
if (isViewMode(view))
|
|
28
|
+
rememberView(view);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!current) {
|
|
32
|
+
try {
|
|
33
|
+
const stored = localStorage.getItem(VIEW_STORAGE_KEY);
|
|
34
|
+
if (isViewMode(stored))
|
|
35
|
+
current = stored;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Storage blocked: the view still follows through travel URLs.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return current;
|
|
42
|
+
}
|
|
43
|
+
export function getRememberedView() {
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
export function rememberView(mode) {
|
|
47
|
+
current = mode;
|
|
48
|
+
try {
|
|
49
|
+
localStorage.setItem(VIEW_STORAGE_KEY, mode);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Storage blocked: the view still follows through travel URLs.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Adds the remembered view to a destination URL's fragment, next to any other WorldMesh handoff. */
|
|
56
|
+
export function withView(url) {
|
|
57
|
+
if (!current || !/^https?:$/.test(url.protocol))
|
|
58
|
+
return url;
|
|
59
|
+
const params = new URLSearchParams(url.hash.slice(1));
|
|
60
|
+
params.set(VIEW_PARAM, current);
|
|
61
|
+
url.hash = params.toString();
|
|
62
|
+
return url;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=viewHandoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewHandoff.js","sourceRoot":"","sources":["../../src/camera/viewHandoff.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC;AACpC,MAAM,CAAC,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;AAEjD,IAAI,OAAO,GAAoB,IAAI,CAAC;AAEpC,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,OAAO,CAAC;AAChD,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,eAAe;IAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3C,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzH,IAAI,UAAU,CAAC,IAAI,CAAC;gBAAE,YAAY,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;YACtD,IAAI,UAAU,CAAC,MAAM,CAAC;gBAAE,OAAO,GAAG,MAAM,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,+DAA+D;QACjE,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAc;IACzC,OAAO,GAAG,IAAI,CAAC;IACf,IAAI,CAAC;QACH,YAAY,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;AACH,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,QAAQ,CAAC,GAAQ;IAC/B,IAAI,CAAC,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,GAAG,CAAC;IAC5D,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChC,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC7B,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/controls/input.d.ts
CHANGED
|
@@ -37,16 +37,45 @@ export declare class Input {
|
|
|
37
37
|
private onEscape?;
|
|
38
38
|
private touch?;
|
|
39
39
|
private disposed;
|
|
40
|
+
/** Letting go of the cursor to leave the page, not to pause. */
|
|
41
|
+
private releasing;
|
|
42
|
+
private xrSession;
|
|
43
|
+
private xrMoveX;
|
|
44
|
+
private xrMoveZ;
|
|
45
|
+
private xrTurn;
|
|
46
|
+
private xrSprint;
|
|
47
|
+
private xrJumpHeld;
|
|
48
|
+
private xrInteractHeld;
|
|
49
|
+
/** Movement carried over from outside (a hub's joystick, still held as the visitor walked in). */
|
|
50
|
+
private carriedX;
|
|
51
|
+
private carriedZ;
|
|
40
52
|
constructor(options: InputOptions);
|
|
41
53
|
requestPointerLock(touchTriggered?: boolean): void;
|
|
42
54
|
setLocked(locked: boolean): void;
|
|
43
55
|
exitPointerLock(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Free the cursor without bringing up the pause screen, for leaving the page.
|
|
58
|
+
* With moveBeforeLock the visitor stays walking, so coming back with the
|
|
59
|
+
* browser's back button finds them in the world instead of on the menu.
|
|
60
|
+
*/
|
|
61
|
+
release(): void;
|
|
44
62
|
triggerAction(action: InputAction): void;
|
|
63
|
+
/** WebXR controllers join the same move / jump / interact actions as the keyboard. */
|
|
64
|
+
setXrSession(session: XRSession | null): void;
|
|
65
|
+
/**
|
|
66
|
+
* Read XR gamepads for this frame. Left stick walks, right stick turns,
|
|
67
|
+
* trigger or A/X jumps, squeeze interacts.
|
|
68
|
+
*/
|
|
69
|
+
pollXr(dt: number): void;
|
|
70
|
+
/** Radians to apply to the XR play-space this frame (right stick). */
|
|
71
|
+
readXrTurn(): number;
|
|
45
72
|
isDown(action: InputAction): boolean;
|
|
46
73
|
/** True once per key press. Reading it clears the flag. */
|
|
47
74
|
consume(action: InputAction): boolean;
|
|
48
75
|
/** The number key (0–9) pressed this frame, once. Reading it clears it. */
|
|
49
76
|
consumeDigit(): number | null;
|
|
77
|
+
/** Keep moving this way (same axes as getMoveAxis) until told otherwise; 0, 0 stops. */
|
|
78
|
+
carry(x: number, z: number): void;
|
|
50
79
|
/** Movement intent on the local XZ plane, already normalized. */
|
|
51
80
|
getMoveAxis(): {
|
|
52
81
|
x: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/controls/input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIvD,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,iEAAiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,qBAAa,KAAK;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,WAAW,SAAK;IAChB,WAAW,SAAK;IAChB,sDAAsD;IACtD,UAAU,SAAK;IACf,MAAM,UAAS;IAEf,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,OAAO,CAAqB;IACpC,wEAAwE;IACxE,OAAO,CAAC,WAAW,CAA0B;IAC7C,sDAAsD;IACtD,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,mBAAmB,CAAC,CAA4B;IACxD,OAAO,CAAC,QAAQ,CAAC,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAC,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAS;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/controls/input.ts"],"names":[],"mappings":"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"}
|
package/dist/controls/input.js
CHANGED
|
@@ -25,6 +25,18 @@ export class Input {
|
|
|
25
25
|
onEscape;
|
|
26
26
|
touch;
|
|
27
27
|
disposed = false;
|
|
28
|
+
/** Letting go of the cursor to leave the page, not to pause. */
|
|
29
|
+
releasing = false;
|
|
30
|
+
xrSession = null;
|
|
31
|
+
xrMoveX = 0;
|
|
32
|
+
xrMoveZ = 0;
|
|
33
|
+
xrTurn = 0;
|
|
34
|
+
xrSprint = false;
|
|
35
|
+
xrJumpHeld = false;
|
|
36
|
+
xrInteractHeld = false;
|
|
37
|
+
/** Movement carried over from outside (a hub's joystick, still held as the visitor walked in). */
|
|
38
|
+
carriedX = 0;
|
|
39
|
+
carriedZ = 0;
|
|
28
40
|
constructor(options) {
|
|
29
41
|
this.element = options.element;
|
|
30
42
|
this.moveBeforeLock = options.moveBeforeLock === true;
|
|
@@ -60,7 +72,7 @@ export class Input {
|
|
|
60
72
|
this.touch?.setVisible(true);
|
|
61
73
|
}
|
|
62
74
|
requestPointerLock(touchTriggered = false) {
|
|
63
|
-
if (this.disposed || this.locked)
|
|
75
|
+
if (this.disposed || this.locked || this.xrSession)
|
|
64
76
|
return;
|
|
65
77
|
// Mobile / touch devices do not support pointer lock. Enter directly.
|
|
66
78
|
if (touchTriggered || isTouchDevice()) {
|
|
@@ -114,12 +126,98 @@ export class Input {
|
|
|
114
126
|
this.onPointerLockChange?.(false);
|
|
115
127
|
}
|
|
116
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Free the cursor without bringing up the pause screen, for leaving the page.
|
|
131
|
+
* With moveBeforeLock the visitor stays walking, so coming back with the
|
|
132
|
+
* browser's back button finds them in the world instead of on the menu.
|
|
133
|
+
*/
|
|
134
|
+
release() {
|
|
135
|
+
if (!this.moveBeforeLock) {
|
|
136
|
+
this.exitPointerLock();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.pressed.clear();
|
|
140
|
+
this.justPressed.clear();
|
|
141
|
+
this.touch?.reset();
|
|
142
|
+
if (document.pointerLockElement === this.element) {
|
|
143
|
+
this.releasing = true;
|
|
144
|
+
document.exitPointerLock();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
117
147
|
triggerAction(action) {
|
|
118
148
|
this.justPressed.add(action);
|
|
119
149
|
}
|
|
150
|
+
/** WebXR controllers join the same move / jump / interact actions as the keyboard. */
|
|
151
|
+
setXrSession(session) {
|
|
152
|
+
this.xrSession = session;
|
|
153
|
+
this.xrMoveX = 0;
|
|
154
|
+
this.xrMoveZ = 0;
|
|
155
|
+
this.xrTurn = 0;
|
|
156
|
+
this.xrSprint = false;
|
|
157
|
+
this.xrJumpHeld = false;
|
|
158
|
+
this.xrInteractHeld = false;
|
|
159
|
+
if (session)
|
|
160
|
+
this.touch?.setVisible(false);
|
|
161
|
+
else
|
|
162
|
+
this.touch?.setVisible(this.locked && isTouchDevice());
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Read XR gamepads for this frame. Left stick walks, right stick turns,
|
|
166
|
+
* trigger or A/X jumps, squeeze interacts.
|
|
167
|
+
*/
|
|
168
|
+
pollXr(dt) {
|
|
169
|
+
this.xrMoveX = 0;
|
|
170
|
+
this.xrMoveZ = 0;
|
|
171
|
+
this.xrTurn = 0;
|
|
172
|
+
this.xrSprint = false;
|
|
173
|
+
let jump = false;
|
|
174
|
+
let interact = false;
|
|
175
|
+
if (this.xrSession) {
|
|
176
|
+
for (const source of this.xrSession.inputSources) {
|
|
177
|
+
const pad = source.gamepad;
|
|
178
|
+
if (!pad)
|
|
179
|
+
continue;
|
|
180
|
+
const stick = readThumbstick(pad);
|
|
181
|
+
if (source.handedness === 'right') {
|
|
182
|
+
this.xrTurn += stick.x * XR_TURN_SPEED * dt;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.xrMoveX += stick.x;
|
|
186
|
+
this.xrMoveZ += stick.y;
|
|
187
|
+
}
|
|
188
|
+
if (pad.buttons[0]?.pressed || pad.buttons[4]?.pressed)
|
|
189
|
+
jump = true;
|
|
190
|
+
if (pad.buttons[1]?.pressed)
|
|
191
|
+
interact = true;
|
|
192
|
+
if (pad.buttons[3]?.pressed)
|
|
193
|
+
this.xrSprint = true;
|
|
194
|
+
}
|
|
195
|
+
const length = Math.hypot(this.xrMoveX, this.xrMoveZ);
|
|
196
|
+
if (length > 1) {
|
|
197
|
+
this.xrMoveX /= length;
|
|
198
|
+
this.xrMoveZ /= length;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (jump && !this.xrJumpHeld)
|
|
202
|
+
this.justPressed.add('jump');
|
|
203
|
+
if (interact && !this.xrInteractHeld)
|
|
204
|
+
this.justPressed.add('interact');
|
|
205
|
+
this.xrJumpHeld = jump;
|
|
206
|
+
this.xrInteractHeld = interact;
|
|
207
|
+
}
|
|
208
|
+
/** Radians to apply to the XR play-space this frame (right stick). */
|
|
209
|
+
readXrTurn() {
|
|
210
|
+
const turn = this.xrTurn;
|
|
211
|
+
this.xrTurn = 0;
|
|
212
|
+
return turn;
|
|
213
|
+
}
|
|
120
214
|
isDown(action) {
|
|
121
215
|
if (this.touch?.isDown(action))
|
|
122
216
|
return true;
|
|
217
|
+
if (action === 'sprint' && this.xrSprint)
|
|
218
|
+
return true;
|
|
219
|
+
if (action === 'jump' && this.xrJumpHeld)
|
|
220
|
+
return true;
|
|
123
221
|
for (const code of this.keymap[action])
|
|
124
222
|
if (this.pressed.has(code))
|
|
125
223
|
return true;
|
|
@@ -140,6 +238,11 @@ export class Input {
|
|
|
140
238
|
this.pendingDigit = null;
|
|
141
239
|
return digit;
|
|
142
240
|
}
|
|
241
|
+
/** Keep moving this way (same axes as getMoveAxis) until told otherwise; 0, 0 stops. */
|
|
242
|
+
carry(x, z) {
|
|
243
|
+
this.carriedX = Number.isFinite(x) ? Math.max(-1, Math.min(1, x)) : 0;
|
|
244
|
+
this.carriedZ = Number.isFinite(z) ? Math.max(-1, Math.min(1, z)) : 0;
|
|
245
|
+
}
|
|
143
246
|
/** Movement intent on the local XZ plane, already normalized. */
|
|
144
247
|
getMoveAxis() {
|
|
145
248
|
let x = (this.isDown('right') ? 1 : 0) - (this.isDown('left') ? 1 : 0);
|
|
@@ -149,6 +252,10 @@ export class Input {
|
|
|
149
252
|
x += touchAxis.x;
|
|
150
253
|
z += touchAxis.z;
|
|
151
254
|
}
|
|
255
|
+
x += this.xrMoveX;
|
|
256
|
+
z += this.xrMoveZ;
|
|
257
|
+
x += this.carriedX;
|
|
258
|
+
z += this.carriedZ;
|
|
152
259
|
const length = Math.hypot(x, z);
|
|
153
260
|
if (length === 0)
|
|
154
261
|
return { x: 0, z: 0 };
|
|
@@ -232,6 +339,7 @@ export class Input {
|
|
|
232
339
|
handleBlur = () => {
|
|
233
340
|
this.pressed.clear();
|
|
234
341
|
this.justPressed.clear();
|
|
342
|
+
this.touch?.reset();
|
|
235
343
|
};
|
|
236
344
|
/**
|
|
237
345
|
* A click captures the cursor for looking. Touch is left alone: the joysticks
|
|
@@ -270,6 +378,12 @@ export class Input {
|
|
|
270
378
|
};
|
|
271
379
|
handlePointerLockChange = () => {
|
|
272
380
|
const isLocked = document.pointerLockElement === this.element;
|
|
381
|
+
if (!isLocked && this.releasing) {
|
|
382
|
+
// Quiet: the overlay keeps its walking state.
|
|
383
|
+
this.releasing = false;
|
|
384
|
+
this.locked = false;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
273
387
|
if (isLocked) {
|
|
274
388
|
this.setLocked(true);
|
|
275
389
|
}
|
|
@@ -285,6 +399,15 @@ export class Input {
|
|
|
285
399
|
}
|
|
286
400
|
};
|
|
287
401
|
}
|
|
402
|
+
const XR_TURN_SPEED = 2.4;
|
|
403
|
+
const XR_DEADZONE = 0.18;
|
|
404
|
+
function readThumbstick(pad) {
|
|
405
|
+
const x = pad.axes.length >= 4 ? pad.axes[2] : (pad.axes[0] ?? 0);
|
|
406
|
+
const y = pad.axes.length >= 4 ? pad.axes[3] : (pad.axes[1] ?? 0);
|
|
407
|
+
if (Math.hypot(x, y) < XR_DEADZONE)
|
|
408
|
+
return { x: 0, y: 0 };
|
|
409
|
+
return { x, y };
|
|
410
|
+
}
|
|
288
411
|
function isTypingTarget(target) {
|
|
289
412
|
if (!(target instanceof HTMLElement))
|
|
290
413
|
return false;
|