@threenative/core 0.1.0 → 0.2.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 → audio-CEAw0w5y.d.ts} +9 -9
- package/dist/game-DRt1Qhq3.d.ts +429 -0
- package/dist/hot.d.ts +17 -5
- package/dist/index.d.ts +95 -39
- package/dist/index.js +1841 -157
- package/dist/playtest.d.ts +26 -4
- package/dist/playtest.js +161 -23
- package/package.json +3 -2
- package/dist/game-DRPs3M7r.d.ts +0 -269
package/dist/playtest.d.ts
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import { JsonValue } from '@threenative/playtest';
|
|
2
|
-
import {
|
|
2
|
+
import { c as IGamePluginHooks } from './game-DRt1Qhq3.js';
|
|
3
3
|
import 'three';
|
|
4
4
|
import 'zustand/vanilla';
|
|
5
5
|
|
|
6
|
-
declare function playtest<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined>(options?:
|
|
7
|
-
|
|
6
|
+
declare function playtest<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined>(options?: IPlaytestOptions): IGamePluginHooks<TState, TPhysics>;
|
|
7
|
+
declare const PLAYTEST_ATTACH_TIMEOUT_MS = 30000;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves when a runner first calls `describe()`, the handshake every runner performs before
|
|
10
|
+
* it observes anything.
|
|
11
|
+
*
|
|
12
|
+
* Without this, a scenario races the game: a proof that does finite work at startup can finish
|
|
13
|
+
* before the runner takes its first observation, and the assertion then reports
|
|
14
|
+
* TN_PLAYTEST_ASSERTION_TRIVIAL or a zero-delta failure depending only on how fast the device
|
|
15
|
+
* booted. Opt-in, because a game that holds for a runner that never arrives is worse than the
|
|
16
|
+
* race for every non-test caller.
|
|
17
|
+
*
|
|
18
|
+
* Fails closed: if no runner attaches within the timeout, setup throws rather than quietly
|
|
19
|
+
* starting anyway and reproducing the race it was added to remove.
|
|
20
|
+
*/
|
|
21
|
+
/** The global a playtest runner sets before the page loads, so a game can tell one is coming. */
|
|
22
|
+
declare const PLAYTEST_RUNNER_EXPECTED_GLOBAL = "__THREENATIVE_PLAYTEST_RUNNER_EXPECTED__";
|
|
23
|
+
interface IPlaytestOptions {
|
|
8
24
|
readonly events?: () => JsonValue[];
|
|
25
|
+
/**
|
|
26
|
+
* Hold the frame loop until a runner attaches. Default false.
|
|
27
|
+
*/
|
|
28
|
+
readonly holdUntilAttached?: boolean;
|
|
29
|
+
/** Milliseconds to wait when `holdUntilAttached` is set. Default {@link PLAYTEST_ATTACH_TIMEOUT_MS}. */
|
|
30
|
+
readonly attachTimeoutMs?: number;
|
|
9
31
|
}
|
|
10
32
|
|
|
11
|
-
export { type
|
|
33
|
+
export { type IPlaytestOptions, PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, playtest };
|
package/dist/playtest.js
CHANGED
|
@@ -17,10 +17,11 @@ var PLAYTEST_CAPABILITY_REGISTRY = [
|
|
|
17
17
|
capability("runtime.audio", "Samples application-owned audio runtime state."),
|
|
18
18
|
capability("runtime.components", "Samples registered JSON-safe component state."),
|
|
19
19
|
capability("runtime.contacts", "Samples bounded collision and trigger contacts."),
|
|
20
|
-
capability("runtime.diagnostics", "
|
|
20
|
+
capability("runtime.diagnostics", "Captures application runtime errors."),
|
|
21
21
|
capability("runtime.events", "Drains bounded application event observations."),
|
|
22
22
|
capability("runtime.fixedStep", "Advances an application-owned deterministic tick."),
|
|
23
23
|
capability("runtime.physics", "Samples bounded application-owned physics observations."),
|
|
24
|
+
capability("runtime.performance", "Samples bounded per-render frame cost and renderer counts."),
|
|
24
25
|
capability("runtime.resources", "Reads and writes registered JSON-safe application state."),
|
|
25
26
|
capability("runtime.state", "Samples application-owned state-machine state."),
|
|
26
27
|
capability("runtime.tags", "Samples bounded application-owned entity tags."),
|
|
@@ -129,6 +130,7 @@ function sampleThreeObservations(input, request) {
|
|
|
129
130
|
input.camera.updateMatrixWorld(true);
|
|
130
131
|
const rendererSize = input.renderer.getDrawingBufferSize(new Vector2());
|
|
131
132
|
const entities = input.registry.select(request.entities).map(({ id, object }) => observeEntity(id, object, input.camera, rendererSize.x, rendererSize.y));
|
|
133
|
+
const renderPerformance = rendererPerformance(input.renderer);
|
|
132
134
|
return {
|
|
133
135
|
clock: {
|
|
134
136
|
mode: input.clockMode,
|
|
@@ -137,9 +139,24 @@ function sampleThreeObservations(input, request) {
|
|
|
137
139
|
...input.diagnostics === void 0 ? {} : { diagnostics: input.diagnostics() },
|
|
138
140
|
entities,
|
|
139
141
|
...input.gameplay === void 0 ? {} : { gameplay: input.gameplay() },
|
|
142
|
+
...renderPerformance === void 0 ? {} : { performance: renderPerformance },
|
|
143
|
+
...input.runtimeDiagnosticsSeries === void 0 ? {} : { runtimeDiagnosticsSeries: input.runtimeDiagnosticsSeries().map((sample) => ({ ...sample })) },
|
|
140
144
|
...input.resources === void 0 ? {} : { resources: input.resources() }
|
|
141
145
|
};
|
|
142
146
|
}
|
|
147
|
+
function rendererPerformance(renderer) {
|
|
148
|
+
const render = renderer.info?.render;
|
|
149
|
+
const drawCalls = finiteNumber(render?.drawCalls) ? render.drawCalls : void 0;
|
|
150
|
+
const triangles = finiteNumber(render?.triangles) ? render.triangles : void 0;
|
|
151
|
+
if (drawCalls === void 0 && triangles === void 0) return void 0;
|
|
152
|
+
return {
|
|
153
|
+
...drawCalls === void 0 ? {} : { drawCalls },
|
|
154
|
+
...triangles === void 0 ? {} : { triangles }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function finiteNumber(value) {
|
|
158
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
159
|
+
}
|
|
143
160
|
function observeEntity(id, object, camera, viewportWidth, viewportHeight) {
|
|
144
161
|
const position = object.getWorldPosition(new Vector3());
|
|
145
162
|
const bounds = projectedBounds(object, camera, viewportWidth, viewportHeight);
|
|
@@ -182,6 +199,49 @@ function projectedBounds(object, camera, viewportWidth, viewportHeight) {
|
|
|
182
199
|
y: (1 - maxY) * 0.5 * viewportHeight
|
|
183
200
|
};
|
|
184
201
|
}
|
|
202
|
+
var PLAYTEST_PHYSICS_BODY_LIMIT = 100;
|
|
203
|
+
var PLAYTEST_PHYSICS_SAMPLE_LIMIT = 100;
|
|
204
|
+
var ThreePlaytestPhysicsRecorder = class {
|
|
205
|
+
#physics;
|
|
206
|
+
#series = [];
|
|
207
|
+
constructor(physics) {
|
|
208
|
+
this.#physics = physics;
|
|
209
|
+
}
|
|
210
|
+
sample(request, tick) {
|
|
211
|
+
if (request.label !== void 0) {
|
|
212
|
+
if (this.#series.some(({ label }) => label === request.label))
|
|
213
|
+
throw new Error(`TN_PLAYTEST_PHYSICS_LABEL_DUPLICATE: '${request.label}' was already sampled.`);
|
|
214
|
+
if (this.#series.length >= PLAYTEST_PHYSICS_SAMPLE_LIMIT)
|
|
215
|
+
throw new Error(`TN_PLAYTEST_PHYSICS_SAMPLE_LIMIT: at most ${PLAYTEST_PHYSICS_SAMPLE_LIMIT} labelled samples are retained.`);
|
|
216
|
+
this.#series.push({ label: request.label, snapshot: physicsSnapshot(this.#physics.bodies()), tick });
|
|
217
|
+
}
|
|
218
|
+
return this.#series.map((sample) => ({ ...sample }));
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function physicsSnapshot(bodies) {
|
|
222
|
+
if (!Array.isArray(bodies))
|
|
223
|
+
throw new Error("TN_PLAYTEST_PHYSICS_BODIES: the physics provider must return an array of bodies.");
|
|
224
|
+
const retained = bodies.slice(0, PLAYTEST_PHYSICS_BODY_LIMIT);
|
|
225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
226
|
+
const primitives = retained.flatMap((body) => {
|
|
227
|
+
if (typeof body?.id !== "string" || body.id.length === 0)
|
|
228
|
+
throw new Error("TN_PLAYTEST_PHYSICS_BODY_ID: every observed physics body needs a non-empty string id.");
|
|
229
|
+
if (seen.has(body.id))
|
|
230
|
+
throw new Error(`TN_PLAYTEST_PHYSICS_BODY_DUPLICATE: body id '${body.id}' was reported twice in one sample.`);
|
|
231
|
+
seen.add(body.id);
|
|
232
|
+
if (typeof body.sleeping !== "boolean")
|
|
233
|
+
throw new Error(`TN_PLAYTEST_PHYSICS_BODY_SLEEPING: body '${body.id}' must report sleeping as a boolean.`);
|
|
234
|
+
if (!isFiniteVec3(body.position))
|
|
235
|
+
throw new Error(`TN_PLAYTEST_PHYSICS_BODY_POSITION: body '${body.id}' must report a finite [x, y, z] position.`);
|
|
236
|
+
const sleep = { category: "sleep", entity: body.id, value: body.sleeping ? 1 : 0 };
|
|
237
|
+
const centerOfMass = { category: "center-of-mass", entity: body.id, position: [...body.position] };
|
|
238
|
+
return [sleep, centerOfMass];
|
|
239
|
+
});
|
|
240
|
+
return { artifact: { overflow: { omittedBodies: bodies.length - retained.length }, primitives } };
|
|
241
|
+
}
|
|
242
|
+
function isFiniteVec3(value) {
|
|
243
|
+
return Array.isArray(value) && value.length === 3 && value.every((component) => typeof component === "number" && Number.isFinite(component));
|
|
244
|
+
}
|
|
185
245
|
var BRIDGE_METHODS = /* @__PURE__ */ new Set([
|
|
186
246
|
"advance",
|
|
187
247
|
"applySetup",
|
|
@@ -339,11 +399,15 @@ function isRecord(value) {
|
|
|
339
399
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
340
400
|
}
|
|
341
401
|
function installThreePlaytestBridge(options) {
|
|
402
|
+
if (options.fixedStep === void 0 !== (options.tick === void 0))
|
|
403
|
+
throw new Error("fixedStep and its authoritative tick provider must be installed together.");
|
|
404
|
+
if (options.physics !== void 0 && options.tick === void 0)
|
|
405
|
+
throw new Error("A physics provider requires the authoritative tick provider, and therefore fixedStep.");
|
|
406
|
+
const recorder = options.physics === void 0 ? void 0 : new ThreePlaytestPhysicsRecorder(options.physics);
|
|
342
407
|
const host = globalThis;
|
|
343
408
|
const previous = host[PLAYTEST_BRIDGE_GLOBAL];
|
|
344
409
|
const registry = new ThreePlaytestEntityRegistry();
|
|
345
410
|
syncEntities(registry, options.entities);
|
|
346
|
-
let tick = 0;
|
|
347
411
|
const capabilities = () => [
|
|
348
412
|
"camera.observe",
|
|
349
413
|
"entity.bounds",
|
|
@@ -355,17 +419,23 @@ function installThreePlaytestBridge(options) {
|
|
|
355
419
|
...Object.keys(options.components?.() ?? {}).length === 0 ? [] : ["runtime.components"],
|
|
356
420
|
...options.events === void 0 ? [] : ["runtime.events"],
|
|
357
421
|
...options.gameplay === void 0 ? [] : ["runtime.animation", "runtime.state"],
|
|
422
|
+
...options.physics === void 0 ? [] : ["runtime.physics"],
|
|
423
|
+
...options.runtimeDiagnosticsSeries === void 0 ? [] : ["runtime.performance"],
|
|
358
424
|
...options.gameplayChannels?.().includes("runtime.contacts") === true ? ["runtime.contacts"] : [],
|
|
359
|
-
...options.gameplayChannels?.().includes("runtime.tags") === true ? ["runtime.tags"] : []
|
|
425
|
+
...options.gameplayChannels?.().includes("runtime.tags") === true ? ["runtime.tags"] : [],
|
|
426
|
+
...options.gameplayChannels?.().includes("runtime.world") === true ? ["runtime.world"] : []
|
|
360
427
|
];
|
|
361
428
|
const bridge = {
|
|
362
429
|
...options.fixedStep === void 0 ? {} : {
|
|
363
430
|
advance: async (ticks) => {
|
|
364
431
|
if (!Number.isInteger(ticks) || ticks <= 0) throw new Error("advance ticks must be a positive integer.");
|
|
432
|
+
const startTick = options.tick();
|
|
365
433
|
await options.fixedStep(ticks);
|
|
366
|
-
const
|
|
367
|
-
tick
|
|
368
|
-
|
|
434
|
+
const tick = options.tick();
|
|
435
|
+
const advanced = tick - startTick;
|
|
436
|
+
if (advanced !== ticks)
|
|
437
|
+
throw new Error(`fixedStep advanced ${advanced} actual ticks; expected ${ticks}.`);
|
|
438
|
+
return { clock: { mode: "fixed-step", tick }, ticks: advanced };
|
|
369
439
|
}
|
|
370
440
|
},
|
|
371
441
|
applySetup: async (request) => applySetup(registry, options.resources, request),
|
|
@@ -391,14 +461,16 @@ function installThreePlaytestBridge(options) {
|
|
|
391
461
|
clockMode: options.fixedStep === void 0 ? "render-frame" : "fixed-step",
|
|
392
462
|
diagnostics: options.diagnostics,
|
|
393
463
|
gameplay: options.gameplay,
|
|
464
|
+
runtimeDiagnosticsSeries: options.runtimeDiagnosticsSeries,
|
|
394
465
|
registry,
|
|
395
466
|
renderer: options.renderer,
|
|
396
467
|
resources: options.resources === void 0 ? void 0 : () => options.resources.read(),
|
|
397
468
|
scene: options.scene,
|
|
398
|
-
tick: options.
|
|
469
|
+
tick: options.tick?.()
|
|
399
470
|
}, request);
|
|
400
471
|
const components = options.components?.();
|
|
401
|
-
const
|
|
472
|
+
const withComponents = components === void 0 || Object.keys(components).length === 0 ? snapshot : { ...snapshot, components };
|
|
473
|
+
const result = recorder === void 0 || request.include?.includes("physicsDebugSeries") !== true ? withComponents : { ...withComponents, physicsDebugSeries: recorder.sample(request, options.tick()) };
|
|
402
474
|
assertJsonSafe2(result);
|
|
403
475
|
return result;
|
|
404
476
|
}
|
|
@@ -454,10 +526,10 @@ var CORE_VERSION = "0.1.0";
|
|
|
454
526
|
var currentAgent = typeof navigator === "undefined" ? "node" : navigator.userAgent;
|
|
455
527
|
function playtest(options = {}) {
|
|
456
528
|
let dispose;
|
|
529
|
+
let attached;
|
|
457
530
|
let contactHistory = [];
|
|
458
531
|
return {
|
|
459
|
-
setup: (ctx, runtime) => {
|
|
460
|
-
const diagnostics = [];
|
|
532
|
+
setup: async (ctx, runtime) => {
|
|
461
533
|
const seed = runtime?.seed ?? null;
|
|
462
534
|
const replayRuntime = runtime?.seed === null || runtime?.seed === void 0 ? void 0 : {
|
|
463
535
|
agent: currentAgent,
|
|
@@ -468,10 +540,10 @@ function playtest(options = {}) {
|
|
|
468
540
|
};
|
|
469
541
|
const installation = installThreePlaytestBridge({
|
|
470
542
|
camera: ctx.camera,
|
|
471
|
-
diagnostics: () => [...diagnostics],
|
|
472
543
|
entities: () => bridgeEntities(ctx),
|
|
473
544
|
components: () => componentObservations(ctx.entities.snapshot()),
|
|
474
|
-
...runtime === void 0 ? {} : { fixedStep:
|
|
545
|
+
...runtime === void 0 ? {} : { fixedStep: runtime.fixedStep, tick: runtime.tick },
|
|
546
|
+
...runtime?.runtimeDiagnosticsSeries === void 0 ? {} : { runtimeDiagnosticsSeries: runtime.runtimeDiagnosticsSeries },
|
|
475
547
|
...options.events === void 0 ? {} : { events: options.events },
|
|
476
548
|
gameplay: () => gameplayObservations(ctx, contactHistory, seed, replayRuntime),
|
|
477
549
|
gameplayChannels: () => gameplayChannels(),
|
|
@@ -479,27 +551,93 @@ function playtest(options = {}) {
|
|
|
479
551
|
resources: stateResources(ctx),
|
|
480
552
|
scene: ctx.scene
|
|
481
553
|
});
|
|
482
|
-
installRuntimeChannels(installation.bridge);
|
|
554
|
+
installRuntimeChannels(installation.bridge, runtime);
|
|
483
555
|
dispose = installation.dispose;
|
|
484
|
-
|
|
556
|
+
attached = holdUntilAttached(installation.bridge, options);
|
|
557
|
+
const cleanup = () => {
|
|
485
558
|
dispose?.();
|
|
486
559
|
dispose = void 0;
|
|
560
|
+
attached = void 0;
|
|
487
561
|
contactHistory = [];
|
|
488
562
|
};
|
|
563
|
+
if (attached !== void 0) {
|
|
564
|
+
const gate = attached.catch((error) => {
|
|
565
|
+
cleanup();
|
|
566
|
+
throw error;
|
|
567
|
+
});
|
|
568
|
+
if (runtime?.holdStart === void 0) await gate;
|
|
569
|
+
else runtime.holdStart(gate);
|
|
570
|
+
}
|
|
571
|
+
return cleanup;
|
|
489
572
|
}
|
|
490
573
|
};
|
|
491
574
|
}
|
|
492
|
-
|
|
575
|
+
var PLAYTEST_ATTACH_TIMEOUT_MS = 3e4;
|
|
576
|
+
var PLAYTEST_RUNNER_EXPECTED_GLOBAL = "__THREENATIVE_PLAYTEST_RUNNER_EXPECTED__";
|
|
577
|
+
function shouldHoldUntilAttached(options) {
|
|
578
|
+
if (options.holdUntilAttached !== void 0) return options.holdUntilAttached;
|
|
579
|
+
return globalThis[PLAYTEST_RUNNER_EXPECTED_GLOBAL] === true;
|
|
580
|
+
}
|
|
581
|
+
function holdUntilAttached(bridge, options) {
|
|
582
|
+
if (!shouldHoldUntilAttached(options)) return void 0;
|
|
583
|
+
const timeoutMs = options.attachTimeoutMs ?? PLAYTEST_ATTACH_TIMEOUT_MS;
|
|
584
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
585
|
+
throw new Error(`TN_PLAYTEST_ATTACH_TIMEOUT_INVALID: ${String(options.attachTimeoutMs)}`);
|
|
586
|
+
}
|
|
493
587
|
const describe = bridge.describe;
|
|
494
|
-
|
|
588
|
+
return new Promise((resolve, reject) => {
|
|
589
|
+
const timer = setTimeout(() => {
|
|
590
|
+
reject(
|
|
591
|
+
new Error(
|
|
592
|
+
`TN_PLAYTEST_ATTACH_TIMEOUT: no playtest runner called describe() within ${timeoutMs}ms.`
|
|
593
|
+
)
|
|
594
|
+
);
|
|
595
|
+
}, timeoutMs);
|
|
596
|
+
timer.unref?.();
|
|
597
|
+
bridge.describe = () => {
|
|
598
|
+
clearTimeout(timer);
|
|
599
|
+
resolve();
|
|
600
|
+
return describe();
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function installRuntimeChannels(bridge, runtime) {
|
|
605
|
+
const describe = bridge.describe;
|
|
606
|
+
bridge.describe = () => Promise.resolve(describe()).then(
|
|
607
|
+
(description) => addRuntimeCapabilities(description, runtime?.observations.contributions() ?? [])
|
|
608
|
+
);
|
|
609
|
+
const sample = bridge.sample;
|
|
610
|
+
bridge.sample = (request) => Promise.resolve(sample(request)).then(
|
|
611
|
+
(snapshot) => addRuntimeObservations(snapshot, request, runtime?.observations.contributions() ?? [])
|
|
612
|
+
);
|
|
495
613
|
}
|
|
496
|
-
function addRuntimeCapabilities(description) {
|
|
614
|
+
function addRuntimeCapabilities(description, contributions) {
|
|
497
615
|
const capabilities = [...description.capabilities];
|
|
498
|
-
|
|
616
|
+
const contributed = contributions.flatMap(({ capabilities: capabilities2 }) => capabilities2);
|
|
617
|
+
for (const capability2 of ["runtime.audio", "runtime.world", ...contributed]) {
|
|
499
618
|
if (!capabilities.includes(capability2)) capabilities.push(capability2);
|
|
500
619
|
}
|
|
501
620
|
return { ...description, capabilities };
|
|
502
621
|
}
|
|
622
|
+
function addRuntimeObservations(snapshot, request, contributions) {
|
|
623
|
+
const result = { ...snapshot };
|
|
624
|
+
for (const contribution of contributions) {
|
|
625
|
+
const slice = contribution.sample(request);
|
|
626
|
+
if (typeof slice !== "object" || slice === null || Array.isArray(slice)) {
|
|
627
|
+
throw new TypeError("A runtime observation contribution must return a top-level object.");
|
|
628
|
+
}
|
|
629
|
+
assertJsonSafe(slice, "$.runtimeContribution");
|
|
630
|
+
for (const [key, value] of Object.entries(slice)) {
|
|
631
|
+
if (Object.hasOwn(result, key)) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`TN_PLAYTEST_OBSERVATION_COLLISION: top-level key '${key}' already exists.`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
result[key] = value;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
503
641
|
function runtimeObservation(seed, replayRuntime) {
|
|
504
642
|
return {
|
|
505
643
|
audio: audioRuntimeSnapshot(),
|
|
@@ -509,9 +647,6 @@ function runtimeObservation(seed, replayRuntime) {
|
|
|
509
647
|
}
|
|
510
648
|
};
|
|
511
649
|
}
|
|
512
|
-
function advance(runtime, ticks) {
|
|
513
|
-
runtime.fixedStep(ticks);
|
|
514
|
-
}
|
|
515
650
|
function bridgeEntities(ctx) {
|
|
516
651
|
return [
|
|
517
652
|
{ id: "camera.main", object: ctx.camera },
|
|
@@ -606,9 +741,12 @@ function stateResources(ctx) {
|
|
|
606
741
|
const state = ctx.state.getState();
|
|
607
742
|
assertJsonSafe(state);
|
|
608
743
|
const value = state;
|
|
609
|
-
return
|
|
744
|
+
return Object.fromEntries([
|
|
745
|
+
["GameState", value],
|
|
746
|
+
["state", value]
|
|
747
|
+
]);
|
|
610
748
|
}
|
|
611
749
|
};
|
|
612
750
|
}
|
|
613
751
|
|
|
614
|
-
export { playtest };
|
|
752
|
+
export { PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, playtest };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threenative/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The small vanilla Three.js game runtime foundation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"three": "0.185.1",
|
|
33
|
+
"three-mesh-bvh": "0.9.14",
|
|
33
34
|
"zustand": "5.x"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"publint": "0.3.22",
|
|
38
39
|
"tsup": "8.5.1",
|
|
39
40
|
"typescript": "5.9.3",
|
|
40
|
-
"@threenative/playtest": "0.
|
|
41
|
+
"@threenative/playtest": "0.2.0"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "tsup --config tsup.config.ts && publint",
|
package/dist/game-DRPs3M7r.d.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { Texture, Vector2, Object3D, Camera, Vector3, Scene as Scene$1 } from 'three';
|
|
2
|
-
import { StoreApi } from 'zustand/vanilla';
|
|
3
|
-
|
|
4
|
-
interface AssetLoaderOptions {
|
|
5
|
-
readonly basePath?: string;
|
|
6
|
-
readonly model?: (url: string) => Promise<unknown>;
|
|
7
|
-
readonly texture?: (url: string) => Promise<Texture>;
|
|
8
|
-
readonly audio?: (url: string) => Promise<AudioBuffer>;
|
|
9
|
-
}
|
|
10
|
-
interface AssetLoader {
|
|
11
|
-
model<T = unknown>(path: string): Promise<T>;
|
|
12
|
-
texture(path: string): Promise<Texture>;
|
|
13
|
-
audio(path: string): Promise<AudioBuffer>;
|
|
14
|
-
release(kind: "audio" | "model" | "texture", path: string): boolean;
|
|
15
|
-
clear(): void;
|
|
16
|
-
}
|
|
17
|
-
declare function createAssetLoader(options?: AssetLoaderOptions): AssetLoader;
|
|
18
|
-
|
|
19
|
-
interface InputAction {
|
|
20
|
-
readonly buttons?: readonly number[];
|
|
21
|
-
readonly down?: readonly string[];
|
|
22
|
-
readonly left?: readonly string[];
|
|
23
|
-
readonly pointer?: boolean;
|
|
24
|
-
readonly right?: readonly string[];
|
|
25
|
-
readonly up?: readonly string[];
|
|
26
|
-
}
|
|
27
|
-
type InputBindings = Record<string, InputAction>;
|
|
28
|
-
interface InputGamepad {
|
|
29
|
-
readonly axes: ArrayLike<number>;
|
|
30
|
-
readonly buttons: readonly {
|
|
31
|
-
readonly pressed: boolean;
|
|
32
|
-
}[];
|
|
33
|
-
}
|
|
34
|
-
type InputPlatformSource = () => readonly (InputGamepad | null)[];
|
|
35
|
-
interface RawInputState {
|
|
36
|
-
readonly keys: ReadonlySet<string>;
|
|
37
|
-
readonly pointer: {
|
|
38
|
-
buttons: number;
|
|
39
|
-
down: boolean;
|
|
40
|
-
readonly position: Vector2;
|
|
41
|
-
};
|
|
42
|
-
readonly gamepad: {
|
|
43
|
-
axes: readonly number[];
|
|
44
|
-
buttons: readonly boolean[];
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
declare class InputMap {
|
|
48
|
-
#private;
|
|
49
|
-
readonly raw: RawInputState;
|
|
50
|
-
constructor(bindings?: InputBindings, target?: EventTarget, pointerTarget?: EventTarget, source?: InputPlatformSource);
|
|
51
|
-
vector(name: string): Vector2;
|
|
52
|
-
pressed(name: string): boolean;
|
|
53
|
-
justPressed(name: string): boolean;
|
|
54
|
-
justReleased(name: string): boolean;
|
|
55
|
-
tick(): void;
|
|
56
|
-
clear(): void;
|
|
57
|
-
dispose(): void;
|
|
58
|
-
}
|
|
59
|
-
declare function input(bindings?: InputBindings, target?: EventTarget, pointerTarget?: EventTarget, source?: InputPlatformSource): InputMap;
|
|
60
|
-
|
|
61
|
-
interface Random {
|
|
62
|
-
(): number;
|
|
63
|
-
pick<T>(items: readonly T[]): T;
|
|
64
|
-
range(min: number, max: number): number;
|
|
65
|
-
state: number;
|
|
66
|
-
}
|
|
67
|
-
declare function createRandom(seed?: number): Random;
|
|
68
|
-
|
|
69
|
-
type RendererKind = "webgpu" | "webgl2";
|
|
70
|
-
interface RendererLike {
|
|
71
|
-
readonly domElement: HTMLCanvasElement;
|
|
72
|
-
readonly kind: RendererKind;
|
|
73
|
-
readonly raw: unknown;
|
|
74
|
-
compute(node: unknown): void;
|
|
75
|
-
render(scene: Object3D, camera: Camera): void;
|
|
76
|
-
setOutputNode(node: unknown): void;
|
|
77
|
-
setSize(width: number, height: number, updateStyle?: boolean): void;
|
|
78
|
-
dispose(): void;
|
|
79
|
-
}
|
|
80
|
-
interface RendererPlatformSource {
|
|
81
|
-
createCanvas(): HTMLCanvasElement;
|
|
82
|
-
hasWebGPU(): boolean;
|
|
83
|
-
observeResize(canvas: HTMLCanvasElement, resize: () => void): () => void;
|
|
84
|
-
readSize(canvas: HTMLCanvasElement): readonly [width: number, height: number];
|
|
85
|
-
}
|
|
86
|
-
interface RendererOptions {
|
|
87
|
-
canvas?: HTMLCanvasElement;
|
|
88
|
-
preferWebGPU?: boolean;
|
|
89
|
-
source?: RendererPlatformSource;
|
|
90
|
-
webgpuFactory?: (canvas: HTMLCanvasElement) => Promise<unknown> | unknown;
|
|
91
|
-
webgl2Factory?: (canvas: HTMLCanvasElement) => unknown;
|
|
92
|
-
}
|
|
93
|
-
declare function createRenderer(options?: RendererOptions): Promise<RendererLike>;
|
|
94
|
-
|
|
95
|
-
type EntitySnapshot = Record<string, Record<string, unknown> & {
|
|
96
|
-
tags?: string[];
|
|
97
|
-
}>;
|
|
98
|
-
interface Debuggable {
|
|
99
|
-
debug(): Record<string, unknown>;
|
|
100
|
-
}
|
|
101
|
-
declare function autoFields(entity: object): Record<string, unknown>;
|
|
102
|
-
declare class Registry {
|
|
103
|
-
#private;
|
|
104
|
-
add<T extends object>(name: string, entity: T): T;
|
|
105
|
-
get<T extends object = object>(name: string): T | undefined;
|
|
106
|
-
remove(name: string): void;
|
|
107
|
-
clear(): void;
|
|
108
|
-
snapshot(): EntitySnapshot;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
type ScheduleHandle = (() => void) & {
|
|
112
|
-
cancel(): void;
|
|
113
|
-
readonly active: boolean;
|
|
114
|
-
};
|
|
115
|
-
type TweenProperties<T extends object> = {
|
|
116
|
-
[K in keyof T]?: number;
|
|
117
|
-
};
|
|
118
|
-
declare class Scheduler {
|
|
119
|
-
#private;
|
|
120
|
-
get size(): number;
|
|
121
|
-
after(delay: number, callback: () => void): ScheduleHandle;
|
|
122
|
-
every(callback: (dt: number) => void): ScheduleHandle;
|
|
123
|
-
tween<T extends object>(target: T, properties: TweenProperties<T>, duration: number): Promise<void>;
|
|
124
|
-
tick(dt: number): void;
|
|
125
|
-
clear(): void;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
type StatePatch<T extends Record<string, unknown>> = Partial<T> | ((state: T) => Partial<T>);
|
|
129
|
-
type GameStore<T extends Record<string, unknown>> = StoreApi<T> & {
|
|
130
|
-
set(patch: StatePatch<T>): void;
|
|
131
|
-
flush(): void;
|
|
132
|
-
start(): void;
|
|
133
|
-
stop(): void;
|
|
134
|
-
};
|
|
135
|
-
declare function createGameStore<T extends Record<string, unknown>>(initial: T, intervalMs?: number): GameStore<T>;
|
|
136
|
-
|
|
137
|
-
interface ViewportSize {
|
|
138
|
-
readonly aspect: number;
|
|
139
|
-
readonly height: number;
|
|
140
|
-
readonly width: number;
|
|
141
|
-
}
|
|
142
|
-
interface ViewportOptions {
|
|
143
|
-
readonly camera: Camera;
|
|
144
|
-
readonly renderer: RendererLike;
|
|
145
|
-
readonly source?: ViewportPlatformSource;
|
|
146
|
-
}
|
|
147
|
-
type ViewportResizeHandler = (size: ViewportSize) => void;
|
|
148
|
-
interface ViewportPlatformSource {
|
|
149
|
-
observeResize(canvas: HTMLCanvasElement, resize: () => void): () => void;
|
|
150
|
-
readSize(canvas: HTMLCanvasElement): ViewportSize;
|
|
151
|
-
}
|
|
152
|
-
declare class Viewport {
|
|
153
|
-
#private;
|
|
154
|
-
readonly camera: Camera;
|
|
155
|
-
readonly renderer: RendererLike;
|
|
156
|
-
constructor(options: ViewportOptions);
|
|
157
|
-
get size(): ViewportSize;
|
|
158
|
-
projectPosition(screen: Vector2, z?: number): Vector3;
|
|
159
|
-
unprojectPosition(world: Vector3): Vector2;
|
|
160
|
-
onResize(handler: ViewportResizeHandler): () => void;
|
|
161
|
-
resize(): void;
|
|
162
|
-
dispose(): void;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
declare abstract class Scene<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
166
|
-
static readonly initialState: Record<string, unknown> | undefined;
|
|
167
|
-
load(_ctx: Ctx<TState, TPhysics>): void | Promise<void>;
|
|
168
|
-
enter(_ctx: Ctx<TState, TPhysics>): SceneEnterResult<TState, TPhysics>;
|
|
169
|
-
exit(_ctx: Ctx<TState, TPhysics>): void;
|
|
170
|
-
update(_ctx: Ctx<TState, TPhysics>, _dt: number): void;
|
|
171
|
-
render(_ctx: Ctx<TState, TPhysics>): void;
|
|
172
|
-
}
|
|
173
|
-
type SceneConstructor<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = new () => Scene<TState, TPhysics>;
|
|
174
|
-
type SceneFrame<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: Ctx<TState, TPhysics>, dt: number) => void;
|
|
175
|
-
type SceneEnterResult<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = // biome-ignore lint/suspicious/noConfusingVoidType: void preserves existing Scene.enter overrides.
|
|
176
|
-
void | SceneFrame<TState, TPhysics>;
|
|
177
|
-
interface Ctx<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
178
|
-
readonly fps: number;
|
|
179
|
-
readonly renderer: RendererLike;
|
|
180
|
-
readonly viewport: Viewport;
|
|
181
|
-
readonly scene: Scene$1;
|
|
182
|
-
readonly camera: Camera;
|
|
183
|
-
readonly entities: Registry;
|
|
184
|
-
readonly add: (object: Object3D) => Object3D;
|
|
185
|
-
readonly input: InputMap;
|
|
186
|
-
readonly assets: AssetLoader;
|
|
187
|
-
readonly after: (delay: number, callback: () => void) => ScheduleHandle;
|
|
188
|
-
readonly every: (callback: (dt: number) => void) => ScheduleHandle;
|
|
189
|
-
readonly state: GameStore<TState>;
|
|
190
|
-
readonly tween: <T extends object>(target: T, properties: {
|
|
191
|
-
[K in keyof T]?: number;
|
|
192
|
-
}, duration: number) => Promise<void>;
|
|
193
|
-
readonly random: Random;
|
|
194
|
-
readonly goto: (name: string) => Promise<void>;
|
|
195
|
-
physics: TPhysics;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
type PluginCleanup = () => void;
|
|
199
|
-
interface GamePluginRuntime {
|
|
200
|
-
fixedStep(ticks: number): number;
|
|
201
|
-
readonly random?: Pick<Random, "state">;
|
|
202
|
-
rapier?: string | null;
|
|
203
|
-
readonly seed: number | null;
|
|
204
|
-
readonly step: number;
|
|
205
|
-
}
|
|
206
|
-
interface GamePlatformSource {
|
|
207
|
-
readonly devToolsHost?: Record<string, unknown>;
|
|
208
|
-
readonly input: NonNullable<ConstructorParameters<typeof InputMap>[3]>;
|
|
209
|
-
readonly inputTarget?: EventTarget;
|
|
210
|
-
readonly renderer: NonNullable<RendererOptions["source"]>;
|
|
211
|
-
readonly viewport: NonNullable<ViewportOptions["source"]>;
|
|
212
|
-
mountCanvas(canvas: HTMLCanvasElement, container?: HTMLElement): void;
|
|
213
|
-
unmountCanvas(canvas: HTMLCanvasElement): void;
|
|
214
|
-
}
|
|
215
|
-
type GamePluginFunction<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: Ctx<TState, TPhysics>) => undefined | PluginCleanup;
|
|
216
|
-
interface GamePluginHooks<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
217
|
-
setup?(ctx: Ctx<TState, TPhysics>, runtime?: GamePluginRuntime): undefined | PluginCleanup | Promise<undefined | PluginCleanup>;
|
|
218
|
-
beforeUpdate?(ctx: Ctx<TState, TPhysics>, dt: number): void;
|
|
219
|
-
update?(ctx: Ctx<TState, TPhysics>, dt: number): void;
|
|
220
|
-
sceneExit?(ctx: Ctx<TState, TPhysics>): void;
|
|
221
|
-
dispose?(ctx: Ctx<TState, TPhysics>): void;
|
|
222
|
-
}
|
|
223
|
-
type GamePlugin<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = GamePluginFunction<TState, TPhysics> | GamePluginHooks<TState, TPhysics>;
|
|
224
|
-
interface GameConfig<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
225
|
-
readonly assets?: AssetLoaderOptions;
|
|
226
|
-
readonly camera?: CameraConfig;
|
|
227
|
-
readonly canvas?: HTMLCanvasElement;
|
|
228
|
-
readonly container?: HTMLElement;
|
|
229
|
-
readonly input?: InputBindings;
|
|
230
|
-
readonly initialState?: TState;
|
|
231
|
-
readonly inputTarget?: EventTarget;
|
|
232
|
-
readonly maxSteps?: number;
|
|
233
|
-
readonly platform?: GamePlatformSource;
|
|
234
|
-
readonly plugins?: readonly GamePlugin<TState, TPhysics>[];
|
|
235
|
-
readonly render?: Pick<RendererOptions, "preferWebGPU">;
|
|
236
|
-
readonly renderer?: RendererOptions;
|
|
237
|
-
readonly seed?: number;
|
|
238
|
-
readonly scenes: Record<string, SceneConstructor<TState, TPhysics>>;
|
|
239
|
-
readonly step?: number;
|
|
240
|
-
readonly start: string;
|
|
241
|
-
readonly stateFlushMs?: number;
|
|
242
|
-
}
|
|
243
|
-
interface PerspectiveCameraConfig {
|
|
244
|
-
readonly projection: "perspective";
|
|
245
|
-
readonly fov?: number;
|
|
246
|
-
readonly near?: number;
|
|
247
|
-
readonly far?: number;
|
|
248
|
-
}
|
|
249
|
-
interface OrthogonalCameraConfig {
|
|
250
|
-
readonly projection: "orthogonal";
|
|
251
|
-
readonly size: number;
|
|
252
|
-
readonly near?: number;
|
|
253
|
-
readonly far?: number;
|
|
254
|
-
}
|
|
255
|
-
type CameraConfig = PerspectiveCameraConfig | OrthogonalCameraConfig;
|
|
256
|
-
interface Game<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
257
|
-
readonly ctx: Ctx<TState, TPhysics> | undefined;
|
|
258
|
-
readonly scene: Scene<TState, TPhysics> | undefined;
|
|
259
|
-
readonly state: GameStore<TState>;
|
|
260
|
-
/** Rebuilds the requested scene from the game's declared initial state. */
|
|
261
|
-
goto(name: string): Promise<void>;
|
|
262
|
-
start(): Promise<void>;
|
|
263
|
-
pause(): void;
|
|
264
|
-
resume(): void;
|
|
265
|
-
stop(): void;
|
|
266
|
-
}
|
|
267
|
-
declare function defineGame<TState extends Record<string, unknown>, TPhysics = undefined>(config: GameConfig<TState, TPhysics>): Game<TState, TPhysics>;
|
|
268
|
-
|
|
269
|
-
export { type AssetLoader as A, autoFields as B, type CameraConfig as C, type Debuggable as D, type EntitySnapshot as E, createAssetLoader as F, type Game as G, createGameStore as H, type InputAction as I, createRandom as J, createRenderer as K, defineGame as L, input as M, type OrthogonalCameraConfig as O, type PerspectiveCameraConfig as P, type RendererLike as R, Scene as S, Viewport as V, type GamePluginRuntime as a, type GamePluginHooks as b, type AssetLoaderOptions as c, type Ctx as d, type GameConfig as e, type GamePlatformSource as f, type GamePlugin as g, type GamePluginFunction as h, type GameStore as i, type InputBindings as j, InputMap as k, type PluginCleanup as l, type Random as m, type RawInputState as n, Registry as o, type RendererKind as p, type RendererOptions as q, type SceneConstructor as r, type SceneEnterResult as s, type SceneFrame as t, type ScheduleHandle as u, Scheduler as v, type StatePatch as w, type ViewportOptions as x, type ViewportResizeHandler as y, type ViewportSize as z };
|