@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.
@@ -1,11 +1,33 @@
1
1
  import { JsonValue } from '@threenative/playtest';
2
- import { b as GamePluginHooks } from './game-DRPs3M7r.js';
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?: PlaytestOptions): GamePluginHooks<TState, TPhysics>;
7
- interface PlaytestOptions {
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 PlaytestOptions, playtest };
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", "Samples application-owned 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 startTick = tick;
367
- tick += ticks;
368
- return { clock: { mode: "fixed-step", tick }, ticks: tick - startTick };
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.fixedStep === void 0 ? void 0 : tick
469
+ tick: options.tick?.()
399
470
  }, request);
400
471
  const components = options.components?.();
401
- const result = components === void 0 || Object.keys(components).length === 0 ? snapshot : { ...snapshot, components };
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: (ticks) => advance(runtime, ticks) },
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
- return () => {
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
- function installRuntimeChannels(bridge) {
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
- bridge.describe = () => Promise.resolve(describe()).then((description) => addRuntimeCapabilities(description));
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
- for (const capability2 of ["runtime.audio", "runtime.world"]) {
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 { GameState: value, state: value };
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.1.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.1.0"
41
+ "@threenative/playtest": "0.2.0"
41
42
  },
42
43
  "scripts": {
43
44
  "build": "tsup --config tsup.config.ts && publint",
@@ -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 };