@threenative/core 0.1.0 → 0.3.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.
@@ -0,0 +1,980 @@
1
+ import { Texture, Vector2, Vector3, Object3D, Camera, Intersection, Scene as Scene$1 } from 'three';
2
+ import { h as IFramePhaseSample, U as Viewport, I as IRendererLike, C as CanvasLayer, W as IRendererOptions, X as IViewportOptions, g as IFrameBudgetWindow, e as IFrameBudgetOptions } from './canvas-layer-CtrZHgIh.js';
3
+ import { StoreApi } from 'zustand/vanilla';
4
+
5
+ interface IAssetLoaderOptions {
6
+ readonly basePath?: string;
7
+ /**
8
+ * URL of the manifest written by the asset compile step (`public/assets.manifest.json`).
9
+ * Defaults to `assets.manifest.json` resolved against `basePath`. A logical path is resolved
10
+ * to its compiled output through it; a manifest that is absent — 404 or unfetchable — falls
11
+ * back to loading every path verbatim, while one that is served malformed or with an unknown
12
+ * version throws.
13
+ */
14
+ readonly manifest?: string;
15
+ readonly model?: (url: string) => Promise<unknown>;
16
+ readonly texture?: (url: string) => Promise<Texture>;
17
+ readonly audio?: (url: string) => Promise<AudioBuffer>;
18
+ /**
19
+ * The live renderer (`IRendererLike.raw`), handed to `KTX2Loader.detectSupport()` exactly
20
+ * once so compiled KTX2 textures transcode to a format this machine's GPU actually supports.
21
+ * Required for games whose textures compile to `.ktx2`; without it such a load throws rather
22
+ * than silently uploading decoded RGBA.
23
+ */
24
+ readonly renderer?: unknown;
25
+ }
26
+ /** The structural slice of three's `KTX2Loader` core depends on. */
27
+ interface IKTX2LoaderLike {
28
+ detectSupport(renderer: unknown): unknown;
29
+ load(url: string, onLoad: (texture: Texture) => void, onProgress?: (event: ProgressEvent<EventTarget>) => void, onError?: (error: unknown) => void): unknown;
30
+ setTranscoderPath(path: string): unknown;
31
+ }
32
+ /**
33
+ * Present only when a renderer was handed to the asset loader. `ready` settles once support
34
+ * detection ran; it rejects naming the renderer and platform when no compressed format is
35
+ * supported, and `defineGame` awaits it during boot so such a target fails at construction.
36
+ */
37
+ interface ICompressedTextureSupport {
38
+ /**
39
+ * The one shared instance, also handed to `GLTFLoader.setKTX2Loader()` for models. Resolves
40
+ * `undefined` when the renderer exposed no surface to probe (see `createKtx2Loader`).
41
+ */
42
+ readonly loader: Promise<IKTX2LoaderLike | undefined>;
43
+ readonly ready: Promise<void>;
44
+ }
45
+ interface IAssetLoader {
46
+ readonly compressedTextures?: ICompressedTextureSupport;
47
+ model<T = unknown>(path: string): Promise<T>;
48
+ texture(path: string): Promise<Texture>;
49
+ audio(path: string): Promise<AudioBuffer>;
50
+ release(kind: "audio" | "model" | "texture", path: string): boolean;
51
+ clear(): void;
52
+ }
53
+ declare function createAssetLoader(options?: IAssetLoaderOptions): IAssetLoader;
54
+
55
+ type ThreeNativeOrientation = "landscape" | "portrait" | "sensor";
56
+ /** Which renderer draws a game's `src/ui/`. @see IThreeNativeConfig.ui */
57
+ type ThreeNativeUiRenderer = "native" | "web";
58
+ /** What the native host does with the render loop while the app is off-screen. */
59
+ type ThreeNativeBackgroundMode = "continue" | "pause";
60
+ interface IThreeNativeIconVariants {
61
+ readonly android?: {
62
+ readonly foreground?: string;
63
+ readonly background?: string;
64
+ readonly monochrome?: string;
65
+ };
66
+ readonly ios?: {
67
+ readonly dark?: string;
68
+ readonly tinted?: string;
69
+ };
70
+ readonly web?: {
71
+ readonly favicon?: string;
72
+ readonly maskable?: string;
73
+ readonly monochrome?: string;
74
+ readonly appleTouch?: string;
75
+ };
76
+ }
77
+ interface IThreeNativeBootSplash {
78
+ readonly backgroundColor?: string;
79
+ readonly image?: string;
80
+ }
81
+ /** Texture compression options for the asset compile step; `"none"` ships sources verbatim. */
82
+ interface IThreeNativeTexturesConfig {
83
+ readonly overrides?: readonly {
84
+ readonly codec: "etc1s" | "none" | "uastc";
85
+ readonly glob: string;
86
+ readonly quality?: number;
87
+ }[];
88
+ /** ETC1S encoder quality 1–255. Ignored for UASTC. */
89
+ readonly quality?: number;
90
+ }
91
+ /** Model optimization sub-pass switches; absent means every pass runs. */
92
+ interface IThreeNativeModelPassesConfig {
93
+ readonly dedup?: boolean;
94
+ readonly meshopt?: boolean;
95
+ readonly prune?: boolean;
96
+ readonly quantize?: boolean;
97
+ readonly reorder?: boolean;
98
+ }
99
+ /** Model optimization options for the asset compile step; `"none"` ships sources verbatim. */
100
+ interface IThreeNativeModelsConfig {
101
+ /** Standard glTF TEXCOORD_1 atlas generation for offline static-light assets. */
102
+ readonly lightmap?: {
103
+ readonly atlasSize: number;
104
+ readonly padding: number;
105
+ };
106
+ readonly passes?: IThreeNativeModelPassesConfig;
107
+ readonly quantize?: {
108
+ readonly normalBits?: number;
109
+ readonly positionBits?: number;
110
+ readonly uvBits?: number;
111
+ };
112
+ /**
113
+ * Embedded-texture compression for images carried inside a `.glb`.
114
+ *
115
+ * On by default in the compile step; `"none"` ships every embedded image exactly as
116
+ * authored. `maxSize` caps the longest edge, preserving aspect and snapping to whole 4x4
117
+ * blocks, and never upscales.
118
+ */
119
+ readonly textures?: "none" | {
120
+ readonly maxSize?: number;
121
+ readonly quality?: number;
122
+ readonly overrides?: readonly {
123
+ readonly slot: string;
124
+ readonly codec: "etc1s" | "none" | "uastc";
125
+ }[];
126
+ };
127
+ /**
128
+ * Mesh simplification. Absent means none at all, which is the default.
129
+ *
130
+ * `ratio` is the fraction of triangles to keep. `error` is a quality guard rather than a
131
+ * target — the largest a vertex may move as a fraction of the mesh's extent — so a loose
132
+ * ratio with a tight error stops short, and the compile step reports the ratio it actually
133
+ * achieved next to the one that was asked for.
134
+ */
135
+ readonly simplify?: {
136
+ readonly ratio: number;
137
+ readonly error?: number;
138
+ };
139
+ /**
140
+ * Cluster-DAG bake for virtual geometry, or `"none"` to ship every primitive as authored.
141
+ *
142
+ * Absent means on with defaults: any primitive of 65,536 triangles or more bakes to a cluster
143
+ * DAG the loader turns into a `ClusteredMesh`, and everything below that line compiles
144
+ * byte-identically. The payload costs roughly 3-4x the primitive's compiled bytes, which is
145
+ * what `"none"` and `minSourceTriangles` are for.
146
+ */
147
+ readonly virtual?: "none" | {
148
+ /** Clusters folded together per group, default 4. */
149
+ readonly groupSize?: number;
150
+ /** Upper bound on a cluster's triangles, default 128. */
151
+ readonly maxTriangles?: number;
152
+ /** Lower bound on a cluster's triangles, default 96. */
153
+ readonly minTriangles?: number;
154
+ /** Primitives below this many triangles are left alone, default 65,536. */
155
+ readonly minSourceTriangles?: number;
156
+ /** Fraction of a group's triangles kept per level, default 0.5. */
157
+ readonly simplifyRatio?: number;
158
+ };
159
+ }
160
+ interface IThreeNativeConfig {
161
+ readonly app?: {
162
+ readonly id?: string;
163
+ readonly name?: string;
164
+ readonly version?: string;
165
+ readonly build?: number;
166
+ readonly icon?: string;
167
+ readonly icons?: IThreeNativeIconVariants;
168
+ };
169
+ readonly display?: {
170
+ readonly orientation?: ThreeNativeOrientation;
171
+ readonly fullscreen?: boolean;
172
+ readonly keepScreenOn?: boolean;
173
+ /**
174
+ * Maximum native presentation rate in frames per second. Defaults to 60; `0` removes the
175
+ * software ceiling. Android also submits this value as the surface's preferred frame rate,
176
+ * which the display policy may decline because of hardware, power, or thermal state. Android
177
+ * uses non-blocking presentation above 60 fps so a missed high-refresh interval does not fall
178
+ * to an integer refresh-rate divisor.
179
+ */
180
+ readonly maxFps?: number;
181
+ /**
182
+ * What the native host does when the player leaves the app — presses the power button,
183
+ * switches away, minimizes the window. `"pause"` (the default) stops running frames and
184
+ * suspends audio until the app comes back; `"continue"` keeps rendering off-screen, which a
185
+ * server-shaped or split-screen game may genuinely want.
186
+ *
187
+ * Turning the pause off does not turn the reporting off: `TN_LIFECYCLE` markers are emitted
188
+ * either way and name the mode that executed.
189
+ */
190
+ readonly backgroundMode?: ThreeNativeBackgroundMode;
191
+ };
192
+ readonly window?: {
193
+ readonly title?: string;
194
+ readonly width?: number;
195
+ readonly height?: number;
196
+ readonly resizable?: boolean;
197
+ };
198
+ readonly assets?: {
199
+ readonly models?: "none" | IThreeNativeModelsConfig;
200
+ readonly output?: string;
201
+ readonly source?: string;
202
+ readonly targets?: {
203
+ readonly maxMaterials?: number;
204
+ readonly maxTriangles?: number;
205
+ readonly maxTextureDimension?: number;
206
+ };
207
+ /** Texture compression options, or `"none"` to ship every texture exactly as committed. */
208
+ readonly textures?: "none" | IThreeNativeTexturesConfig;
209
+ };
210
+ readonly bootSplash?: IThreeNativeBootSplash;
211
+ readonly nativeEntry?: string;
212
+ readonly renderer?: {
213
+ readonly preferWebGPU?: boolean;
214
+ /**
215
+ * Portable drawing-buffer scale. CSS and UI layout dimensions are unchanged.
216
+ *
217
+ * `"auto"` — the default a template ships — lets the engine hold the `display.maxFps` budget
218
+ * without the game hand-authoring a resolution constant. A number in `(0, 1]` pins it and
219
+ * turns the loop off. Either way the active scale is reported in every `TN_FRAME_BUDGET`
220
+ * window: turning the convention off does not turn its measurement off.
221
+ */
222
+ readonly resolutionScale?: number | "auto";
223
+ /** Portable multisampling. Sampling and resolution are one pixel budget, not two. */
224
+ readonly antialias?: boolean;
225
+ /**
226
+ * Android-only rendering overrides selected by the engine.
227
+ *
228
+ * `antialias` belongs here beside `resolutionScale` because they spend the same budget: a
229
+ * tile-based mobile GPU resolves MSAA in tile memory and prices it quite differently from a
230
+ * desktop one, so a platform that scales resolution down must be able to buy sampling back
231
+ * on that same platform rather than accepting whatever the portable value happened to be.
232
+ */
233
+ readonly android?: {
234
+ readonly resolutionScale?: number | "auto";
235
+ readonly antialias?: boolean;
236
+ };
237
+ };
238
+ readonly ui?: {
239
+ /**
240
+ * Which renderer draws `src/ui/`.
241
+ *
242
+ * `"web"` runs the same React DOM, Tailwind, CSS, SVG and fonts on every target, through
243
+ * that platform's own browser-class renderer composited over the game surface. What is
244
+ * guaranteed is source parity — one `src/ui/` — not browser-binary parity, which no design
245
+ * using the platforms' own engines can offer once iOS is in the set.
246
+ *
247
+ * `"native"` maps React to `CanvasLayer` quads with no web view, no CSS and no second
248
+ * process. Choose it for a UI that is part of the rendered frame, or a target with no web
249
+ * view, or zero extra processes — and own the appearance difference, which is the trade
250
+ * being made rather than something to discover in a screenshot.
251
+ *
252
+ * Which surface `"web"` lands on is the platform's business and never a game's: no config,
253
+ * type or document names the engine underneath.
254
+ */
255
+ readonly renderer?: ThreeNativeUiRenderer;
256
+ };
257
+ }
258
+
259
+ /**
260
+ * One action, read either as a button through `pressed`/`justPressed`, a 2D axis through
261
+ * `vector`, or a scalar axis through `axis`. Which one you get depends on the fields you fill in,
262
+ * and mixing the two is what makes bindings confusing to read:
263
+ *
264
+ * ```ts
265
+ * jump: { keys: ["Space"], buttons: [0] } // a button: key or *gamepad* button 0
266
+ * fire: { keys: ["Space"], mouseButtons: [0] } // a button: key or *left mouse*
267
+ * move: { up: ["KeyW"], down: ["KeyS"], left: [...], right: [...] } // an axis
268
+ * ```
269
+ *
270
+ * **`buttons` is the gamepad, `mouseButtons` is the mouse.** They are separate devices and
271
+ * `buttons: [0]` on a machine with no gamepad plugged in silently never fires.
272
+ *
273
+ * `up`/`down`/`left`/`right` are **directions of an axis**, not "the keys that press this".
274
+ * They are named after the vector they build, so `pressed("move")` is true whenever any
275
+ * direction is held — including `ArrowDown`. For a button, use `keys`.
276
+ */
277
+ interface IInputAction {
278
+ /** **Gamepad** button indices that press this action. For the mouse, see `mouseButtons`. */
279
+ readonly buttons?: readonly number[];
280
+ /** **Gamepad** axis indices whose values contribute to `axis(name)`. */
281
+ readonly gamepadAxes?: readonly number[];
282
+ /**
283
+ * **Mouse** button indices that press this action, numbered as `MouseEvent.button`:
284
+ * `0` left, `1` middle, `2` right. Binding `2` also suppresses the browser context menu on
285
+ * the pointer target, because a right-click binding is unusable while the menu eats it.
286
+ */
287
+ readonly mouseButtons?: readonly number[];
288
+ /**
289
+ * Keyboard codes that press this action. Use this for a button — `jump`, `restart`, `fire`.
290
+ * A key listed here never contributes to `vector`.
291
+ */
292
+ readonly keys?: readonly string[];
293
+ /** The **−y** direction of `vector(name)`. Not "the keys that press this action" — see `keys`. */
294
+ readonly down?: readonly string[];
295
+ /** The −x direction of `vector(name)`. */
296
+ readonly left?: readonly string[];
297
+ /** Any active pointer or touch presses this action. */
298
+ readonly pointer?: boolean;
299
+ /** Add normalized browser/native wheel motion to `axis(name)`; negative DOM deltaY (toward-user) is positive. */
300
+ readonly scroll?: boolean;
301
+ /** Add the signed two-pointer distance change to `axis(name)`; moving apart is positive. */
302
+ readonly pinch?: boolean;
303
+ /** Add raw mouse movement since the last input tick to `vector(name)`. */
304
+ readonly pointerRelative?: boolean;
305
+ /** The +x direction of `vector(name)`. */
306
+ readonly right?: readonly string[];
307
+ /** The +y direction of `vector(name)`. */
308
+ readonly up?: readonly string[];
309
+ }
310
+ type InputBindings = Record<string, IInputAction>;
311
+ /**
312
+ * What the browser context menu does over the game surface. `"suppress"` is the default: a
313
+ * right-click binding cannot work while the menu opens on the same press, and a context menu
314
+ * over a game canvas is almost never what anyone wants. `"allow"` restores the browser default.
315
+ */
316
+ type ContextMenuPolicy = "allow" | "suppress";
317
+ interface IInputGamepad {
318
+ readonly axes: ArrayLike<number>;
319
+ readonly buttons: readonly {
320
+ readonly pressed: boolean;
321
+ }[];
322
+ }
323
+ type InputPlatformSource = () => readonly (IInputGamepad | null)[];
324
+ interface IRawInputPointer {
325
+ readonly id: number;
326
+ buttons: number;
327
+ readonly position: Vector2;
328
+ }
329
+ interface IRawInputPointerEdge {
330
+ readonly buttons: number;
331
+ readonly id: number;
332
+ readonly position: Vector2;
333
+ readonly type: "cancel" | "down" | "up";
334
+ }
335
+ interface IRawInputState {
336
+ readonly keys: ReadonlySet<string>;
337
+ readonly pointer: {
338
+ buttons: number;
339
+ captured: boolean;
340
+ down: boolean;
341
+ readonly position: Vector2;
342
+ /** Accumulated relative mouse motion since the last `tick`, in canvas pixels. */
343
+ readonly relative: Vector2;
344
+ };
345
+ /** Pointer transitions captured since the previous input tick, grouped by pointer id. */
346
+ readonly pointerEdges: ReadonlyMap<number, readonly IRawInputPointerEdge[]>;
347
+ readonly pointers: ReadonlyMap<number, IRawInputPointer>;
348
+ readonly gamepad: {
349
+ axes: readonly number[];
350
+ buttons: readonly boolean[];
351
+ };
352
+ }
353
+ declare class InputMap {
354
+ #private;
355
+ readonly raw: IRawInputState;
356
+ constructor(bindings?: InputBindings, target?: EventTarget, pointerTarget?: EventTarget, source?: InputPlatformSource, contextMenu?: ContextMenuPolicy);
357
+ /**
358
+ * Returns a 2D action vector where +y is up. On a conventional XZ ground plane whose
359
+ * forward direction is -z, map `vector.y` to `-z`. This differs from Godot's
360
+ * `Input.get_vector`, where up is -y.
361
+ */
362
+ vector(name: string): Vector2;
363
+ /**
364
+ * Returns a scalar action for this fixed-step tick, clamped to `[-1, 1]`.
365
+ *
366
+ * `scroll: true` normalizes wheel deltas to pixels before scaling them: one line is 16 pixels,
367
+ * one page is 800 pixels, and 100 pixels is one axis unit. Browser and native wheel events use
368
+ * the same DOM sign convention, so scrolling the wheel toward the user (negative DOM `deltaY`) is
369
+ * positive and scrolling away (positive DOM `deltaY`) is negative. `pinch: true` reports the relative distance change of the first two active pointers;
370
+ * a third pointer is ignored and a changing pair starts a new gesture without a jump.
371
+ */
372
+ axis(name: string): number;
373
+ /** Request pointer capture. Call this from a user gesture on the game surface. */
374
+ captureMouse(): void;
375
+ /** Release pointer capture. A browser refusal remains an unhandled rejection. */
376
+ releaseMouse(): void;
377
+ pressed(name: string): boolean;
378
+ justPressed(name: string): boolean;
379
+ justReleased(name: string): boolean;
380
+ tick(): void;
381
+ clear(): void;
382
+ dispose(): void;
383
+ }
384
+
385
+ interface IRenderPerformanceMetrics {
386
+ readonly drawCalls?: number;
387
+ readonly triangles?: number;
388
+ }
389
+ interface IRenderPerformanceSample extends IRenderPerformanceMetrics {
390
+ readonly frameMs: number;
391
+ /** Where this frame's milliseconds went. Present whenever a frame budget is installed. */
392
+ readonly phases?: IFramePhaseSample;
393
+ }
394
+
395
+ interface IRandom {
396
+ (): number;
397
+ pick<T>(items: readonly T[]): T;
398
+ range(min: number, max: number): number;
399
+ state: number;
400
+ }
401
+ declare function createRandom(seed?: number): IRandom;
402
+
403
+ type EntitySnapshot = Record<string, Record<string, unknown> & {
404
+ tags?: string[];
405
+ }>;
406
+
407
+ declare class Registry {
408
+ #private;
409
+ add<T extends object>(name: string, entity: T): T;
410
+ get<T extends object = object>(name: string): T | undefined;
411
+ remove(name: string): void;
412
+ queueFree(target: string | object): void;
413
+ sweep(): void;
414
+ clear(): void;
415
+ snapshot(): EntitySnapshot;
416
+ }
417
+
418
+ interface IRaycastOptions {
419
+ /** Screen point in canvas pixels. Mutually exclusive with `origin` and `direction`. */
420
+ readonly screen?: Vector2;
421
+ /** World-space ray origin. Requires `direction`. */
422
+ readonly origin?: Vector3;
423
+ /** World-space ray direction. The caller must provide a normalised vector. */
424
+ readonly direction?: Vector3;
425
+ /** Maximum distance. Defaults to unbounded. */
426
+ readonly far?: number;
427
+ /** What to test. Defaults to the whole scene. */
428
+ readonly targets?: Object3D | readonly Object3D[];
429
+ /** Subtrees never hit, whatever `targets` says. */
430
+ readonly exclude?: Object3D | readonly Object3D[];
431
+ }
432
+ interface IScenePickerOptions {
433
+ readonly camera: Camera;
434
+ readonly pointer: () => Vector2;
435
+ readonly scene: Object3D;
436
+ readonly viewport: Viewport;
437
+ }
438
+ /**
439
+ * Ray queries against scene geometry, accelerated by a bounding volume hierarchy that is
440
+ * built on first use and rebuilt when the geometry's positions change.
441
+ *
442
+ * The acceleration is an implementation detail: nothing about it reaches the caller, no
443
+ * `three` prototype is patched, and a game that never calls `raycast` never builds a tree.
444
+ * Skinned, instanced, batched and morphed meshes fall back to the stock `three` path,
445
+ * because a hierarchy over their rest positions would report hits in the wrong place.
446
+ */
447
+ declare class ScenePicker {
448
+ #private;
449
+ constructor(options: IScenePickerOptions);
450
+ /**
451
+ * The closest hit, or `undefined` when the ray hits nothing.
452
+ *
453
+ * Collects at most one hit per target: the traversal runs with `firstHitOnly` so BVH-backed
454
+ * meshes stop at their own closest triangle (PRD-186 phase 1), and anything on the stock
455
+ * fallback path — instanced, skinned, morphed — is reduced to its own nearest before it joins
456
+ * the candidates. The winner is a running minimum, never a full sort: a game asking "what did
457
+ * this round hit" should not pay for every wall behind the answer.
458
+ */
459
+ raycast(options?: IRaycastOptions): Intersection | undefined;
460
+ /** Every hit, sorted from the nearest to the farthest. */
461
+ raycastAll(options?: IRaycastOptions, target?: Intersection[]): readonly Intersection[];
462
+ /** Drops every cached hierarchy. The next `raycast` rebuilds what it needs. */
463
+ dispose(): void;
464
+ }
465
+
466
+ declare const POINTER_EVENT_TYPES: readonly ["pointerEntered", "pointerExited", "pointerPressed", "pointerReleased", "tapped", "dragStarted", "dragged", "dragEnded"];
467
+ type PointerEvent3DType = (typeof POINTER_EVENT_TYPES)[number];
468
+ interface IPointerState {
469
+ readonly id: number;
470
+ readonly buttons: number;
471
+ readonly position: Vector2;
472
+ }
473
+ interface IPointerEvent3D {
474
+ readonly buttons: number;
475
+ readonly intersection: Intersection | undefined;
476
+ readonly object: Object3D;
477
+ readonly point: Vector3;
478
+ readonly pointerId: number;
479
+ readonly target: Object3D;
480
+ readonly type: PointerEvent3DType;
481
+ stopPropagation(): void;
482
+ }
483
+ type PointerEvent3DListener = (event: IPointerEvent3D) => void;
484
+ interface IPointerDragHandle {
485
+ cancel(): void;
486
+ }
487
+ interface IPointerEvents3DPicker {
488
+ raycastAll(options?: IRaycastOptions): readonly Intersection[];
489
+ }
490
+ interface IPointerEvents3D {
491
+ on(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): () => void;
492
+ off(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): void;
493
+ drag(object: Object3D): IPointerDragHandle;
494
+ }
495
+ interface IPointerEvents3DOptions {
496
+ /** Convert an input position into the canvas-relative pixels expected by ScenePicker. */
497
+ readonly screen?: (position: Vector2, target: Vector2) => Vector2;
498
+ }
499
+ type PrimaryPointer = Pick<IPointerState, "buttons" | "position">;
500
+ type PointerEdge = IRawInputPointerEdge;
501
+ type PointerEdges = ReadonlyMap<number, readonly PointerEdge[]>;
502
+ type PointerPicker = Pick<ScenePicker, "raycastAll"> | IPointerEvents3DPicker;
503
+ /**
504
+ * Dispatch portable pointer events from the input stream to registered Three.js objects.
505
+ *
506
+ * Listeners live in a side table, so this never patches Three.js prototypes. The picker receives
507
+ * one target list and one query per active pointer, while the empty registration path does no
508
+ * queries at all.
509
+ */
510
+ declare class PointerEvents3D implements IPointerEvents3D {
511
+ #private;
512
+ constructor(options?: IPointerEvents3DOptions);
513
+ /** Listen for one event on an object. The returned function removes exactly this listener. */
514
+ on(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): () => void;
515
+ /** Remove a listener previously passed to `on`. */
516
+ off(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): void;
517
+ /** Make an object capture a pointer for drag events until its handle is cancelled. */
518
+ drag(object: Object3D): IPointerDragHandle;
519
+ /**
520
+ * Advance every pointer by one simulation tick.
521
+ *
522
+ * `primary` is the legacy mouse position: InputMap keeps it even when no button is down, which
523
+ * lets hover work on web without changing the existing held-touch map. It is ignored whenever
524
+ * the per-id map has a live pointer.
525
+ */
526
+ tick(pointers: ReadonlyMap<number, IPointerState>, picker: PointerPicker, primary?: PrimaryPointer, edges?: PointerEdges): void;
527
+ /** Remove all registrations and release all per-pointer state, usually on scene change. */
528
+ clear(): void;
529
+ dispose(): void;
530
+ }
531
+
532
+ type ScheduleHandle = (() => void) & {
533
+ cancel(): void;
534
+ readonly active: boolean;
535
+ };
536
+ type TweenProperties<T extends object> = {
537
+ [K in keyof T]?: number;
538
+ };
539
+ interface ITweenOptions {
540
+ readonly ease?: (t: number) => number;
541
+ }
542
+ declare class Scheduler {
543
+ #private;
544
+ get size(): number;
545
+ after(delay: number, callback: () => void): ScheduleHandle;
546
+ every(callback: (dt: number) => void): ScheduleHandle;
547
+ tween<T extends object>(target: T, properties: TweenProperties<T>, duration: number, options?: ITweenOptions): Promise<void>;
548
+ tick(dt: number): void;
549
+ clear(): void;
550
+ }
551
+
552
+ type StatePatch<T extends Record<string, unknown>> = Partial<T> | ((state: T) => Partial<T>);
553
+ type GameStore<T extends Record<string, unknown>> = StoreApi<T> & {
554
+ getPublishedState(): T;
555
+ set(patch: StatePatch<T>): void;
556
+ flush(): void;
557
+ start(): void;
558
+ stop(): void;
559
+ };
560
+
561
+ declare abstract class Scene<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
562
+ static readonly initialState: Record<string, unknown> | undefined;
563
+ load(_ctx: ICtx<TState, TPhysics>): void | Promise<void>;
564
+ enter(_ctx: ICtx<TState, TPhysics>): SceneEnterResult<TState, TPhysics>;
565
+ exit(_ctx: ICtx<TState, TPhysics>): void;
566
+ update(_ctx: ICtx<TState, TPhysics>, _dt: number): void;
567
+ render(_ctx: ICtx<TState, TPhysics>): void;
568
+ }
569
+ type SceneConstructor<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = new () => Scene<TState, TPhysics>;
570
+ type SceneFrame<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: ICtx<TState, TPhysics>, dt: number) => void;
571
+ type SceneEnterResult<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = // biome-ignore lint/suspicious/noConfusingVoidType: void preserves existing Scene.enter overrides.
572
+ void | SceneFrame<TState, TPhysics>;
573
+ interface IStartupStatus {
574
+ /**
575
+ * True once first-use compilation has settled — earlier than `phase === "ready"`, which also
576
+ * waits for a sustained in-budget frame window.
577
+ *
578
+ * This is the signal a game wants when something must not run during the launch. `phase` cannot
579
+ * express it: it is binary, and on a software rasteriser the frame window can only ever expire
580
+ * rather than be met, so a game gated on `phase` alone does nothing for tens of seconds there.
581
+ * Measured as a chase route of length `0.000000` against a required `6`, because the scenario
582
+ * ended before the window did.
583
+ */
584
+ readonly compileSettled: boolean;
585
+ /**
586
+ * `collapsing` until first-use work and a sustained in-budget frame window complete, `ready`
587
+ * once the world is safe to show.
588
+ */
589
+ readonly phase: "observing" | "collapsing" | "ready";
590
+ /** 0 while first-use work or the sustained frame window is pending, then 1. */
591
+ readonly progress: number;
592
+ /** Resolves after first-use work and the sustained frame window, so it is always awaitable. */
593
+ whenReady(): Promise<void>;
594
+ }
595
+ interface ICtx<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
596
+ readonly fps: number;
597
+ readonly renderer: IRendererLike;
598
+ readonly viewport: Viewport;
599
+ readonly scene: Scene$1;
600
+ readonly camera: Camera;
601
+ readonly canvasLayer: CanvasLayer;
602
+ readonly entities: Registry;
603
+ /**
604
+ * Adds a node to the scene and hands it straight back, with its own type intact.
605
+ *
606
+ * Generic rather than `Object3D` because a game writes `const sea = ctx.add(new SpectralOcean())`
607
+ * and then calls a method on it. Erasing the type here makes every typed node in every scene need
608
+ * a cast back to what it already was, and that cast is where a game stops noticing it is holding
609
+ * something else.
610
+ */
611
+ readonly add: <T extends Object3D>(object: T) => T;
612
+ readonly input: InputMap;
613
+ readonly pointer: IPointerEvents3D;
614
+ readonly assets: IAssetLoader;
615
+ readonly after: (delay: number, callback: () => void) => ScheduleHandle;
616
+ readonly every: (callback: (dt: number) => void) => ScheduleHandle;
617
+ readonly state: GameStore<TState>;
618
+ readonly tween: <T extends object>(target: T, properties: {
619
+ [K in keyof T]?: number;
620
+ }, duration: number, options?: ITweenOptions) => Promise<void>;
621
+ readonly random: IRandom;
622
+ readonly raycast: (options?: IRaycastOptions) => Intersection | undefined;
623
+ readonly raycastAll: (options?: IRaycastOptions, target?: Intersection[]) => readonly Intersection[];
624
+ /**
625
+ * The framework's own startup work — what a loading screen waits on.
626
+ *
627
+ * A shader may compile the first time something using it is drawn, and the render projection may
628
+ * do its first build in that same frame. Both costs are real and belong before the world is shown.
629
+ *
630
+ * Keeping the world hidden until `whenReady()` resolves does more than hide the mess: the
631
+ * shaders that would have been compiled for geometry the projection then discards are never
632
+ * compiled at all, so waiting is *faster* than not waiting.
633
+ */
634
+ readonly startup: IStartupStatus;
635
+ readonly goto: (name: string) => Promise<void>;
636
+ physics: TPhysics;
637
+ }
638
+
639
+ /**
640
+ * Compiles a scene's pipelines before the first frame, in slices, yielding between them.
641
+ *
642
+ * On a phone every distinct pipeline is built the first time something using it is drawn. Nothing
643
+ * spreads that cost: the first `render()` of a fully built scene compiles all of it, on the main
644
+ * loop, inside one frame. Measured on a Pixel 8, `sandbox/fps-framework` spent **24.5 seconds
645
+ * across 107 pipeline compiles** inside a single frame — 92 % of everything the launch-stall
646
+ * budget could attribute (`TN_STALL_SEGMENTS`, PRD-218). For that whole span the loop presented
647
+ * nothing, drained no UI messages and ran no callback, so the loading screen froze mid-animation
648
+ * and the game read as hung. The player's report was "the loading screen takes thirty seconds".
649
+ *
650
+ * This does not make the compiling cheaper — the same pipelines are built either way, and a claim
651
+ * otherwise would be a lie the next measurement catches. What it changes is **who waits and
652
+ * whether anything moves**: the work is cut into slices with a frame between them, so the loop
653
+ * presents, the loading screen animates, the UI bridge delivers, and progress is a number the game
654
+ * can show instead of a static label. A cost that cannot be removed must at least be visible; that
655
+ * is the whole of what this buys, and it is worth saying plainly.
656
+ *
657
+ * The slices are compiled through the renderer's own `compileAsync`, which is stock `three`.
658
+ * Nothing here mutates the scene: no reparenting, no temporary groups, no visibility flipping. A
659
+ * warm-up that edited the graph to compile it would be a correctness risk taken for a progress
660
+ * bar, and the object it handed back would not be the one the game authored.
661
+ */
662
+ /** One renderable's worth of progress, reported as the warm-up advances. */
663
+ interface IWarmUpProgress {
664
+ /** Distinct pipelines warmed so far. */
665
+ readonly done: number;
666
+ /** Distinct pipelines the warm-up will build in total. Known before the first slice. */
667
+ readonly total: number;
668
+ }
669
+ interface IWarmUpOptions {
670
+ /** Compute kernels to compile in the same bounded startup window as draw pipelines. */
671
+ readonly computeNodes?: readonly unknown[];
672
+ /**
673
+ * Distinct pipelines compiled between yields. Default 24.
674
+ *
675
+ * The trade is presented frames against total warm-up time: every yield costs one frame's
676
+ * present, and a slice of one would spend more time presenting than compiling. 24 puts a frame
677
+ * on the screen roughly every 30 objects' worth of work while adding a handful of presents to
678
+ * the launch.
679
+ */
680
+ readonly sliceSize?: number;
681
+ /** Called after each slice. Never called with `done` greater than `total`. */
682
+ readonly onProgress?: (progress: IWarmUpProgress) => void;
683
+ /**
684
+ * How the warm-up lets the host present between slices. Defaults to yielding one macrotask,
685
+ * which is one native-runtime loop iteration and therefore one presented frame. A caller that
686
+ * knows its host wants a different signal passes one; a test passes a resolved promise.
687
+ */
688
+ readonly yieldFrame?: () => Promise<void>;
689
+ /**
690
+ * How long one pipeline may take before the warm-up gives up on it. Default 2000 ms.
691
+ *
692
+ * A real compile on a Pixel 8 measured ~77 ms; two seconds is a compile that is not coming back.
693
+ */
694
+ readonly compileTimeoutMs?: number;
695
+ /**
696
+ * How long the whole warm-up may take before it stops and lets the game start. Default 15000 ms.
697
+ *
698
+ * The launch must never be *worse* for having tried to optimize it.
699
+ */
700
+ readonly budgetMs?: number;
701
+ /**
702
+ * How the scene is handed to the renderer. Default `"scene"`.
703
+ *
704
+ * `"scene"` makes one `compileAsync(scene, camera)` call, which is the shape `three` is built
705
+ * for and the only one measured to be affordable: on a Pixel 8 the renderer builds all 107 of
706
+ * this game's pipelines in **8.1 s** that way.
707
+ *
708
+ * `"object"` walks one representative per pipeline and yields between slices, which is the only
709
+ * way to show progress — but it was measured at **more than 2 s per call** on the same device,
710
+ * so warming the same scene would take minutes. It is kept, and kept off, because a scene with
711
+ * few pipelines can afford it and a progress bar is worth something there; the default may not
712
+ * be a mechanism that turns a 8 s launch into a 15 s one.
713
+ */
714
+ readonly granularity?: "scene" | "object";
715
+ }
716
+ /** What the warm-up did, so a caller can report it rather than assume it. */
717
+ interface IWarmUpReport {
718
+ /** Distinct pipelines warmed — one representative object each, not one per renderable. */
719
+ readonly compiled: number;
720
+ /** Slices the work was cut into, and therefore the frames the loop got to present. */
721
+ readonly slices: number;
722
+ /** Wall-clock milliseconds the warm-up took, compiling and yielding together. */
723
+ readonly elapsedMs: number;
724
+ /**
725
+ * True when the renderer had no `compileAsync` to call — a WebGL renderer, or a stub.
726
+ *
727
+ * Reported rather than silently skipped: "the warm-up ran and found nothing to do" and "this
728
+ * renderer cannot warm up" produce the same zero, and only one of them is a reason a launch
729
+ * still stalls.
730
+ */
731
+ readonly unsupported: boolean;
732
+ /**
733
+ * Pipelines whose compile never came back inside `compileTimeoutMs`, and pipelines never
734
+ * reached because the whole warm-up ran out of budget.
735
+ *
736
+ * Reported, never thrown. A warm-up is an optimization on the launch path: the one thing it must
737
+ * never do is stop the game from starting, and the first version of this did exactly that — a
738
+ * `compileAsync` that never resolved on the device left `#boot` awaiting forever, so the loop
739
+ * stayed held, the simulation never advanced and the game sat on its loading screen. A launch
740
+ * that is slower than it could be is a disappointment; a launch that never finishes is a bug.
741
+ */
742
+ readonly abandoned: number;
743
+ /** True when the overall budget ran out before every pipeline was warmed. */
744
+ readonly timedOut: boolean;
745
+ /** Compute kernels compiled before the scene pipelines. Present only when computeNodes was set. */
746
+ readonly computeCompiled?: number;
747
+ /** Compute kernels that rejected or exceeded their bound. Present only when computeNodes was set. */
748
+ readonly computeAbandoned?: number;
749
+ /** True when the renderer exposed no computeAsync seam. Present only when computeNodes was set. */
750
+ readonly computeUnsupported?: boolean;
751
+ /** True when compute warm-up consumed the startup budget. Present only when computeNodes was set. */
752
+ readonly computeTimedOut?: boolean;
753
+ }
754
+ /** The narrow slice of the renderer this needs. Structural so a test needs no renderer. */
755
+ interface IWarmUpRenderer {
756
+ compileAsync?: (scene: Object3D, camera: Camera, targetScene?: Object3D) => Promise<void>;
757
+ computeAsync?: (node: unknown) => Promise<void>;
758
+ raw?: unknown;
759
+ }
760
+ /**
761
+ * Warms up `scene` for `camera`, in slices, presenting a frame between each.
762
+ *
763
+ * Fail closed on a nonsensical slice size rather than quietly choosing one: a zero or negative
764
+ * slice would loop forever, and a caller that passed it has a bug worth seeing now.
765
+ * @situation compile a scene's shaders during the loading screen instead of on the first frame
766
+ * @situation stop a native launch freezing for seconds inside its first rendered frame
767
+ * @example await warmUpScene(renderer, scene, camera, { onProgress: (p) => setLoading(p) });
768
+ */
769
+ declare function warmUpScene(renderer: IWarmUpRenderer, scene: Object3D, camera: Camera, options?: IWarmUpOptions): Promise<IWarmUpReport>;
770
+
771
+ type PluginCleanup = () => void;
772
+ interface IGameObservationSampleRequest {
773
+ readonly entities?: readonly string[];
774
+ readonly include?: readonly string[];
775
+ readonly label?: string;
776
+ readonly resources?: readonly string[];
777
+ }
778
+ interface IGameObservationContribution {
779
+ readonly capabilities: readonly string[];
780
+ readonly sample: (request: IGameObservationSampleRequest) => Readonly<Record<string, unknown>>;
781
+ }
782
+ interface IGameRuntimeObservations {
783
+ contribute(contribution: IGameObservationContribution): PluginCleanup;
784
+ contributions(): readonly IGameObservationContribution[];
785
+ }
786
+ interface IGamePluginRuntime {
787
+ readonly fixedStep: (ticks: number) => number;
788
+ /**
789
+ * Announces that a diagnostics consumer is going to read render metrics, turning per-frame
790
+ * sample collection on for the rest of the run. Optional: a runtime without collection
791
+ * support just never enables. Games collect nothing until this fires — the samples exist for
792
+ * assertions, not for every frame of every game.
793
+ */
794
+ readonly enableRuntimeDiagnostics?: () => void;
795
+ /** The frame's cost attribution so far, or undefined when the game turned the budget off. */
796
+ readonly frameBudgetWindow?: () => IFrameBudgetWindow | undefined;
797
+ /**
798
+ * Hold the frame loop until `gate` settles, after the start scene has entered.
799
+ *
800
+ * A plugin that blocks its own `setup` blocks it too early: entity-derived capabilities are
801
+ * registered by the scene, which runs after plugin setup, so a runner reading `describe()`
802
+ * during that hold sees a description missing them. Handing the gate here instead holds the
803
+ * loop at the last possible moment — everything is registered, nothing has stepped.
804
+ */
805
+ readonly holdStart?: (gate: Promise<void>) => void;
806
+ readonly observations: IGameRuntimeObservations;
807
+ readonly tick: () => number;
808
+ readonly runtimeDiagnosticsSeries?: () => readonly IRenderPerformanceSample[];
809
+ readonly random?: Pick<IRandom, "state">;
810
+ rapier?: string | null;
811
+ readonly seed: number | null;
812
+ /**
813
+ * Whether first-use compilation has settled, separately from full readiness.
814
+ *
815
+ * Readiness also requires a sustained in-budget frame window, which is a player-experience
816
+ * gate — a CPU rasteriser never meets it and resolves on the window's own timeout instead. A
817
+ * harness that has been told the machine has no GPU needs the earlier, cheaper signal, and it
818
+ * must be reported rather than inferred from a phase that cannot distinguish the two.
819
+ */
820
+ readonly startupCompileSettled?: () => boolean;
821
+ readonly step: number;
822
+ }
823
+ interface IGamePlatformSource {
824
+ readonly devToolsHost?: Record<string, unknown>;
825
+ readonly input: NonNullable<ConstructorParameters<typeof InputMap>[3]>;
826
+ readonly inputTarget?: EventTarget;
827
+ readonly renderer: NonNullable<IRendererOptions["source"]>;
828
+ readonly viewport: NonNullable<IViewportOptions["source"]>;
829
+ mountCanvas(canvas: HTMLCanvasElement, container?: HTMLElement): void;
830
+ unmountCanvas(canvas: HTMLCanvasElement): void;
831
+ }
832
+ type GamePluginFunction<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: ICtx<TState, TPhysics>) => undefined | PluginCleanup;
833
+ interface IGamePluginHooks<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
834
+ setup?(ctx: ICtx<TState, TPhysics>, runtime?: IGamePluginRuntime): undefined | PluginCleanup | Promise<undefined | PluginCleanup>;
835
+ beforeUpdate?(ctx: ICtx<TState, TPhysics>, dt: number): void;
836
+ update?(ctx: ICtx<TState, TPhysics>, dt: number): void;
837
+ sceneExit?(ctx: ICtx<TState, TPhysics>): void;
838
+ dispose?(ctx: ICtx<TState, TPhysics>): void;
839
+ }
840
+ type GamePlugin<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = GamePluginFunction<TState, TPhysics> | IGamePluginHooks<TState, TPhysics>;
841
+ interface IGameConfig<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
842
+ readonly assets?: IAssetLoaderOptions;
843
+ readonly camera?: CameraConfig;
844
+ readonly canvas?: HTMLCanvasElement;
845
+ readonly container?: HTMLElement;
846
+ readonly input?: InputBindings;
847
+ /**
848
+ * Browser context menu over the game surface. Defaults to `"suppress"`, which is what a game
849
+ * wants: right-click is a binding, not a menu. Set `"allow"` only if your game genuinely needs
850
+ * the browser menu over its canvas.
851
+ */
852
+ readonly contextMenu?: ContextMenuPolicy;
853
+ /**
854
+ * Per-frame cost attribution, on by default. Every `reportEvery` presented frames the game
855
+ * prints one `TN_FRAME_BUDGET` line naming where the frame went — present wait, simulation,
856
+ * three.js render, overlay, the rest — which is what a device lane reads instead of guessing.
857
+ * Pass `false` to silence the marker; the same numbers still reach a playtest `performance`
858
+ * assertion, because turning a convention off must not turn its measurement off.
859
+ */
860
+ readonly frameBudget?: IFrameBudgetOptions | false;
861
+ readonly initialState?: TState;
862
+ /**
863
+ * Shader warm-up before the first frame. **Off by default, on the evidence below.**
864
+ *
865
+ * Every distinct pipeline is otherwise built the first time something using it is drawn, inside
866
+ * the first rendered frame of a fully built scene. On a Pixel 8 that frame lasted **12.0 s, of
867
+ * which 8.0 s was 105 pipeline compiles** — a launch the player reads as a hang, because the
868
+ * loop presents nothing for the whole span. Warming those pipelines while the loading screen is
869
+ * up is the obvious fix, and it is the one this option exists for.
870
+ *
871
+ * It remains opt-in for callers that want to pay this cost before `start()` releases the held
872
+ * loop. On the native host it currently cannot complete: **`renderer.compileAsync()` never
873
+ * resolves there**. Measured on the same device, both granularities were abandoned by their own
874
+ * budget having compiled nothing —
875
+ * `TN_WARMUP:{"compiled":0,"abandoned":1,"timedOut":true,"elapsedMs":15325}` for one
876
+ * whole-scene call, and 6 of 490 in 15 s for the per-object walk — while the first frame
877
+ * compiled the identical pipelines synchronously in 8.0 s. Turning this on today buys nothing
878
+ * and spends the budget waiting, so the default may not be on until the host resolves that
879
+ * promise; the seam is the async-pipeline shim in `runtime-native`'s WebGPU bindings.
880
+ *
881
+ * Set it to `{}` or an options object to enable it — on web, where `compileAsync` does resolve,
882
+ * it does what it says. The default loading layer has its own bounded post-enter readiness gate,
883
+ * so setting this option is not required for a loading screen to cover first-use work. Either
884
+ * way `TN_WARMUP` reports what happened, because turning a convention off must not turn its
885
+ * measurement off.
886
+ */
887
+ readonly warmUp?: IWarmUpOptions | false | true;
888
+ readonly inputTarget?: EventTarget;
889
+ /**
890
+ * Maximum simulation steps per rendered frame. Default 5. Caps the catch-up burst after a
891
+ * stall so a slow frame cannot cascade into a spiral of longer frames.
892
+ */
893
+ readonly maxSteps?: number;
894
+ readonly platform?: IGamePlatformSource;
895
+ readonly plugins?: readonly GamePlugin<TState, TPhysics>[];
896
+ /**
897
+ * The project's `display` block, passed straight through from `threenative.config.ts`. The
898
+ * adaptive scale holds `maxFps` as its budget, so a game that does not pass this gets the
899
+ * 60 fps default rather than a scaler with no target.
900
+ */
901
+ readonly display?: NonNullable<IThreeNativeConfig["display"]>;
902
+ readonly render?: NonNullable<IThreeNativeConfig["renderer"]>;
903
+ readonly renderer?: IRendererOptions;
904
+ readonly seed?: number;
905
+ readonly scenes: Record<string, SceneConstructor<TState, TPhysics>>;
906
+ /**
907
+ * Fixed simulation step in seconds, e.g. `1 / 60`. **This is the fixed-step knob a game
908
+ * wants**; every `update(ctx, dt)` receives exactly this `dt`, never a variable frame time,
909
+ * so gameplay and physics advance together and never see a stall.
910
+ *
911
+ * Do not write your own accumulator on top of this. Doing so runs the scene's update several
912
+ * times per already-fixed step and decouples gameplay from the simulation — a real build lost
913
+ * its largest wrong turn to exactly that, because this field carried no documentation.
914
+ */
915
+ readonly step?: number;
916
+ readonly start: string;
917
+ readonly stateFlushMs?: number;
918
+ }
919
+ interface IPerspectiveCameraConfig {
920
+ readonly projection: "perspective";
921
+ readonly fov?: number;
922
+ readonly near?: number;
923
+ readonly far?: number;
924
+ }
925
+ interface IOrthogonalCameraConfig {
926
+ readonly projection: "orthogonal";
927
+ readonly size: number;
928
+ readonly near?: number;
929
+ readonly far?: number;
930
+ }
931
+ type CameraConfig = IPerspectiveCameraConfig | IOrthogonalCameraConfig;
932
+ /**
933
+ * The game's end of the UI bridge.
934
+ *
935
+ * The UI renders through the platform's own browser-class renderer, which on every native
936
+ * target is a different realm from this one — so it holds a mirror of the game's published
937
+ * state and sends intents back rather than calling into the game. The web target uses the same
938
+ * two channels through an in-process broker, which is what keeps one `src/ui/` honest: a HUD
939
+ * that works here works on a phone.
940
+ *
941
+ * Publication is automatic and throttled to the store's own published cadence, and it stops
942
+ * entirely when nothing is listening — a game whose `ui.renderer` is `native` pays nothing.
943
+ */
944
+ interface IGameUi {
945
+ /**
946
+ * Whether a UI layer has announced itself.
947
+ *
948
+ * Stricter than "a transport exists": the UI sends `tn:ready` once its tree has rendered and its
949
+ * interactive rectangles are published, and only then is this true. A transport with nothing on
950
+ * the other end and a UI that failed to render look the same to a game otherwise.
951
+ */
952
+ readonly connected: boolean;
953
+ /** Handle an intent the UI sent — `restart`, `pause`, whatever the game defines. */
954
+ onIntent(listener: (intent: string, payload: unknown) => void): () => void;
955
+ /** Publish the current state now, whether or not it changed. */
956
+ publish(): void;
957
+ }
958
+ interface IGotoOptions<TState extends Record<string, unknown>> {
959
+ readonly carry?: Partial<TState>;
960
+ }
961
+ interface IGame<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
962
+ readonly ctx: ICtx<TState, TPhysics> | undefined;
963
+ readonly scene: Scene<TState, TPhysics> | undefined;
964
+ /** The name of the entered scene, or undefined before `start()`. */
965
+ readonly sceneName: string | undefined;
966
+ readonly state: GameStore<TState>;
967
+ /** The seam between the game and its UI layer, on every target. @see IGameUi */
968
+ readonly ui: IGameUi;
969
+ /** Rebuilds the requested scene from its initial state, then merges an optional carry patch. */
970
+ goto(name: string, options?: IGotoOptions<TState>): Promise<void>;
971
+ /** Boot into `name` instead of `config.start` on the next `start()`. Hot reload's restore path. */
972
+ resumeScene(name: string): void;
973
+ start(): Promise<void>;
974
+ pause(): void;
975
+ resume(): void;
976
+ stop(): void;
977
+ }
978
+ declare function defineGame<TState extends Record<string, unknown>, TPhysics = undefined>(config: IGameConfig<TState, TPhysics>): IGame<TState, TPhysics>;
979
+
980
+ export { type ITweenOptions as A, type IWarmUpOptions as B, type ContextMenuPolicy as C, type IWarmUpProgress as D, type IWarmUpRenderer as E, type IWarmUpReport as F, type InputBindings as G, type InputPlatformSource as H, type IGame as I, type PointerEvent3DType as J, PointerEvents3D as K, type SceneFrame as L, ScenePicker as M, type ScheduleHandle as N, Scheduler as O, type PointerEvent3DListener as P, type ThreeNativeOrientation as Q, type ThreeNativeUiRenderer as R, Scene as S, type ThreeNativeBackgroundMode as T, createAssetLoader as U, createRandom as V, defineGame as W, warmUpScene as X, type IGamePluginRuntime as a, type IGamePluginHooks as b, type IAssetLoader as c, type IAssetLoaderOptions as d, type ICtx as e, type IGameObservationContribution as f, type IGameObservationSampleRequest as g, type IGamePlatformSource as h, type IInputAction as i, type IInputGamepad as j, type IPointerDragHandle as k, type IPointerEvent3D as l, type IPointerEvents3D as m, type IPointerEvents3DOptions as n, type IPointerEvents3DPicker as o, type IPointerState as p, type IRandom as q, type IRawInputPointer as r, type IRawInputPointerEdge as s, type IRawInputState as t, type IRaycastOptions as u, type IScenePickerOptions as v, type IThreeNativeBootSplash as w, type IThreeNativeConfig as x, type IThreeNativeIconVariants as y, type IThreeNativeTexturesConfig as z };