@threenative/core 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.
- package/dist/audio-3vkjtiuo.d.ts +35 -0
- package/dist/game-DRPs3M7r.d.ts +269 -0
- package/dist/hot.d.ts +17 -0
- package/dist/hot.js +109 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +1514 -0
- package/dist/playtest.d.ts +11 -0
- package/dist/playtest.js +614 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
import { AudioListener, Audio, PositionalAudio, Object3D, AnimationMixer, Vector2, Sprite, Raycaster, Plane, Vector3, PerspectiveCamera, OrthographicCamera, Scene as Scene$1 } from 'three';
|
|
2
|
+
import { instancedArray } from 'three/tsl';
|
|
3
|
+
import { RenderPipeline } from 'three/webgpu';
|
|
4
|
+
import { createStore } from 'zustand/vanilla';
|
|
5
|
+
|
|
6
|
+
// src/audio.ts
|
|
7
|
+
var buses = /* @__PURE__ */ new Set();
|
|
8
|
+
var AudioBus = class {
|
|
9
|
+
listener;
|
|
10
|
+
#camera;
|
|
11
|
+
#gestureTarget;
|
|
12
|
+
#gesture;
|
|
13
|
+
#queue = [];
|
|
14
|
+
#voices = /* @__PURE__ */ new Set();
|
|
15
|
+
#unlocked = false;
|
|
16
|
+
#disposed = false;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.#camera = options.camera;
|
|
19
|
+
this.listener = options.listener ?? new AudioListener();
|
|
20
|
+
const source = options.source ?? (() => typeof window === "undefined" ? void 0 : window);
|
|
21
|
+
this.#gestureTarget = options.gestureTarget ?? source();
|
|
22
|
+
this.setCamera(options.camera);
|
|
23
|
+
this.#gesture = () => {
|
|
24
|
+
void this.unlock().catch(() => void 0);
|
|
25
|
+
};
|
|
26
|
+
for (const event2 of ["keydown", "pointerdown", "touchstart"]) {
|
|
27
|
+
this.#gestureTarget?.addEventListener(event2, this.#gesture);
|
|
28
|
+
}
|
|
29
|
+
buses.add(this);
|
|
30
|
+
}
|
|
31
|
+
get queued() {
|
|
32
|
+
return this.#queue.length;
|
|
33
|
+
}
|
|
34
|
+
get voices() {
|
|
35
|
+
return this.#voices.size;
|
|
36
|
+
}
|
|
37
|
+
setCamera(camera) {
|
|
38
|
+
this.#camera = camera;
|
|
39
|
+
camera.add(this.listener);
|
|
40
|
+
}
|
|
41
|
+
reparent(camera) {
|
|
42
|
+
this.setCamera(camera);
|
|
43
|
+
}
|
|
44
|
+
async unlock() {
|
|
45
|
+
if (this.#disposed || this.#unlocked) return;
|
|
46
|
+
const context = this.listener.context;
|
|
47
|
+
if (context.resume !== void 0) await context.resume();
|
|
48
|
+
this.#unlocked = true;
|
|
49
|
+
const queued = this.#queue.splice(0);
|
|
50
|
+
for (const { start } of queued) start();
|
|
51
|
+
}
|
|
52
|
+
play(buffer, options = {}) {
|
|
53
|
+
assertBuffer(buffer);
|
|
54
|
+
const voice = new Audio(this.listener);
|
|
55
|
+
configureVoice(voice, options);
|
|
56
|
+
voice.setBuffer(buffer);
|
|
57
|
+
this.#queueOrStart(voice, options.fade, options.volume ?? 1);
|
|
58
|
+
return voice;
|
|
59
|
+
}
|
|
60
|
+
playAt(buffer, source, options = {}) {
|
|
61
|
+
assertBuffer(buffer);
|
|
62
|
+
const voice = new PositionalAudio(this.listener);
|
|
63
|
+
configureVoice(voice, options);
|
|
64
|
+
voice.setBuffer(buffer);
|
|
65
|
+
if (source instanceof Object3D) source.add(voice);
|
|
66
|
+
else {
|
|
67
|
+
voice.position.copy(source);
|
|
68
|
+
(this.#camera.parent ?? this.#camera).add(voice);
|
|
69
|
+
}
|
|
70
|
+
this.#queueOrStart(voice, options.fade, options.volume ?? 1);
|
|
71
|
+
return voice;
|
|
72
|
+
}
|
|
73
|
+
music(buffer, options = {}) {
|
|
74
|
+
return this.play(buffer, { ...options, loop: options.loop ?? true });
|
|
75
|
+
}
|
|
76
|
+
stop() {
|
|
77
|
+
for (const { voice } of this.#queue) voice.removeFromParent();
|
|
78
|
+
this.#queue = [];
|
|
79
|
+
for (const voice of this.#voices) {
|
|
80
|
+
try {
|
|
81
|
+
voice.stop();
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
voice.removeFromParent();
|
|
85
|
+
}
|
|
86
|
+
this.#voices.clear();
|
|
87
|
+
}
|
|
88
|
+
dispose() {
|
|
89
|
+
if (this.#disposed) return;
|
|
90
|
+
this.#disposed = true;
|
|
91
|
+
this.stop();
|
|
92
|
+
this.listener.removeFromParent();
|
|
93
|
+
for (const event2 of ["keydown", "pointerdown", "touchstart"]) {
|
|
94
|
+
if (this.#gesture !== void 0)
|
|
95
|
+
this.#gestureTarget?.removeEventListener(event2, this.#gesture);
|
|
96
|
+
}
|
|
97
|
+
buses.delete(this);
|
|
98
|
+
}
|
|
99
|
+
#queueOrStart(voice, fade, volume) {
|
|
100
|
+
const start = () => this.#start(voice, fade, volume);
|
|
101
|
+
if (this.#unlocked) start();
|
|
102
|
+
else this.#queue.push({ start, voice });
|
|
103
|
+
}
|
|
104
|
+
#start(voice, fade, volume) {
|
|
105
|
+
if (this.#disposed) return;
|
|
106
|
+
if (fade !== void 0 && fade > 0) {
|
|
107
|
+
const now = this.listener.context.currentTime;
|
|
108
|
+
voice.gain.gain.setValueAtTime(0, now);
|
|
109
|
+
voice.gain.gain.linearRampToValueAtTime(volume, now + fade);
|
|
110
|
+
}
|
|
111
|
+
voice.play();
|
|
112
|
+
this.#voices.add(voice);
|
|
113
|
+
const source = voice.source;
|
|
114
|
+
if (source !== null && "onended" in source) {
|
|
115
|
+
const onended = source.onended;
|
|
116
|
+
source.onended = () => {
|
|
117
|
+
onended?.();
|
|
118
|
+
this.#voices.delete(voice);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function configureVoice(voice, options) {
|
|
124
|
+
const volume = options.volume ?? 1;
|
|
125
|
+
if (!Number.isFinite(volume) || volume < 0)
|
|
126
|
+
throw new RangeError("volume must be finite and non-negative.");
|
|
127
|
+
if (options.fade !== void 0 && (!Number.isFinite(options.fade) || options.fade < 0)) {
|
|
128
|
+
throw new RangeError("fade must be finite and non-negative.");
|
|
129
|
+
}
|
|
130
|
+
voice.setLoop(options.loop ?? false);
|
|
131
|
+
voice.setVolume(options.fade === void 0 || options.fade === 0 ? volume : 0);
|
|
132
|
+
}
|
|
133
|
+
function assertBuffer(buffer) {
|
|
134
|
+
if (buffer === null || buffer === void 0)
|
|
135
|
+
throw new TypeError("AudioBus requires a non-null AudioBuffer.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/assets.ts
|
|
139
|
+
function resolvePath(basePath, path) {
|
|
140
|
+
if (/^(?:[a-z]+:)?\/\//iu.test(path) || path.startsWith("data:")) return path;
|
|
141
|
+
if (basePath.length === 0) return path;
|
|
142
|
+
return `${basePath.replace(/\/$/u, "")}/${path.replace(/^\//u, "")}`;
|
|
143
|
+
}
|
|
144
|
+
function loadWith(loader, url) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
loader.load(url, resolve, void 0, reject);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function createAssetLoader(options = {}) {
|
|
150
|
+
const basePath = options.basePath ?? "";
|
|
151
|
+
const cache = /* @__PURE__ */ new Map();
|
|
152
|
+
const cached = (kind, path, load) => {
|
|
153
|
+
const url = resolvePath(basePath, path);
|
|
154
|
+
const key = `${kind}:${url}`;
|
|
155
|
+
const existing = cache.get(key);
|
|
156
|
+
if (existing !== void 0) return existing;
|
|
157
|
+
const promise = load(url);
|
|
158
|
+
cache.set(key, promise);
|
|
159
|
+
return promise;
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
audio: (path) => cached("audio", path, async (url) => {
|
|
163
|
+
if (options.audio !== void 0) return options.audio(url);
|
|
164
|
+
const { AudioLoader: Loader } = await import('three');
|
|
165
|
+
return loadWith(new Loader(), url);
|
|
166
|
+
}),
|
|
167
|
+
clear: () => cache.clear(),
|
|
168
|
+
model: (path) => cached("model", path, async (url) => {
|
|
169
|
+
if (options.model !== void 0) return await options.model(url);
|
|
170
|
+
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
|
|
171
|
+
return await loadWith(new GLTFLoader(), url);
|
|
172
|
+
}),
|
|
173
|
+
release: (kind, path) => cache.delete(`${kind}:${resolvePath(basePath, path)}`),
|
|
174
|
+
texture: (path) => cached("texture", path, async (url) => {
|
|
175
|
+
if (options.texture !== void 0) return options.texture(url);
|
|
176
|
+
const { TextureLoader: Loader } = await import('three');
|
|
177
|
+
return loadWith(new Loader(), url);
|
|
178
|
+
})
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
var AnimationPlayer = class {
|
|
182
|
+
mixer;
|
|
183
|
+
#actions = /* @__PURE__ */ new Map();
|
|
184
|
+
#current;
|
|
185
|
+
#advancedFrames = 0;
|
|
186
|
+
#fadeFrom;
|
|
187
|
+
#fadeElapsed = 0;
|
|
188
|
+
#fadeDuration = 0;
|
|
189
|
+
constructor(options) {
|
|
190
|
+
this.mixer = new AnimationMixer(options.root);
|
|
191
|
+
for (const clip of options.clips) {
|
|
192
|
+
if (this.#actions.has(clip.name)) throw new Error(`Duplicate animation clip '${clip.name}'.`);
|
|
193
|
+
const action = this.mixer.clipAction(clip);
|
|
194
|
+
if (action === null)
|
|
195
|
+
throw new Error(`Animation clip '${clip.name}' could not create an action.`);
|
|
196
|
+
this.#actions.set(clip.name, action);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
get current() {
|
|
200
|
+
return this.#current;
|
|
201
|
+
}
|
|
202
|
+
get advancedFrames() {
|
|
203
|
+
return this.#advancedFrames;
|
|
204
|
+
}
|
|
205
|
+
play(name, options = {}) {
|
|
206
|
+
const next = this.#actions.get(name);
|
|
207
|
+
if (next === void 0) throw new Error(`Unknown animation clip '${name}'.`);
|
|
208
|
+
if (this.#current === name) return;
|
|
209
|
+
const previous = this.#current === void 0 ? void 0 : this.#actions.get(this.#current);
|
|
210
|
+
const fade = Math.max(0, options.fade ?? 0);
|
|
211
|
+
for (const action of this.#actions.values()) {
|
|
212
|
+
if (action !== previous) action.setEffectiveWeight(0).stop();
|
|
213
|
+
}
|
|
214
|
+
previous?.setEffectiveWeight(1).play();
|
|
215
|
+
next.reset().setEffectiveWeight(fade > 0 && previous !== void 0 ? 0 : 1).play();
|
|
216
|
+
if (previous === void 0 || fade === 0) {
|
|
217
|
+
previous?.stop();
|
|
218
|
+
this.#fadeFrom = void 0;
|
|
219
|
+
} else {
|
|
220
|
+
previous.play();
|
|
221
|
+
this.#fadeFrom = previous;
|
|
222
|
+
this.#fadeElapsed = 0;
|
|
223
|
+
this.#fadeDuration = fade;
|
|
224
|
+
}
|
|
225
|
+
this.#current = name;
|
|
226
|
+
this.#advancedFrames = 0;
|
|
227
|
+
}
|
|
228
|
+
update(dt) {
|
|
229
|
+
if (!Number.isFinite(dt) || dt < 0)
|
|
230
|
+
throw new Error("AnimationPlayer.update requires a finite non-negative dt.");
|
|
231
|
+
const before = this.mixer.time;
|
|
232
|
+
this.mixer.update(dt);
|
|
233
|
+
if (this.#fadeFrom !== void 0) {
|
|
234
|
+
this.#fadeElapsed = Math.min(this.#fadeDuration, this.#fadeElapsed + dt);
|
|
235
|
+
const progress = this.#fadeDuration === 0 ? 1 : this.#fadeElapsed / this.#fadeDuration;
|
|
236
|
+
this.#fadeFrom.setEffectiveWeight(1 - progress);
|
|
237
|
+
this.#actions.get(this.#current ?? "")?.setEffectiveWeight(progress);
|
|
238
|
+
if (progress >= 1) {
|
|
239
|
+
this.#fadeFrom.stop();
|
|
240
|
+
this.#fadeFrom = void 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (this.#current !== void 0 && this.mixer.time !== before) this.#advancedFrames += 1;
|
|
244
|
+
}
|
|
245
|
+
stop() {
|
|
246
|
+
this.mixer.stopAllAction();
|
|
247
|
+
this.#current = void 0;
|
|
248
|
+
this.#advancedFrames = 0;
|
|
249
|
+
this.#fadeFrom = void 0;
|
|
250
|
+
}
|
|
251
|
+
dispose() {
|
|
252
|
+
this.stop();
|
|
253
|
+
this.mixer.uncacheRoot(this.mixer.getRoot());
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/entities.ts
|
|
258
|
+
function autoFields(entity) {
|
|
259
|
+
const fields = {};
|
|
260
|
+
for (const key of Object.keys(entity)) {
|
|
261
|
+
if (Object.keys(fields).length >= 24) break;
|
|
262
|
+
const value = entity[key];
|
|
263
|
+
if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
|
|
264
|
+
fields[key] = value;
|
|
265
|
+
} else if (value !== null && typeof value === "object" && "toArray" in value) {
|
|
266
|
+
const toArray = value.toArray;
|
|
267
|
+
if (typeof toArray === "function") fields[key] = toArray.call(value);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return fields;
|
|
271
|
+
}
|
|
272
|
+
function disposeEntity(entity) {
|
|
273
|
+
const dispose = entity.dispose;
|
|
274
|
+
if (typeof dispose === "function") dispose.call(entity);
|
|
275
|
+
}
|
|
276
|
+
var Registry = class {
|
|
277
|
+
#named = /* @__PURE__ */ new Map();
|
|
278
|
+
add(name, entity) {
|
|
279
|
+
if (this.#named.has(name)) throw new Error(`Entity "${name}" already registered.`);
|
|
280
|
+
this.#named.set(name, entity);
|
|
281
|
+
return entity;
|
|
282
|
+
}
|
|
283
|
+
get(name) {
|
|
284
|
+
return this.#named.get(name);
|
|
285
|
+
}
|
|
286
|
+
remove(name) {
|
|
287
|
+
const entity = this.#named.get(name);
|
|
288
|
+
this.#named.delete(name);
|
|
289
|
+
if (entity !== void 0) disposeEntity(entity);
|
|
290
|
+
}
|
|
291
|
+
clear() {
|
|
292
|
+
const entities = [...new Set(this.#named.values())];
|
|
293
|
+
this.#named.clear();
|
|
294
|
+
for (const entity of entities) disposeEntity(entity);
|
|
295
|
+
}
|
|
296
|
+
snapshot() {
|
|
297
|
+
const result = {};
|
|
298
|
+
for (const [name, entity] of this.#named) {
|
|
299
|
+
const debug = entity.debug;
|
|
300
|
+
const fields = typeof debug === "function" ? debug.call(entity) : autoFields(entity);
|
|
301
|
+
const tags = entity.tags;
|
|
302
|
+
result[name] = {
|
|
303
|
+
...fields,
|
|
304
|
+
...Array.isArray(tags) && tags.every((tag) => typeof tag === "string") ? { tags: [...tags] } : {}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var DEFAULT_BINDINGS = {
|
|
311
|
+
move: {
|
|
312
|
+
down: ["KeyS", "ArrowDown"],
|
|
313
|
+
left: ["KeyA", "ArrowLeft"],
|
|
314
|
+
right: ["KeyD", "ArrowRight"],
|
|
315
|
+
up: ["KeyW", "ArrowUp"]
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function eventCode(event2) {
|
|
319
|
+
const candidate = event2;
|
|
320
|
+
return candidate.code ?? candidate.key;
|
|
321
|
+
}
|
|
322
|
+
var browserGamepads = () => globalThis.navigator?.getGamepads?.() ?? [];
|
|
323
|
+
var InputMap = class {
|
|
324
|
+
raw;
|
|
325
|
+
#bindings;
|
|
326
|
+
#target;
|
|
327
|
+
#heldKeys = /* @__PURE__ */ new Set();
|
|
328
|
+
#pointerPosition = new Vector2();
|
|
329
|
+
#pointerDown = false;
|
|
330
|
+
#pointerButtons = 0;
|
|
331
|
+
#gamepadAxes = [];
|
|
332
|
+
#gamepadButtons = [];
|
|
333
|
+
#previousPressed = /* @__PURE__ */ new Map();
|
|
334
|
+
#justPressed = /* @__PURE__ */ new Set();
|
|
335
|
+
#justReleased = /* @__PURE__ */ new Set();
|
|
336
|
+
#pointerTarget;
|
|
337
|
+
#source;
|
|
338
|
+
#listeners = [];
|
|
339
|
+
constructor(bindings = DEFAULT_BINDINGS, target = globalThis, pointerTarget = target, source = browserGamepads) {
|
|
340
|
+
this.#bindings = { ...DEFAULT_BINDINGS, ...bindings };
|
|
341
|
+
this.#target = target;
|
|
342
|
+
this.#pointerTarget = pointerTarget;
|
|
343
|
+
this.#source = source;
|
|
344
|
+
this.raw = {
|
|
345
|
+
gamepad: { axes: this.#gamepadAxes, buttons: this.#gamepadButtons },
|
|
346
|
+
keys: this.#heldKeys,
|
|
347
|
+
pointer: {
|
|
348
|
+
buttons: this.#pointerButtons,
|
|
349
|
+
down: this.#pointerDown,
|
|
350
|
+
position: this.#pointerPosition
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
this.#listen(this.#target, "keydown", (event2) => {
|
|
354
|
+
const code = eventCode(event2);
|
|
355
|
+
if (code !== void 0) this.#heldKeys.add(code);
|
|
356
|
+
});
|
|
357
|
+
this.#listen(this.#target, "keyup", (event2) => {
|
|
358
|
+
const code = eventCode(event2);
|
|
359
|
+
if (code !== void 0) this.#heldKeys.delete(code);
|
|
360
|
+
});
|
|
361
|
+
this.#listen(this.#pointerTarget, "pointerdown", (event2) => this.#pointerEvent(event2, true));
|
|
362
|
+
this.#listen(
|
|
363
|
+
this.#pointerTarget,
|
|
364
|
+
"pointermove",
|
|
365
|
+
(event2) => this.#pointerEvent(event2, this.#pointerDown)
|
|
366
|
+
);
|
|
367
|
+
this.#listen(this.#pointerTarget, "pointerup", (event2) => this.#pointerEvent(event2, false));
|
|
368
|
+
this.#listen(this.#target, "blur", () => this.clear());
|
|
369
|
+
}
|
|
370
|
+
vector(name) {
|
|
371
|
+
const binding = this.#bindings[name];
|
|
372
|
+
if (binding === void 0) return new Vector2();
|
|
373
|
+
const vector = new Vector2(
|
|
374
|
+
this.#isHeld(binding.left) ? -1 : 0,
|
|
375
|
+
this.#isHeld(binding.down) ? -1 : 0
|
|
376
|
+
);
|
|
377
|
+
if (this.#isHeld(binding.right)) vector.x += 1;
|
|
378
|
+
if (this.#isHeld(binding.up)) vector.y += 1;
|
|
379
|
+
if (name === "move" && this.#gamepadAxes.length >= 2) {
|
|
380
|
+
vector.x += this.#gamepadAxes[0] ?? 0;
|
|
381
|
+
vector.y += -(this.#gamepadAxes[1] ?? 0);
|
|
382
|
+
}
|
|
383
|
+
return vector.clampLength(0, 1);
|
|
384
|
+
}
|
|
385
|
+
pressed(name) {
|
|
386
|
+
const binding = this.#bindings[name];
|
|
387
|
+
if (binding === void 0) return false;
|
|
388
|
+
return this.#isHeld(binding.down) || this.#isHeld(binding.left) || this.#isHeld(binding.right) || this.#isHeld(binding.up) || binding.pointer === true && this.#pointerDown || (binding.buttons?.some((button) => this.#gamepadButtons[button] === true) ?? false);
|
|
389
|
+
}
|
|
390
|
+
justPressed(name) {
|
|
391
|
+
return this.#justPressed.has(name);
|
|
392
|
+
}
|
|
393
|
+
justReleased(name) {
|
|
394
|
+
return this.#justReleased.has(name);
|
|
395
|
+
}
|
|
396
|
+
tick() {
|
|
397
|
+
const gamepad = this.#source().find((item) => item !== null);
|
|
398
|
+
this.#gamepadAxes = gamepad?.axes ? Array.from(gamepad.axes) : [];
|
|
399
|
+
this.#gamepadButtons = gamepad?.buttons.map((button) => button.pressed) ?? [];
|
|
400
|
+
this.raw.gamepad.axes = this.#gamepadAxes;
|
|
401
|
+
this.raw.gamepad.buttons = this.#gamepadButtons;
|
|
402
|
+
this.#justPressed.clear();
|
|
403
|
+
this.#justReleased.clear();
|
|
404
|
+
for (const name of Object.keys(this.#bindings)) {
|
|
405
|
+
const current = this.pressed(name);
|
|
406
|
+
const previous = this.#previousPressed.get(name) ?? false;
|
|
407
|
+
if (current && !previous) this.#justPressed.add(name);
|
|
408
|
+
if (!current && previous) this.#justReleased.add(name);
|
|
409
|
+
this.#previousPressed.set(name, current);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
clear() {
|
|
413
|
+
this.#heldKeys.clear();
|
|
414
|
+
this.#pointerDown = false;
|
|
415
|
+
this.#pointerButtons = 0;
|
|
416
|
+
this.#pointerPosition.set(0, 0);
|
|
417
|
+
this.raw.pointer.buttons = 0;
|
|
418
|
+
this.raw.pointer.down = false;
|
|
419
|
+
this.#previousPressed.clear();
|
|
420
|
+
this.#justPressed.clear();
|
|
421
|
+
this.#justReleased.clear();
|
|
422
|
+
}
|
|
423
|
+
dispose() {
|
|
424
|
+
for (const [target, type, listener] of this.#listeners)
|
|
425
|
+
target.removeEventListener(type, listener);
|
|
426
|
+
this.#listeners = [];
|
|
427
|
+
this.clear();
|
|
428
|
+
}
|
|
429
|
+
#isHeld(codes) {
|
|
430
|
+
return codes?.some((code) => this.#heldKeys.has(code)) ?? false;
|
|
431
|
+
}
|
|
432
|
+
#listen(target, type, listener) {
|
|
433
|
+
target.addEventListener(type, listener);
|
|
434
|
+
this.#listeners.push([target, type, listener]);
|
|
435
|
+
}
|
|
436
|
+
#pointerEvent(event2, down) {
|
|
437
|
+
const pointer = event2;
|
|
438
|
+
this.#pointerDown = down;
|
|
439
|
+
this.#pointerButtons = pointer.buttons ?? 0;
|
|
440
|
+
this.#pointerPosition.set(pointer.clientX ?? 0, pointer.clientY ?? 0);
|
|
441
|
+
this.raw.pointer.buttons = this.#pointerButtons;
|
|
442
|
+
this.raw.pointer.down = down;
|
|
443
|
+
const capture = this.#pointerTarget;
|
|
444
|
+
if (down) capture.setPointerCapture?.(pointer.pointerId ?? 0);
|
|
445
|
+
else capture.releasePointerCapture?.(pointer.pointerId ?? 0);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
function input(bindings, target, pointerTarget, source) {
|
|
449
|
+
return new InputMap(bindings, target, pointerTarget, source);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/loop.ts
|
|
453
|
+
var FixedStepLoop = class {
|
|
454
|
+
step;
|
|
455
|
+
maxSteps;
|
|
456
|
+
#onUpdate;
|
|
457
|
+
#onRender;
|
|
458
|
+
#requestFrame;
|
|
459
|
+
#cancelFrame;
|
|
460
|
+
#accumulator = 0;
|
|
461
|
+
#lastTime;
|
|
462
|
+
#frameHandle;
|
|
463
|
+
#running = false;
|
|
464
|
+
#fps = 0;
|
|
465
|
+
#lastRenderTime;
|
|
466
|
+
constructor(options) {
|
|
467
|
+
this.step = options.step ?? 1 / 60;
|
|
468
|
+
this.maxSteps = options.maxSteps ?? 5;
|
|
469
|
+
this.#onUpdate = options.onUpdate;
|
|
470
|
+
this.#onRender = options.onRender ?? (() => void 0);
|
|
471
|
+
this.#requestFrame = options.requestFrame ?? ((callback) => typeof globalThis.requestAnimationFrame === "function" ? globalThis.requestAnimationFrame(callback) : 0);
|
|
472
|
+
this.#cancelFrame = options.cancelFrame ?? ((handle) => {
|
|
473
|
+
if (typeof globalThis.cancelAnimationFrame === "function")
|
|
474
|
+
globalThis.cancelAnimationFrame(handle);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
get running() {
|
|
478
|
+
return this.#running;
|
|
479
|
+
}
|
|
480
|
+
get fps() {
|
|
481
|
+
return this.#fps;
|
|
482
|
+
}
|
|
483
|
+
start(now = globalThis.performance?.now() ?? 0) {
|
|
484
|
+
if (this.#running) return;
|
|
485
|
+
this.#running = true;
|
|
486
|
+
this.#lastTime = now;
|
|
487
|
+
this.#lastRenderTime = void 0;
|
|
488
|
+
this.#fps = 0;
|
|
489
|
+
this.#frameHandle = this.#requestFrame((time) => this.#frame(time));
|
|
490
|
+
}
|
|
491
|
+
stop() {
|
|
492
|
+
this.#running = false;
|
|
493
|
+
if (this.#frameHandle !== void 0) this.#cancelFrame(this.#frameHandle);
|
|
494
|
+
this.#frameHandle = void 0;
|
|
495
|
+
this.#lastTime = void 0;
|
|
496
|
+
this.#accumulator = 0;
|
|
497
|
+
}
|
|
498
|
+
stepFrame(now) {
|
|
499
|
+
const last = this.#lastTime ?? now;
|
|
500
|
+
const elapsed = Math.max(0, (now - last) / 1e3);
|
|
501
|
+
this.#lastTime = now;
|
|
502
|
+
this.#accumulator += elapsed;
|
|
503
|
+
let updates = 0;
|
|
504
|
+
while (this.#accumulator + Number.EPSILON >= this.step && updates < this.maxSteps) {
|
|
505
|
+
this.#onUpdate(this.step);
|
|
506
|
+
this.#accumulator -= this.step;
|
|
507
|
+
updates += 1;
|
|
508
|
+
}
|
|
509
|
+
if (updates === this.maxSteps && this.#accumulator >= this.step) this.#accumulator = 0;
|
|
510
|
+
if (Number.isFinite(now)) {
|
|
511
|
+
if (this.#lastRenderTime !== void 0) {
|
|
512
|
+
const frameMs = now - this.#lastRenderTime;
|
|
513
|
+
if (frameMs > 0) this.#fps += (1e3 / frameMs - this.#fps) * 0.1;
|
|
514
|
+
}
|
|
515
|
+
this.#lastRenderTime = now;
|
|
516
|
+
}
|
|
517
|
+
this.#onRender();
|
|
518
|
+
return updates;
|
|
519
|
+
}
|
|
520
|
+
advance(ticks) {
|
|
521
|
+
if (!this.#running) throw new Error("Cannot advance a stopped loop.");
|
|
522
|
+
if (!Number.isInteger(ticks) || ticks <= 0)
|
|
523
|
+
throw new Error("advance ticks must be a positive integer.");
|
|
524
|
+
for (let index = 0; index < ticks; index += 1) {
|
|
525
|
+
this.#lastTime = (this.#lastTime ?? 0) + this.step * 1e3;
|
|
526
|
+
this.#onUpdate(this.step);
|
|
527
|
+
}
|
|
528
|
+
return ticks;
|
|
529
|
+
}
|
|
530
|
+
#frame(time) {
|
|
531
|
+
if (!this.#running) return;
|
|
532
|
+
try {
|
|
533
|
+
this.stepFrame(time);
|
|
534
|
+
} finally {
|
|
535
|
+
if (this.#running) {
|
|
536
|
+
this.#frameHandle = this.#requestFrame((nextTime) => this.#frame(nextTime));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
function computeNode(name, value) {
|
|
542
|
+
if (value === null || typeof value !== "object" || value.isComputeNode !== true) {
|
|
543
|
+
throw new Error(`GPUParticles3D.${name} must return a TSL compute node.`);
|
|
544
|
+
}
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
var GPUParticles3D = class extends Sprite {
|
|
548
|
+
amount;
|
|
549
|
+
buffers;
|
|
550
|
+
emitting = true;
|
|
551
|
+
#start;
|
|
552
|
+
#process;
|
|
553
|
+
#renderer;
|
|
554
|
+
#released = false;
|
|
555
|
+
constructor(options) {
|
|
556
|
+
if (!Number.isInteger(options.amount) || options.amount <= 0)
|
|
557
|
+
throw new Error("GPUParticles3D.amount must be a positive integer.");
|
|
558
|
+
if (options.material === void 0) throw new Error("GPUParticles3D.material is required.");
|
|
559
|
+
if (typeof options.start !== "function")
|
|
560
|
+
throw new Error("GPUParticles3D.start must be a function.");
|
|
561
|
+
if (typeof options.process !== "function")
|
|
562
|
+
throw new Error("GPUParticles3D.process must be a function.");
|
|
563
|
+
super(options.material);
|
|
564
|
+
this.amount = options.amount;
|
|
565
|
+
this.buffers = {
|
|
566
|
+
positions: instancedArray(options.amount, "vec3"),
|
|
567
|
+
velocities: instancedArray(options.amount, "vec3")
|
|
568
|
+
};
|
|
569
|
+
options.material.positionNode = this.buffers.positions.toAttribute();
|
|
570
|
+
this.count = options.amount;
|
|
571
|
+
this.frustumCulled = false;
|
|
572
|
+
this.#start = computeNode("start", options.start(this.buffers));
|
|
573
|
+
this.#process = computeNode("process", options.process(this.buffers));
|
|
574
|
+
this.addEventListener("removed", this.#onRemoved);
|
|
575
|
+
}
|
|
576
|
+
get released() {
|
|
577
|
+
return this.#released;
|
|
578
|
+
}
|
|
579
|
+
attachRenderer(renderer) {
|
|
580
|
+
if (this.#released) throw new Error("GPUParticles3D cannot be attached after release.");
|
|
581
|
+
if (this.#renderer === renderer) return;
|
|
582
|
+
this.#renderer = renderer;
|
|
583
|
+
renderer.compute(this.#start);
|
|
584
|
+
}
|
|
585
|
+
process(renderer = this.#renderer) {
|
|
586
|
+
if (this.#released || !this.emitting) return;
|
|
587
|
+
if (renderer === void 0) throw new Error("GPUParticles3D is not attached to a renderer.");
|
|
588
|
+
renderer.compute(this.#process);
|
|
589
|
+
}
|
|
590
|
+
restart() {
|
|
591
|
+
if (this.#released) throw new Error("GPUParticles3D cannot restart after release.");
|
|
592
|
+
if (this.#renderer === void 0)
|
|
593
|
+
throw new Error("GPUParticles3D is not attached to a renderer.");
|
|
594
|
+
this.#renderer.compute(this.#start);
|
|
595
|
+
}
|
|
596
|
+
detach() {
|
|
597
|
+
if (this.#released) return;
|
|
598
|
+
this.#renderer = void 0;
|
|
599
|
+
this.#start.dispose();
|
|
600
|
+
this.#process.dispose();
|
|
601
|
+
this.buffers.positions.value.dispose();
|
|
602
|
+
this.buffers.velocities.value.dispose();
|
|
603
|
+
this.#released = true;
|
|
604
|
+
}
|
|
605
|
+
#onRemoved = () => this.detach();
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/random.ts
|
|
609
|
+
function createRandom(seed) {
|
|
610
|
+
if (seed !== void 0 && !Number.isFinite(seed)) throw new TypeError("seed must be finite.");
|
|
611
|
+
let state = seed === void 0 ? 0 : seed >>> 0;
|
|
612
|
+
const deterministic = seed !== void 0;
|
|
613
|
+
const random = (() => {
|
|
614
|
+
if (!deterministic) return Math.random();
|
|
615
|
+
state = Math.imul(1664525, state) + 1013904223 >>> 0;
|
|
616
|
+
return state / 4294967296;
|
|
617
|
+
});
|
|
618
|
+
random.range = (min, max) => {
|
|
619
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
620
|
+
throw new RangeError("range requires finite bounds with max greater than min.");
|
|
621
|
+
}
|
|
622
|
+
return min + random() * (max - min);
|
|
623
|
+
};
|
|
624
|
+
random.pick = (items) => {
|
|
625
|
+
if (items.length === 0) throw new RangeError("pick requires a non-empty list.");
|
|
626
|
+
return items[Math.floor(random() * items.length)];
|
|
627
|
+
};
|
|
628
|
+
Object.defineProperty(random, "state", {
|
|
629
|
+
get: () => {
|
|
630
|
+
if (!deterministic) throw new Error("state is unavailable on an unseeded random.");
|
|
631
|
+
return state;
|
|
632
|
+
},
|
|
633
|
+
set: (value) => {
|
|
634
|
+
if (!deterministic) throw new Error("state is unavailable on an unseeded random.");
|
|
635
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
636
|
+
throw new TypeError("state must be a finite integer.");
|
|
637
|
+
}
|
|
638
|
+
state = value >>> 0;
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
return random;
|
|
642
|
+
}
|
|
643
|
+
function readCanvasSize(canvas) {
|
|
644
|
+
return [
|
|
645
|
+
Math.max(1, canvas.clientWidth || globalThis.innerWidth || 1),
|
|
646
|
+
Math.max(1, canvas.clientHeight || globalThis.innerHeight || 1)
|
|
647
|
+
];
|
|
648
|
+
}
|
|
649
|
+
function observeCanvasResize(canvas, resize) {
|
|
650
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
651
|
+
const observer = new ResizeObserver(resize);
|
|
652
|
+
observer.observe(canvas);
|
|
653
|
+
return () => observer.disconnect();
|
|
654
|
+
}
|
|
655
|
+
if (typeof globalThis.addEventListener !== "function") return () => void 0;
|
|
656
|
+
globalThis.addEventListener("resize", resize);
|
|
657
|
+
return () => globalThis.removeEventListener("resize", resize);
|
|
658
|
+
}
|
|
659
|
+
function wrapRenderer(raw, kind) {
|
|
660
|
+
let outputPipeline;
|
|
661
|
+
return {
|
|
662
|
+
domElement: raw.domElement,
|
|
663
|
+
kind,
|
|
664
|
+
raw,
|
|
665
|
+
compute: (node) => {
|
|
666
|
+
if (kind !== "webgpu") throw new Error(`compute is unavailable on the ${kind} renderer.`);
|
|
667
|
+
if (typeof raw.compute !== "function")
|
|
668
|
+
throw new Error("webgpu renderer does not expose compute().");
|
|
669
|
+
raw.compute(node);
|
|
670
|
+
},
|
|
671
|
+
dispose: () => {
|
|
672
|
+
outputPipeline?.dispose();
|
|
673
|
+
outputPipeline = void 0;
|
|
674
|
+
raw.dispose?.();
|
|
675
|
+
},
|
|
676
|
+
render: (scene, camera) => {
|
|
677
|
+
if (outputPipeline === void 0) raw.render(scene, camera);
|
|
678
|
+
else outputPipeline.render();
|
|
679
|
+
},
|
|
680
|
+
setOutputNode: (node) => {
|
|
681
|
+
if (kind !== "webgpu")
|
|
682
|
+
throw new Error(`setOutputNode is unavailable on the ${kind} renderer.`);
|
|
683
|
+
outputPipeline?.dispose();
|
|
684
|
+
outputPipeline = new RenderPipeline(
|
|
685
|
+
raw,
|
|
686
|
+
node
|
|
687
|
+
);
|
|
688
|
+
},
|
|
689
|
+
setSize: (width, height, updateStyle = false) => raw.setSize(width, height, updateStyle)
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function addResizeHandling(renderer, source) {
|
|
693
|
+
const resize = () => {
|
|
694
|
+
const [width, height] = source?.readSize(renderer.domElement) ?? readCanvasSize(renderer.domElement);
|
|
695
|
+
renderer.setSize(width, height, false);
|
|
696
|
+
};
|
|
697
|
+
resize();
|
|
698
|
+
return source?.observeResize(renderer.domElement, resize) ?? observeCanvasResize(renderer.domElement, resize);
|
|
699
|
+
}
|
|
700
|
+
async function createRenderer(options = {}) {
|
|
701
|
+
const source = options.source;
|
|
702
|
+
const canvas = options.canvas ?? source?.createCanvas() ?? document.createElement("canvas");
|
|
703
|
+
const preferWebGPU = options.preferWebGPU ?? true;
|
|
704
|
+
let renderer;
|
|
705
|
+
if (preferWebGPU && (source?.hasWebGPU() ?? "gpu" in (globalThis.navigator ?? {}))) {
|
|
706
|
+
try {
|
|
707
|
+
const raw = options.webgpuFactory ? await options.webgpuFactory(canvas) : new (await import('three/webgpu')).WebGPURenderer({ canvas, antialias: true });
|
|
708
|
+
const instance = raw;
|
|
709
|
+
await instance.init?.();
|
|
710
|
+
renderer = wrapRenderer(instance, "webgpu");
|
|
711
|
+
} catch {
|
|
712
|
+
renderer = void 0;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (renderer === void 0) {
|
|
716
|
+
const raw = options.webgl2Factory ? options.webgl2Factory(canvas) : new (await import('three')).WebGLRenderer({ canvas, antialias: true });
|
|
717
|
+
renderer = wrapRenderer(raw, "webgl2");
|
|
718
|
+
}
|
|
719
|
+
const stopResize = addResizeHandling(renderer, source);
|
|
720
|
+
const dispose = renderer.dispose;
|
|
721
|
+
renderer.dispose = () => {
|
|
722
|
+
stopResize();
|
|
723
|
+
dispose();
|
|
724
|
+
};
|
|
725
|
+
return renderer;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/schedule.ts
|
|
729
|
+
function createHandle(cancel, active) {
|
|
730
|
+
const handle = (() => cancel());
|
|
731
|
+
handle.cancel = handle;
|
|
732
|
+
Object.defineProperty(handle, "active", { get: active });
|
|
733
|
+
return handle;
|
|
734
|
+
}
|
|
735
|
+
var Scheduler = class {
|
|
736
|
+
#entries = /* @__PURE__ */ new Set();
|
|
737
|
+
get size() {
|
|
738
|
+
return this.#entries.size;
|
|
739
|
+
}
|
|
740
|
+
after(delay, callback) {
|
|
741
|
+
assertDelay(delay, "delay");
|
|
742
|
+
let elapsed = 0;
|
|
743
|
+
const entry = this.#add((dt) => {
|
|
744
|
+
elapsed += dt;
|
|
745
|
+
if (elapsed + Number.EPSILON < delay) return;
|
|
746
|
+
entry.cancel();
|
|
747
|
+
callback();
|
|
748
|
+
});
|
|
749
|
+
return entry.handle;
|
|
750
|
+
}
|
|
751
|
+
every(callback) {
|
|
752
|
+
const entry = this.#add(callback);
|
|
753
|
+
return entry.handle;
|
|
754
|
+
}
|
|
755
|
+
tween(target, properties, duration) {
|
|
756
|
+
assertDelay(duration, "duration");
|
|
757
|
+
const values = Object.entries(properties).map(([key, end]) => {
|
|
758
|
+
if (typeof end !== "number" || !Number.isFinite(end)) {
|
|
759
|
+
throw new TypeError(`Tween property '${key}' must be a finite number.`);
|
|
760
|
+
}
|
|
761
|
+
const start = target[key];
|
|
762
|
+
if (typeof start !== "number" || !Number.isFinite(start)) {
|
|
763
|
+
throw new TypeError(`Tween property '${key}' must start as a finite number.`);
|
|
764
|
+
}
|
|
765
|
+
return { end, key, start };
|
|
766
|
+
});
|
|
767
|
+
if (duration === 0) {
|
|
768
|
+
for (const { end, key } of values) target[key] = end;
|
|
769
|
+
return Promise.resolve();
|
|
770
|
+
}
|
|
771
|
+
let elapsed = 0;
|
|
772
|
+
let settled = false;
|
|
773
|
+
const settle = () => {
|
|
774
|
+
if (settled) return;
|
|
775
|
+
settled = true;
|
|
776
|
+
resolve();
|
|
777
|
+
};
|
|
778
|
+
let resolve;
|
|
779
|
+
const promise = new Promise((finish) => {
|
|
780
|
+
resolve = finish;
|
|
781
|
+
});
|
|
782
|
+
const entry = this.#add((dt) => {
|
|
783
|
+
elapsed = Math.min(duration, elapsed + dt);
|
|
784
|
+
const progress = elapsed / duration;
|
|
785
|
+
for (const { end, key, start } of values) {
|
|
786
|
+
target[key] = start + (end - start) * progress;
|
|
787
|
+
}
|
|
788
|
+
if (progress >= 1) {
|
|
789
|
+
entry.cancel();
|
|
790
|
+
settle();
|
|
791
|
+
}
|
|
792
|
+
}, settle);
|
|
793
|
+
return promise;
|
|
794
|
+
}
|
|
795
|
+
tick(dt) {
|
|
796
|
+
if (!Number.isFinite(dt) || dt < 0)
|
|
797
|
+
throw new TypeError("Tick duration must be finite and non-negative.");
|
|
798
|
+
for (const entry of [...this.#entries]) {
|
|
799
|
+
if (entry.active) entry.tick(dt);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
clear() {
|
|
803
|
+
for (const entry of [...this.#entries]) entry.cancel();
|
|
804
|
+
}
|
|
805
|
+
#add(tick, onCancel) {
|
|
806
|
+
const entry = {
|
|
807
|
+
active: true,
|
|
808
|
+
cancel: () => {
|
|
809
|
+
if (!entry.active) return;
|
|
810
|
+
entry.active = false;
|
|
811
|
+
this.#entries.delete(entry);
|
|
812
|
+
onCancel?.();
|
|
813
|
+
},
|
|
814
|
+
handle: void 0,
|
|
815
|
+
tick
|
|
816
|
+
};
|
|
817
|
+
entry.handle = createHandle(entry.cancel, () => entry.active);
|
|
818
|
+
this.#entries.add(entry);
|
|
819
|
+
return entry;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
function assertDelay(value, label) {
|
|
823
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
824
|
+
throw new TypeError(`${label} must be finite and non-negative.`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function createGameStore(initial, intervalMs = 100) {
|
|
828
|
+
const store = createStore(() => ({ ...initial }));
|
|
829
|
+
let pending;
|
|
830
|
+
let timer;
|
|
831
|
+
const flush = () => {
|
|
832
|
+
if (pending === void 0) return;
|
|
833
|
+
const patch = pending;
|
|
834
|
+
pending = void 0;
|
|
835
|
+
store.setState(patch);
|
|
836
|
+
};
|
|
837
|
+
const gameStore = store;
|
|
838
|
+
gameStore.set = (patch) => {
|
|
839
|
+
const next = typeof patch === "function" ? patch(store.getState()) : patch;
|
|
840
|
+
pending = { ...pending, ...next };
|
|
841
|
+
};
|
|
842
|
+
gameStore.flush = flush;
|
|
843
|
+
gameStore.start = () => {
|
|
844
|
+
if (timer !== void 0) return;
|
|
845
|
+
timer = setInterval(flush, intervalMs);
|
|
846
|
+
};
|
|
847
|
+
gameStore.stop = () => {
|
|
848
|
+
if (timer !== void 0) clearInterval(timer);
|
|
849
|
+
timer = void 0;
|
|
850
|
+
flush();
|
|
851
|
+
};
|
|
852
|
+
return gameStore;
|
|
853
|
+
}
|
|
854
|
+
var Viewport = class {
|
|
855
|
+
camera;
|
|
856
|
+
renderer;
|
|
857
|
+
#source;
|
|
858
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
859
|
+
#size = { aspect: 1, height: 1, width: 1 };
|
|
860
|
+
#stopObserving = () => void 0;
|
|
861
|
+
#disposed = false;
|
|
862
|
+
#raycaster = new Raycaster();
|
|
863
|
+
#plane = new Plane(new Vector3(0, 0, 1));
|
|
864
|
+
#projected = new Vector3();
|
|
865
|
+
constructor(options) {
|
|
866
|
+
this.camera = options.camera;
|
|
867
|
+
this.renderer = options.renderer;
|
|
868
|
+
this.#source = options.source;
|
|
869
|
+
this.#stopObserving = this.#source?.observeResize(this.renderer.domElement, this.#resize) ?? observeCanvasResize(this.renderer.domElement, this.#resize);
|
|
870
|
+
this.resize();
|
|
871
|
+
}
|
|
872
|
+
get size() {
|
|
873
|
+
return this.#size;
|
|
874
|
+
}
|
|
875
|
+
projectPosition(screen, z = 0) {
|
|
876
|
+
if (!Number.isFinite(z)) throw new Error("Viewport.projectPosition z must be finite.");
|
|
877
|
+
const ndc = new Vector2(
|
|
878
|
+
screen.x / this.#size.width * 2 - 1,
|
|
879
|
+
-(screen.y / this.#size.height * 2 - 1)
|
|
880
|
+
);
|
|
881
|
+
this.#raycaster.setFromCamera(ndc, this.camera);
|
|
882
|
+
this.#plane.constant = -z;
|
|
883
|
+
const point = this.#raycaster.ray.intersectPlane(this.#plane, this.#projected);
|
|
884
|
+
if (point === null)
|
|
885
|
+
throw new Error("Viewport.projectPosition cannot reach the requested z plane.");
|
|
886
|
+
return point.clone();
|
|
887
|
+
}
|
|
888
|
+
unprojectPosition(world) {
|
|
889
|
+
const ndc = world.clone().project(this.camera);
|
|
890
|
+
return new Vector2((ndc.x + 1) / 2 * this.#size.width, (1 - ndc.y) / 2 * this.#size.height);
|
|
891
|
+
}
|
|
892
|
+
onResize(handler) {
|
|
893
|
+
if (this.#disposed) return () => void 0;
|
|
894
|
+
this.#listeners.add(handler);
|
|
895
|
+
return () => this.#listeners.delete(handler);
|
|
896
|
+
}
|
|
897
|
+
resize() {
|
|
898
|
+
if (this.#disposed) return;
|
|
899
|
+
let next = this.#source?.readSize(this.renderer.domElement);
|
|
900
|
+
if (next === void 0) {
|
|
901
|
+
const [width, height] = readCanvasSize(this.renderer.domElement);
|
|
902
|
+
next = { aspect: width / height, height, width };
|
|
903
|
+
}
|
|
904
|
+
const changed = next.width !== this.#size.width || next.height !== this.#size.height || next.aspect !== this.#size.aspect;
|
|
905
|
+
this.#size = next;
|
|
906
|
+
if (this.camera instanceof PerspectiveCamera) {
|
|
907
|
+
this.camera.aspect = next.aspect;
|
|
908
|
+
this.camera.updateProjectionMatrix();
|
|
909
|
+
} else if (this.camera instanceof OrthographicCamera) {
|
|
910
|
+
const size = (this.camera.top - this.camera.bottom) / 2;
|
|
911
|
+
this.camera.left = -size * next.aspect;
|
|
912
|
+
this.camera.right = size * next.aspect;
|
|
913
|
+
this.camera.top = size;
|
|
914
|
+
this.camera.bottom = -size;
|
|
915
|
+
this.camera.updateProjectionMatrix();
|
|
916
|
+
}
|
|
917
|
+
if (changed) for (const listener of this.#listeners) listener(next);
|
|
918
|
+
}
|
|
919
|
+
dispose() {
|
|
920
|
+
if (this.#disposed) return;
|
|
921
|
+
this.#disposed = true;
|
|
922
|
+
this.#stopObserving();
|
|
923
|
+
this.#stopObserving = () => void 0;
|
|
924
|
+
this.#listeners.clear();
|
|
925
|
+
}
|
|
926
|
+
#resize = () => this.resize();
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/game.ts
|
|
930
|
+
function installDevTools(entities, host) {
|
|
931
|
+
const isDev = import.meta.env?.DEV === true;
|
|
932
|
+
if (!isDev || host === void 0) return () => void 0;
|
|
933
|
+
const devTools = {
|
|
934
|
+
...host.__THREENATIVE__,
|
|
935
|
+
snapshot: () => entities.snapshot()
|
|
936
|
+
};
|
|
937
|
+
host.__THREENATIVE__ = devTools;
|
|
938
|
+
return () => {
|
|
939
|
+
if (host.__THREENATIVE__ !== devTools) return;
|
|
940
|
+
const remaining = Object.fromEntries(
|
|
941
|
+
Object.entries(devTools).filter(([key]) => key !== "snapshot")
|
|
942
|
+
);
|
|
943
|
+
host.__THREENATIVE__ = Object.keys(remaining).length === 0 ? void 0 : remaining;
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function positiveCameraValue(name, value) {
|
|
947
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
948
|
+
throw new Error(`camera.${name} must be finite and positive.`);
|
|
949
|
+
return value;
|
|
950
|
+
}
|
|
951
|
+
function validateCameraConfig(config) {
|
|
952
|
+
if (config === void 0) return;
|
|
953
|
+
const near = positiveCameraValue("near", config.near ?? 0.1);
|
|
954
|
+
const far = positiveCameraValue("far", config.far ?? 2e3);
|
|
955
|
+
if (far <= near) throw new Error("camera.far must be greater than camera.near.");
|
|
956
|
+
if (config.projection === "perspective") {
|
|
957
|
+
const fov = config.fov ?? 60;
|
|
958
|
+
if (!Number.isFinite(fov) || fov <= 0 || fov >= 180)
|
|
959
|
+
throw new Error("camera.fov must be finite and between 0 and 180 degrees.");
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
positiveCameraValue("size", config.size);
|
|
963
|
+
}
|
|
964
|
+
function clearScene(scene, particles) {
|
|
965
|
+
for (const particle of particles) particle.detach();
|
|
966
|
+
particles.clear();
|
|
967
|
+
scene.clear();
|
|
968
|
+
scene.background = null;
|
|
969
|
+
scene.environment = null;
|
|
970
|
+
scene.fog = null;
|
|
971
|
+
}
|
|
972
|
+
function createCamera(config) {
|
|
973
|
+
if (config === void 0) return new PerspectiveCamera(60, 1, 0.1, 2e3);
|
|
974
|
+
validateCameraConfig(config);
|
|
975
|
+
const near = config.near ?? 0.1;
|
|
976
|
+
const far = config.far ?? 2e3;
|
|
977
|
+
if (config.projection === "perspective")
|
|
978
|
+
return new PerspectiveCamera(config.fov ?? 60, 1, near, far);
|
|
979
|
+
const size = config.size;
|
|
980
|
+
return new OrthographicCamera(-size, size, size, -size, near, far);
|
|
981
|
+
}
|
|
982
|
+
var GameImpl = class {
|
|
983
|
+
#config;
|
|
984
|
+
#ctx;
|
|
985
|
+
#scene;
|
|
986
|
+
#sceneFrame;
|
|
987
|
+
#renderer;
|
|
988
|
+
#viewport;
|
|
989
|
+
#input;
|
|
990
|
+
#state;
|
|
991
|
+
#initialState;
|
|
992
|
+
#loop;
|
|
993
|
+
#cleanup = [];
|
|
994
|
+
#particles = /* @__PURE__ */ new Set();
|
|
995
|
+
#entities;
|
|
996
|
+
#random;
|
|
997
|
+
#scheduler;
|
|
998
|
+
#activePlugins = [];
|
|
999
|
+
#disposedPlugins = /* @__PURE__ */ new Set();
|
|
1000
|
+
#pendingStart;
|
|
1001
|
+
#aborted = false;
|
|
1002
|
+
#sceneEntered = false;
|
|
1003
|
+
#paused = false;
|
|
1004
|
+
#started = false;
|
|
1005
|
+
constructor(config) {
|
|
1006
|
+
this.#config = config;
|
|
1007
|
+
validateCameraConfig(config.camera);
|
|
1008
|
+
const startScene = this.#config.scenes[this.#config.start];
|
|
1009
|
+
if (startScene === void 0) throw new Error(`Unknown start scene '${this.#config.start}'.`);
|
|
1010
|
+
const initialState = this.#config.initialState ?? startScene.initialState;
|
|
1011
|
+
if (initialState === void 0) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`Scene '${this.#config.start}' must declare static initialState or provide config.initialState.`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
this.#state = createGameStore(initialState, this.#config.stateFlushMs);
|
|
1017
|
+
this.#initialState = { ...initialState };
|
|
1018
|
+
}
|
|
1019
|
+
get ctx() {
|
|
1020
|
+
return this.#ctx;
|
|
1021
|
+
}
|
|
1022
|
+
get scene() {
|
|
1023
|
+
return this.#scene;
|
|
1024
|
+
}
|
|
1025
|
+
get state() {
|
|
1026
|
+
return this.#state;
|
|
1027
|
+
}
|
|
1028
|
+
goto(name) {
|
|
1029
|
+
if (this.#ctx === void 0) throw new Error("Cannot call game.goto() before start().");
|
|
1030
|
+
this.#state.stop();
|
|
1031
|
+
this.#state.setState({ ...this.#initialState });
|
|
1032
|
+
this.#state.start();
|
|
1033
|
+
return this.#goto(name, this.#ctx);
|
|
1034
|
+
}
|
|
1035
|
+
#goto(name, ctx) {
|
|
1036
|
+
const SceneType = this.#config.scenes[name];
|
|
1037
|
+
if (SceneType === void 0) throw new Error(`Unknown scene '${name}'.`);
|
|
1038
|
+
this.#sceneFrame = void 0;
|
|
1039
|
+
this.#scene?.exit(ctx);
|
|
1040
|
+
this.#sceneEntered = false;
|
|
1041
|
+
this.#scheduler?.clear();
|
|
1042
|
+
this.#entities?.clear();
|
|
1043
|
+
for (const plugin of this.#config.plugins ?? []) {
|
|
1044
|
+
if (typeof plugin !== "function") plugin.sceneExit?.(ctx);
|
|
1045
|
+
}
|
|
1046
|
+
clearScene(ctx.scene, this.#particles);
|
|
1047
|
+
const scene = new SceneType();
|
|
1048
|
+
this.#scene = scene;
|
|
1049
|
+
const loaded = scene.load(ctx);
|
|
1050
|
+
if (loaded === void 0) {
|
|
1051
|
+
this.#enterScene(scene, ctx);
|
|
1052
|
+
return Promise.resolve();
|
|
1053
|
+
}
|
|
1054
|
+
return Promise.resolve(loaded).then(() => this.#enterScene(scene, ctx));
|
|
1055
|
+
}
|
|
1056
|
+
#enterScene(scene, ctx) {
|
|
1057
|
+
const frame = scene.enter(ctx);
|
|
1058
|
+
if (frame !== void 0 && typeof frame !== "function") {
|
|
1059
|
+
throw new Error("Scene.enter() must return a frame function or undefined.");
|
|
1060
|
+
}
|
|
1061
|
+
if (this.#scene !== scene) return;
|
|
1062
|
+
this.#sceneFrame = typeof frame === "function" ? frame : void 0;
|
|
1063
|
+
this.#sceneEntered = true;
|
|
1064
|
+
}
|
|
1065
|
+
start() {
|
|
1066
|
+
if (this.#started) return Promise.resolve();
|
|
1067
|
+
if (this.#pendingStart !== void 0) return this.#pendingStart;
|
|
1068
|
+
this.#aborted = false;
|
|
1069
|
+
const pendingStart = this.#boot();
|
|
1070
|
+
this.#pendingStart = pendingStart;
|
|
1071
|
+
void pendingStart.then(
|
|
1072
|
+
() => {
|
|
1073
|
+
if (this.#pendingStart === pendingStart) this.#pendingStart = void 0;
|
|
1074
|
+
},
|
|
1075
|
+
() => {
|
|
1076
|
+
if (this.#pendingStart === pendingStart) this.#pendingStart = void 0;
|
|
1077
|
+
}
|
|
1078
|
+
);
|
|
1079
|
+
return pendingStart;
|
|
1080
|
+
}
|
|
1081
|
+
async #boot() {
|
|
1082
|
+
const SceneType = this.#config.scenes[this.#config.start];
|
|
1083
|
+
if (SceneType === void 0) throw new Error(`Unknown start scene '${this.#config.start}'.`);
|
|
1084
|
+
const renderer = await createRenderer({
|
|
1085
|
+
...this.#config.renderer,
|
|
1086
|
+
canvas: this.#config.canvas ?? this.#config.renderer?.canvas,
|
|
1087
|
+
preferWebGPU: this.#config.render?.preferWebGPU ?? this.#config.renderer?.preferWebGPU,
|
|
1088
|
+
source: this.#config.renderer?.source ?? this.#config.platform?.renderer
|
|
1089
|
+
});
|
|
1090
|
+
if (this.#aborted) {
|
|
1091
|
+
renderer.dispose();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
this.#renderer = renderer;
|
|
1095
|
+
const canvas = renderer.domElement;
|
|
1096
|
+
const platform = this.#config.platform;
|
|
1097
|
+
if (platform !== void 0) platform.mountCanvas(canvas, this.#config.container);
|
|
1098
|
+
else if (this.#config.container !== void 0 && canvas.parentElement !== this.#config.container)
|
|
1099
|
+
this.#config.container.append(canvas);
|
|
1100
|
+
else if (canvas.parentElement === null && typeof document !== "undefined")
|
|
1101
|
+
document.body.append(canvas);
|
|
1102
|
+
const inputTarget = this.#config.inputTarget ?? (platform === void 0 ? typeof window === "undefined" ? canvas : window : platform.inputTarget ?? canvas);
|
|
1103
|
+
this.#input = new InputMap(this.#config.input, inputTarget, canvas, platform?.input);
|
|
1104
|
+
this.#state.start();
|
|
1105
|
+
const threeScene = new Scene$1();
|
|
1106
|
+
const camera = createCamera(this.#config.camera);
|
|
1107
|
+
const viewport = new Viewport({ camera, renderer: this.#renderer, source: platform?.viewport });
|
|
1108
|
+
this.#viewport = viewport;
|
|
1109
|
+
const assets = createAssetLoader(this.#config.assets);
|
|
1110
|
+
const entities = new Registry();
|
|
1111
|
+
const random = createRandom(this.#config.seed);
|
|
1112
|
+
const scheduler = new Scheduler();
|
|
1113
|
+
const loopState = {};
|
|
1114
|
+
const ctx = {
|
|
1115
|
+
add: (object) => {
|
|
1116
|
+
threeScene.add(object);
|
|
1117
|
+
if (object instanceof GPUParticles3D) {
|
|
1118
|
+
const activeRenderer = this.#renderer;
|
|
1119
|
+
if (activeRenderer === void 0)
|
|
1120
|
+
throw new Error("Cannot add particles before the game starts.");
|
|
1121
|
+
object.attachRenderer(activeRenderer);
|
|
1122
|
+
this.#particles.add(object);
|
|
1123
|
+
}
|
|
1124
|
+
return object;
|
|
1125
|
+
},
|
|
1126
|
+
assets,
|
|
1127
|
+
after: (delay, callback) => scheduler.after(delay, callback),
|
|
1128
|
+
camera,
|
|
1129
|
+
entities,
|
|
1130
|
+
every: (callback) => scheduler.every(callback),
|
|
1131
|
+
get fps() {
|
|
1132
|
+
return loopState.current?.fps ?? 0;
|
|
1133
|
+
},
|
|
1134
|
+
goto: (name) => this.#goto(name, ctx),
|
|
1135
|
+
input: this.#input,
|
|
1136
|
+
physics: void 0,
|
|
1137
|
+
random,
|
|
1138
|
+
renderer: this.#renderer,
|
|
1139
|
+
viewport,
|
|
1140
|
+
scene: threeScene,
|
|
1141
|
+
state: this.#state,
|
|
1142
|
+
tween: (target, properties, duration) => scheduler.tween(target, properties, duration)
|
|
1143
|
+
};
|
|
1144
|
+
this.#ctx = ctx;
|
|
1145
|
+
this.#entities = entities;
|
|
1146
|
+
this.#random = random;
|
|
1147
|
+
this.#scheduler = scheduler;
|
|
1148
|
+
const devToolsHost = platform === void 0 ? typeof window === "undefined" ? void 0 : window : platform.devToolsHost;
|
|
1149
|
+
this.#cleanup.push(installDevTools(entities, devToolsHost));
|
|
1150
|
+
this.#scene = new SceneType();
|
|
1151
|
+
const gameLoop = new FixedStepLoop({
|
|
1152
|
+
maxSteps: this.#config.maxSteps,
|
|
1153
|
+
onRender: () => {
|
|
1154
|
+
for (const particle of this.#particles) {
|
|
1155
|
+
if (particle.released || particle.parent === null) {
|
|
1156
|
+
particle.detach();
|
|
1157
|
+
this.#particles.delete(particle);
|
|
1158
|
+
} else {
|
|
1159
|
+
particle.process(this.#renderer);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
renderer.render(threeScene, camera);
|
|
1163
|
+
this.#scene?.render(ctx);
|
|
1164
|
+
},
|
|
1165
|
+
onUpdate: (dt) => {
|
|
1166
|
+
if (this.#paused) return;
|
|
1167
|
+
this.#input?.tick();
|
|
1168
|
+
scheduler.tick(dt);
|
|
1169
|
+
for (const plugin of this.#activePlugins) plugin.beforeUpdate?.(ctx, dt);
|
|
1170
|
+
const scene2 = this.#scene;
|
|
1171
|
+
const frame = this.#sceneFrame;
|
|
1172
|
+
if (frame !== void 0) frame(ctx, dt);
|
|
1173
|
+
else scene2?.update(ctx, dt);
|
|
1174
|
+
if (this.#scene !== scene2 || this.#sceneFrame !== frame) return;
|
|
1175
|
+
for (const plugin of this.#activePlugins) plugin.update?.(ctx, dt);
|
|
1176
|
+
},
|
|
1177
|
+
step: this.#config.step
|
|
1178
|
+
});
|
|
1179
|
+
loopState.current = gameLoop;
|
|
1180
|
+
this.#loop = gameLoop;
|
|
1181
|
+
const runtime = {
|
|
1182
|
+
fixedStep: (ticks) => gameLoop.advance(ticks),
|
|
1183
|
+
random,
|
|
1184
|
+
rapier: null,
|
|
1185
|
+
seed: this.#config.seed ?? null,
|
|
1186
|
+
step: gameLoop.step
|
|
1187
|
+
};
|
|
1188
|
+
for (const plugin of this.#config.plugins ?? []) {
|
|
1189
|
+
const cleanup = typeof plugin === "function" ? plugin(ctx) : await plugin.setup?.(ctx, runtime);
|
|
1190
|
+
if (typeof plugin !== "function") this.#activePlugins.push(plugin);
|
|
1191
|
+
if (this.#aborted) {
|
|
1192
|
+
if (cleanup !== void 0) this.#cleanup.push(cleanup);
|
|
1193
|
+
this.#teardown(ctx);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (cleanup !== void 0) this.#cleanup.push(cleanup);
|
|
1197
|
+
}
|
|
1198
|
+
const scene = this.#scene;
|
|
1199
|
+
if (scene === void 0) {
|
|
1200
|
+
this.#teardown(ctx);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
await scene.load(ctx);
|
|
1204
|
+
if (this.#aborted) {
|
|
1205
|
+
this.#teardown(ctx);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
this.#enterScene(scene, ctx);
|
|
1209
|
+
if (this.#aborted) {
|
|
1210
|
+
this.#teardown(ctx);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
this.#started = true;
|
|
1214
|
+
gameLoop.start();
|
|
1215
|
+
}
|
|
1216
|
+
pause() {
|
|
1217
|
+
this.#paused = true;
|
|
1218
|
+
}
|
|
1219
|
+
resume() {
|
|
1220
|
+
this.#paused = false;
|
|
1221
|
+
}
|
|
1222
|
+
stop() {
|
|
1223
|
+
this.#aborted = true;
|
|
1224
|
+
this.#teardown();
|
|
1225
|
+
}
|
|
1226
|
+
#disposePlugin(plugin, ctx) {
|
|
1227
|
+
if (this.#disposedPlugins.has(plugin)) return;
|
|
1228
|
+
this.#disposedPlugins.add(plugin);
|
|
1229
|
+
plugin.dispose?.(ctx);
|
|
1230
|
+
}
|
|
1231
|
+
#teardown(startingCtx) {
|
|
1232
|
+
const ctx = this.#ctx ?? startingCtx;
|
|
1233
|
+
this.#loop?.stop();
|
|
1234
|
+
if (this.#sceneEntered && ctx !== void 0) this.#scene?.exit(ctx);
|
|
1235
|
+
this.#sceneFrame = void 0;
|
|
1236
|
+
this.#sceneEntered = false;
|
|
1237
|
+
this.#scheduler?.clear();
|
|
1238
|
+
this.#entities?.clear();
|
|
1239
|
+
this.#entities = void 0;
|
|
1240
|
+
if (ctx !== void 0)
|
|
1241
|
+
for (const plugin of this.#activePlugins) this.#disposePlugin(plugin, ctx);
|
|
1242
|
+
this.#activePlugins = [];
|
|
1243
|
+
for (const cleanup of this.#cleanup.splice(0)) cleanup();
|
|
1244
|
+
if (ctx !== void 0) clearScene(ctx.scene, this.#particles);
|
|
1245
|
+
this.#input?.dispose();
|
|
1246
|
+
this.#state.stop();
|
|
1247
|
+
this.#viewport?.dispose();
|
|
1248
|
+
const renderer = this.#renderer;
|
|
1249
|
+
renderer?.dispose();
|
|
1250
|
+
if (renderer !== void 0) {
|
|
1251
|
+
if (this.#config.platform === void 0) renderer.domElement.remove?.();
|
|
1252
|
+
else this.#config.platform.unmountCanvas(renderer.domElement);
|
|
1253
|
+
}
|
|
1254
|
+
this.#renderer = void 0;
|
|
1255
|
+
this.#viewport = void 0;
|
|
1256
|
+
this.#input = void 0;
|
|
1257
|
+
this.#scene = void 0;
|
|
1258
|
+
this.#ctx = void 0;
|
|
1259
|
+
this.#loop = void 0;
|
|
1260
|
+
this.#random = void 0;
|
|
1261
|
+
this.#scheduler = void 0;
|
|
1262
|
+
this.#disposedPlugins.clear();
|
|
1263
|
+
this.#paused = false;
|
|
1264
|
+
this.#started = false;
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
function defineGame(config) {
|
|
1268
|
+
return new GameImpl(config);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// ../playtest/dist/index.js
|
|
1272
|
+
var PLAYTEST_CAPABILITY_REGISTRY = [
|
|
1273
|
+
capability("browser.canvas", "Samples canvas pixels and health."),
|
|
1274
|
+
capability("browser.console", "Captures browser console and page errors."),
|
|
1275
|
+
capability("browser.dom", "Reads bounded DOM observations."),
|
|
1276
|
+
capability("browser.input", "Delivers keyboard and pointer input."),
|
|
1277
|
+
capability("browser.network", "Captures failed browser requests."),
|
|
1278
|
+
capability("browser.screenshot", "Captures viewport screenshots."),
|
|
1279
|
+
capability("browser.trace", "Captures a bounded Playwright trace."),
|
|
1280
|
+
capability("camera.observe", "Samples the active camera transform and projection."),
|
|
1281
|
+
capability("entity.bounds", "Projects registered entity bounds into the viewport."),
|
|
1282
|
+
capability("entity.observe", "Samples registered entity transforms and visibility."),
|
|
1283
|
+
capability("entity.setup", "Applies bounded transforms to registered entities."),
|
|
1284
|
+
capability("runtime.animation", "Samples application-owned animation state."),
|
|
1285
|
+
capability("runtime.audio", "Samples application-owned audio runtime state."),
|
|
1286
|
+
capability("runtime.components", "Samples registered JSON-safe component state."),
|
|
1287
|
+
capability("runtime.contacts", "Samples bounded collision and trigger contacts."),
|
|
1288
|
+
capability("runtime.diagnostics", "Samples application-owned runtime diagnostics."),
|
|
1289
|
+
capability("runtime.events", "Drains bounded application event observations."),
|
|
1290
|
+
capability("runtime.fixedStep", "Advances an application-owned deterministic tick."),
|
|
1291
|
+
capability("runtime.physics", "Samples bounded application-owned physics observations."),
|
|
1292
|
+
capability("runtime.resources", "Reads and writes registered JSON-safe application state."),
|
|
1293
|
+
capability("runtime.state", "Samples application-owned state-machine state."),
|
|
1294
|
+
capability("runtime.tags", "Samples bounded application-owned entity tags."),
|
|
1295
|
+
capability("runtime.ui", "Samples registered JSON-safe UI and HUD state."),
|
|
1296
|
+
capability("runtime.world", "Samples bounded runtime world metadata.")
|
|
1297
|
+
];
|
|
1298
|
+
new Set(PLAYTEST_CAPABILITY_REGISTRY.map(({ name }) => name));
|
|
1299
|
+
function capability(name, description) {
|
|
1300
|
+
return { description, name, protocolVersion: 1 };
|
|
1301
|
+
}
|
|
1302
|
+
function fail(message, code = "TN_REPLAY_INVALID") {
|
|
1303
|
+
throw new Error(`${code}: ${message}`);
|
|
1304
|
+
}
|
|
1305
|
+
function checked(value, allowed) {
|
|
1306
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) fail("bad object");
|
|
1307
|
+
const result = value;
|
|
1308
|
+
const unknown = Object.keys(result).find((key) => !allowed.includes(key));
|
|
1309
|
+
if (unknown !== void 0) fail(`unknown key '${unknown}'`);
|
|
1310
|
+
return result;
|
|
1311
|
+
}
|
|
1312
|
+
function validatePointer(value) {
|
|
1313
|
+
const pointer = Array.isArray(value) ? value : [];
|
|
1314
|
+
if (pointer.length !== 5 || !pointer.every(Number.isFinite) || !Number.isInteger(pointer[2]) || pointer[2] < 0 || Math.min(pointer[3], pointer[4]) <= 0)
|
|
1315
|
+
fail("pointer tuple is invalid");
|
|
1316
|
+
}
|
|
1317
|
+
function parseReplayRecording(value) {
|
|
1318
|
+
const root = checked(value, ["input", "randomState", "runtime", "seed", "ticks", "version"]);
|
|
1319
|
+
if (root.version !== 1 || !Number.isFinite(root.seed) || !Number.isInteger(root.randomState))
|
|
1320
|
+
fail("bad header");
|
|
1321
|
+
const ticks = root.ticks;
|
|
1322
|
+
if (!Number.isInteger(ticks) || ticks < 1) fail("ticks must be positive");
|
|
1323
|
+
const input2 = root.input;
|
|
1324
|
+
if (!Array.isArray(input2) || input2.length === 0) fail("empty input", "TN_REPLAY_EMPTY");
|
|
1325
|
+
const runtime = checked(root.runtime, ["agent", "core", "rapier", "step"]);
|
|
1326
|
+
const step = runtime.step;
|
|
1327
|
+
if (typeof runtime.agent !== "string" || !runtime.agent || typeof runtime.core !== "string" || !runtime.core || runtime.rapier !== null && typeof runtime.rapier !== "string" || !Number.isFinite(step) || step <= 0)
|
|
1328
|
+
fail("runtime fingerprint is invalid");
|
|
1329
|
+
let previousTick = -1;
|
|
1330
|
+
for (const rawSample of input2) {
|
|
1331
|
+
const sample = checked(rawSample, ["keys", "pointer", "tick"]);
|
|
1332
|
+
const tick = sample.tick;
|
|
1333
|
+
if (!Number.isInteger(tick) || tick <= previousTick || tick >= ticks) fail("bad tick");
|
|
1334
|
+
const keys = sample.keys;
|
|
1335
|
+
if (!Array.isArray(keys) || keys.some((key) => typeof key !== "string")) fail("bad keys");
|
|
1336
|
+
if (new Set(keys).size !== keys.length) fail("duplicate keys");
|
|
1337
|
+
if (sample.pointer !== void 0) validatePointer(sample.pointer);
|
|
1338
|
+
previousTick = tick;
|
|
1339
|
+
}
|
|
1340
|
+
return root;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/replay.ts
|
|
1344
|
+
var CORE_VERSION = "0.1.0";
|
|
1345
|
+
var currentAgent = typeof navigator === "undefined" ? "node" : navigator.userAgent;
|
|
1346
|
+
var replayDepth = 0;
|
|
1347
|
+
function fail2(message, code = "TN_REPLAY_INVALID") {
|
|
1348
|
+
throw new Error(`${code}: ${message}`);
|
|
1349
|
+
}
|
|
1350
|
+
var event = (type, init) => Object.assign(new Event(type), init);
|
|
1351
|
+
var pointerEvent = (type, [clientX, clientY, buttons]) => event(type, { buttons, clientX, clientY, pointerId: 0 });
|
|
1352
|
+
function pointerViewport(ctx, point = [0, 0, 0]) {
|
|
1353
|
+
const canvas = ctx.renderer?.domElement;
|
|
1354
|
+
const rect = canvas?.getBoundingClientRect();
|
|
1355
|
+
return [
|
|
1356
|
+
point[0] - (rect?.left ?? 0),
|
|
1357
|
+
point[1] - (rect?.top ?? 0),
|
|
1358
|
+
point[2],
|
|
1359
|
+
canvas?.clientWidth || globalThis.innerWidth || 1,
|
|
1360
|
+
canvas?.clientHeight || globalThis.innerHeight || 1
|
|
1361
|
+
];
|
|
1362
|
+
}
|
|
1363
|
+
var pointerType = (previous, next) => previous && !next ? "pointerup" : !previous && next ? "pointerdown" : "pointermove";
|
|
1364
|
+
function targetPointerPosition(pointer, target) {
|
|
1365
|
+
const viewport = target;
|
|
1366
|
+
const rect = viewport.getBoundingClientRect?.();
|
|
1367
|
+
const width = viewport.clientWidth || viewport.innerWidth || rect?.width || pointer[3];
|
|
1368
|
+
const height = viewport.clientHeight || viewport.innerHeight || rect?.height || pointer[4];
|
|
1369
|
+
return [
|
|
1370
|
+
pointer[0] * width / pointer[3] + (rect?.left ?? 0),
|
|
1371
|
+
pointer[1] * height / pointer[4] + (rect?.top ?? 0),
|
|
1372
|
+
pointer[2]
|
|
1373
|
+
];
|
|
1374
|
+
}
|
|
1375
|
+
function dispatchKeys(target, current, keys) {
|
|
1376
|
+
for (const key of current)
|
|
1377
|
+
if (!keys.includes(key)) target.dispatchEvent(event("keyup", { code: key }));
|
|
1378
|
+
for (const key of keys)
|
|
1379
|
+
if (!current.has(key)) target.dispatchEvent(event("keydown", { code: key }));
|
|
1380
|
+
current.clear();
|
|
1381
|
+
for (const key of keys) current.add(key);
|
|
1382
|
+
}
|
|
1383
|
+
function releasePointer(pointer, target) {
|
|
1384
|
+
if (pointer[2] === 0) return;
|
|
1385
|
+
const released = [pointer[0], pointer[1], 0, pointer[3], pointer[4]];
|
|
1386
|
+
target.dispatchEvent(pointerEvent("pointerup", targetPointerPosition(released, target)));
|
|
1387
|
+
}
|
|
1388
|
+
function dispatchPointer(previous, next, target) {
|
|
1389
|
+
if (previous.join() === next.join()) return previous;
|
|
1390
|
+
target.dispatchEvent(
|
|
1391
|
+
pointerEvent(pointerType(previous[2], next[2]), targetPointerPosition(next, target))
|
|
1392
|
+
);
|
|
1393
|
+
return next;
|
|
1394
|
+
}
|
|
1395
|
+
function replay() {
|
|
1396
|
+
const runId = /* @__PURE__ */ Symbol("replay");
|
|
1397
|
+
const samples = [];
|
|
1398
|
+
let header;
|
|
1399
|
+
let ticks = 0;
|
|
1400
|
+
let previousKeys = [];
|
|
1401
|
+
let previousPointer = [0, 0, 0, 1, 1];
|
|
1402
|
+
return {
|
|
1403
|
+
get recording() {
|
|
1404
|
+
if (header === void 0) return void 0;
|
|
1405
|
+
return {
|
|
1406
|
+
...header,
|
|
1407
|
+
input: [...samples],
|
|
1408
|
+
ticks
|
|
1409
|
+
};
|
|
1410
|
+
},
|
|
1411
|
+
runId,
|
|
1412
|
+
setup: (ctx, runtime) => {
|
|
1413
|
+
if (runtime?.seed === null || runtime?.seed === void 0)
|
|
1414
|
+
fail2("replay requires a seed", "TN_REPLAY_UNSEEDED");
|
|
1415
|
+
const { rapier, seed, step } = runtime;
|
|
1416
|
+
header = {
|
|
1417
|
+
randomState: ctx.random.state,
|
|
1418
|
+
runtime: { agent: currentAgent, core: CORE_VERSION, rapier: rapier ?? null, step },
|
|
1419
|
+
seed,
|
|
1420
|
+
version: 1
|
|
1421
|
+
};
|
|
1422
|
+
samples.length = ticks = 0;
|
|
1423
|
+
[previousKeys, previousPointer] = [[], pointerViewport(ctx)];
|
|
1424
|
+
return void 0;
|
|
1425
|
+
},
|
|
1426
|
+
beforeUpdate: (ctx) => {
|
|
1427
|
+
if (replayDepth > 0) return;
|
|
1428
|
+
const keys = [...ctx.input.raw.keys].sort();
|
|
1429
|
+
const { position, buttons } = ctx.input.raw.pointer;
|
|
1430
|
+
const pointer = pointerViewport(ctx, [position.x, position.y, buttons]);
|
|
1431
|
+
const pointerChanged = pointer.join() !== previousPointer.join();
|
|
1432
|
+
if (pointerChanged || keys.join() !== previousKeys.join()) {
|
|
1433
|
+
samples.push({ keys, ...pointerChanged ? { pointer } : {}, tick: ticks });
|
|
1434
|
+
[previousKeys, previousPointer] = [keys, pointer];
|
|
1435
|
+
}
|
|
1436
|
+
ticks += 1;
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function validateRuntime(runtime, recording) {
|
|
1441
|
+
const random = runtime.random;
|
|
1442
|
+
if (runtime.seed !== recording.seed || runtime.step !== recording.runtime.step || (runtime.rapier ?? null) !== (recording.runtime.rapier ?? null) || recording.runtime.core !== CORE_VERSION || recording.runtime.agent !== currentAgent || random === void 0)
|
|
1443
|
+
fail2("recording runtime does not match the current build", "TN_REPLAY_RUNTIME_MISMATCH");
|
|
1444
|
+
return random;
|
|
1445
|
+
}
|
|
1446
|
+
function restoreRandomState(random, state) {
|
|
1447
|
+
try {
|
|
1448
|
+
random.state = state;
|
|
1449
|
+
if (random.state !== state) throw new Error();
|
|
1450
|
+
} catch {
|
|
1451
|
+
fail2("runtime random state cannot be restored", "TN_REPLAY_RUNTIME_MISMATCH");
|
|
1452
|
+
}
|
|
1453
|
+
return random;
|
|
1454
|
+
}
|
|
1455
|
+
function createReplayDriver(recording, target, pointerTarget = target) {
|
|
1456
|
+
const value = parseReplayRecording(recording);
|
|
1457
|
+
const samples = new Map(value.input.map((sample) => [sample.tick, sample]));
|
|
1458
|
+
let preparedRandom;
|
|
1459
|
+
const driver = Object.assign(
|
|
1460
|
+
(runtime) => {
|
|
1461
|
+
const random = validateRuntime(runtime, value);
|
|
1462
|
+
if (preparedRandom !== random) restoreRandomState(random, value.randomState);
|
|
1463
|
+
preparedRandom = void 0;
|
|
1464
|
+
target.dispatchEvent(new Event("blur"));
|
|
1465
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1466
|
+
let pointer = [0, 0, 0, 1, 1];
|
|
1467
|
+
replayDepth += 1;
|
|
1468
|
+
try {
|
|
1469
|
+
for (let tick = 0; tick < value.ticks; tick += 1) {
|
|
1470
|
+
const sample = samples.get(tick);
|
|
1471
|
+
if (sample !== void 0) {
|
|
1472
|
+
dispatchKeys(target, keys, sample.keys);
|
|
1473
|
+
if (sample.pointer !== void 0)
|
|
1474
|
+
pointer = dispatchPointer(pointer, sample.pointer, pointerTarget);
|
|
1475
|
+
}
|
|
1476
|
+
runtime.fixedStep(1);
|
|
1477
|
+
}
|
|
1478
|
+
dispatchKeys(target, keys, []);
|
|
1479
|
+
releasePointer(pointer, pointerTarget);
|
|
1480
|
+
return value.ticks;
|
|
1481
|
+
} finally {
|
|
1482
|
+
replayDepth -= 1;
|
|
1483
|
+
}
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
prepare: (runtime) => {
|
|
1487
|
+
preparedRandom = restoreRandomState(validateRuntime(runtime, value), value.randomState);
|
|
1488
|
+
},
|
|
1489
|
+
runId: /* @__PURE__ */ Symbol("replay-driver")
|
|
1490
|
+
}
|
|
1491
|
+
);
|
|
1492
|
+
return driver;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// src/scene.ts
|
|
1496
|
+
var Scene = class {
|
|
1497
|
+
static initialState = void 0;
|
|
1498
|
+
load(_ctx) {
|
|
1499
|
+
}
|
|
1500
|
+
enter(_ctx) {
|
|
1501
|
+
return void 0;
|
|
1502
|
+
}
|
|
1503
|
+
exit(_ctx) {
|
|
1504
|
+
}
|
|
1505
|
+
update(_ctx, _dt) {
|
|
1506
|
+
}
|
|
1507
|
+
render(_ctx) {
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
// src/index.ts
|
|
1512
|
+
var version = "0.1.0";
|
|
1513
|
+
|
|
1514
|
+
export { AnimationPlayer, AudioBus, FixedStepLoop, GPUParticles3D, InputMap, Registry, Scene, Scheduler, Viewport, autoFields, createAssetLoader, createGameStore, createRandom, createRenderer, createReplayDriver, defineGame, input, replay, version };
|