@threenative/core 0.2.0 → 0.3.1
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/LICENSE +21 -0
- package/README.md +55 -0
- package/capabilities.json +5292 -0
- package/dist/assets-kyoF7JlJ.d.ts +103 -0
- package/dist/audio-BFiGneTL.d.ts +156 -0
- package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
- package/dist/game-XGrTzapq.d.ts +1166 -0
- package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
- package/dist/hot.d.ts +20 -2
- package/dist/hot.js +14 -2
- package/dist/index.d.ts +2108 -55
- package/dist/index.js +15645 -2222
- package/dist/net.d.ts +65 -0
- package/dist/net.js +643 -0
- package/dist/playtest.d.ts +37 -4
- package/dist/playtest.js +246 -546
- package/dist/react.d.ts +177 -0
- package/dist/react.js +635 -0
- package/dist/renderer-C6hqZpoG.d.ts +770 -0
- package/dist/ui-layer.d.ts +306 -0
- package/dist/ui-layer.js +425 -0
- package/dist/world.d.ts +254 -0
- package/dist/world.js +2686 -0
- package/gpl/LICENSE.GPL +117 -0
- package/gpl/convert.py +192 -0
- package/gpl/recipes/_common.py +169 -0
- package/gpl/recipes/bake_ao.py +111 -0
- package/gpl/recipes/decimate.py +64 -0
- package/gpl/recipes/retarget.py +131 -0
- package/gpl/recipes/unwrap.py +71 -0
- package/mcp/assets.mjs +5 -0
- package/mcp/blender-server.mjs +632 -0
- package/mcp/blender.mjs +27 -0
- package/mcp/engine-server.mjs +501 -0
- package/mcp/engine.mjs +31 -0
- package/mcp/install.d.mts +37 -0
- package/mcp/install.mjs +145 -0
- package/mcp/launch.mjs +72 -0
- package/mcp/sculpt.mjs +5 -0
- package/mcp/servers.d.mts +34 -0
- package/mcp/servers.mjs +160 -0
- package/package.json +76 -6
- package/patches/three@0.185.1.patch +522 -0
- package/scripts/apply-three-patch.mjs +297 -0
- package/scripts/ensure-mcp.mjs +43 -0
- package/scripts/postinstall.mjs +6 -0
- package/dist/audio-CEAw0w5y.d.ts +0 -35
- package/dist/game-DRt1Qhq3.d.ts +0 -429
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
import { I as IAssetLoader, a as IAssetLoaderOptions } from './assets-kyoF7JlJ.js';
|
|
2
|
+
import { h as IFramePhaseSample, i as IPipelineCensus, I as IRendererLike, a6 as IRendererOptions, g as IFrameBudgetWindow, e as IFrameBudgetOptions } from './renderer-C6hqZpoG.js';
|
|
3
|
+
import { Vector2, Vector3, Object3D, Camera, Intersection, Scene as Scene$1 } from 'three';
|
|
4
|
+
import { V as Viewport, C as CanvasLayer, I as IViewportOptions } from './canvas-layer-BLVijiUJ.js';
|
|
5
|
+
import { StoreApi } from 'zustand/vanilla';
|
|
6
|
+
|
|
7
|
+
type ThreeNativeOrientation = "landscape" | "portrait" | "sensor";
|
|
8
|
+
/** Which renderer draws a game's `src/ui/`. @see IThreeNativeConfig.ui */
|
|
9
|
+
type ThreeNativeUiRenderer = "native" | "web";
|
|
10
|
+
/** What the native host does with the render loop while the app is off-screen. */
|
|
11
|
+
type ThreeNativeBackgroundMode = "continue" | "pause";
|
|
12
|
+
interface IThreeNativeIconVariants {
|
|
13
|
+
readonly android?: {
|
|
14
|
+
readonly foreground?: string;
|
|
15
|
+
readonly background?: string;
|
|
16
|
+
readonly monochrome?: string;
|
|
17
|
+
};
|
|
18
|
+
readonly ios?: {
|
|
19
|
+
readonly dark?: string;
|
|
20
|
+
readonly tinted?: string;
|
|
21
|
+
};
|
|
22
|
+
readonly web?: {
|
|
23
|
+
readonly favicon?: string;
|
|
24
|
+
readonly maskable?: string;
|
|
25
|
+
readonly monochrome?: string;
|
|
26
|
+
readonly appleTouch?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface IThreeNativeBootSplash {
|
|
30
|
+
readonly backgroundColor?: string;
|
|
31
|
+
readonly image?: string;
|
|
32
|
+
}
|
|
33
|
+
/** What a clip is for, stated as a bound on where its energy sits, in the inspector's bands. */
|
|
34
|
+
interface IThreeNativeAudioSpectrum {
|
|
35
|
+
readonly band: "air" | "high" | "low" | "mid" | "sub";
|
|
36
|
+
/** Most of the clip's energy that may sit in the band. */
|
|
37
|
+
readonly maxPercent?: number;
|
|
38
|
+
/** Least of the clip's energy that must sit in the band. */
|
|
39
|
+
readonly minPercent?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Loop conditioning for a clip that repeats forever; its seam is cross-faded, then asserted. */
|
|
42
|
+
interface IThreeNativeAudioLoop {
|
|
43
|
+
/** Equal-power cross-fade in milliseconds. `0` keeps the clip's own length and still asserts. */
|
|
44
|
+
readonly crossFadeMs?: number;
|
|
45
|
+
/** How far the splice may move to find a quiet seam, in milliseconds. */
|
|
46
|
+
readonly spliceToleranceMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Per-glob audio declarations. Which clips loop, which are positional, and what a clip is for are
|
|
50
|
+
* facts only the game knows, so they are declared and never inferred from a filename.
|
|
51
|
+
*/
|
|
52
|
+
interface IThreeNativeAudioOverride {
|
|
53
|
+
/** `"none"` ships these bytes as committed; measurement and a declared loop's assertion run. */
|
|
54
|
+
readonly conditioning?: "none";
|
|
55
|
+
/** First matching override wins; matched against the logical path, e.g. `"audio/bed.ogg"`. */
|
|
56
|
+
readonly glob: string;
|
|
57
|
+
readonly loop?: boolean | IThreeNativeAudioLoop;
|
|
58
|
+
readonly normalise?: "ceiling" | "peak";
|
|
59
|
+
/** Peak ceiling in dBFS, between -60 and 0. */
|
|
60
|
+
readonly peakDb?: number;
|
|
61
|
+
/** The clip plays from a place in the world, so it is downmixed to mono. */
|
|
62
|
+
readonly positional?: boolean;
|
|
63
|
+
/** Vorbis VBR quality, -1 to 10. */
|
|
64
|
+
readonly quality?: number;
|
|
65
|
+
/** The largest wrap-to-neighbourhood step ratio a declared loop may ship with, 1 to 5. */
|
|
66
|
+
readonly seamMaxRatio?: number;
|
|
67
|
+
readonly spectrum?: IThreeNativeAudioSpectrum;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Audio conditioning options for the asset compile step; `"none"` ships clips verbatim.
|
|
71
|
+
*
|
|
72
|
+
* `"ceiling"` normalisation only ever attenuates, keeping the game's relative mix; `"peak"` also
|
|
73
|
+
* lifts a quiet clip to the ceiling.
|
|
74
|
+
*/
|
|
75
|
+
interface IThreeNativeAudioConfig {
|
|
76
|
+
readonly normalise?: "ceiling" | "peak";
|
|
77
|
+
readonly overrides?: readonly IThreeNativeAudioOverride[];
|
|
78
|
+
readonly peakDb?: number;
|
|
79
|
+
readonly quality?: number;
|
|
80
|
+
readonly seamMaxRatio?: number;
|
|
81
|
+
}
|
|
82
|
+
/** Texture compression options for the asset compile step; `"none"` ships sources verbatim. */
|
|
83
|
+
interface IThreeNativeTexturesConfig {
|
|
84
|
+
/** Integer at least 4; caps the longest edge, preserving aspect and 4x4 alignment; never upscales. */
|
|
85
|
+
readonly maxSize?: number;
|
|
86
|
+
readonly overrides?: readonly {
|
|
87
|
+
readonly codec: "etc1s" | "none" | "uastc";
|
|
88
|
+
readonly glob: string;
|
|
89
|
+
readonly quality?: number;
|
|
90
|
+
}[];
|
|
91
|
+
/** ETC1S encoder quality 1–255. Ignored for UASTC. */
|
|
92
|
+
readonly quality?: number;
|
|
93
|
+
}
|
|
94
|
+
/** Model optimization sub-pass switches; absent means every pass runs. */
|
|
95
|
+
interface IThreeNativeModelPassesConfig {
|
|
96
|
+
readonly dedup?: boolean;
|
|
97
|
+
readonly meshopt?: boolean;
|
|
98
|
+
readonly prune?: boolean;
|
|
99
|
+
readonly quantize?: boolean;
|
|
100
|
+
readonly reorder?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/** Model optimization options for the asset compile step; `"none"` ships sources verbatim. */
|
|
103
|
+
interface IThreeNativeModelsConfig {
|
|
104
|
+
/** Standard glTF TEXCOORD_1 atlas generation for offline static-light assets. */
|
|
105
|
+
readonly lightmap?: {
|
|
106
|
+
readonly atlasSize: number;
|
|
107
|
+
readonly padding: number;
|
|
108
|
+
};
|
|
109
|
+
readonly passes?: IThreeNativeModelPassesConfig;
|
|
110
|
+
readonly quantize?: {
|
|
111
|
+
readonly normalBits?: number;
|
|
112
|
+
readonly positionBits?: number;
|
|
113
|
+
readonly uvBits?: number;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Write each distinct embedded image once under `shared/images/` and reference it from every
|
|
117
|
+
* model that carries it. A marketplace pack whose eight pines all embed the same bark map then
|
|
118
|
+
* ships and encodes it once. Default true; false embeds and re-encodes duplicate images in
|
|
119
|
+
* each model. The served GLB references files beside it.
|
|
120
|
+
*/
|
|
121
|
+
readonly sharedImages?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Embedded-texture compression for images carried inside a `.glb`.
|
|
124
|
+
*
|
|
125
|
+
* On by default in the compile step; `"none"` ships every embedded image exactly as
|
|
126
|
+
* authored. `maxSize` caps the longest edge, preserving aspect and snapping to whole 4x4
|
|
127
|
+
* blocks, and never upscales.
|
|
128
|
+
*/
|
|
129
|
+
readonly textures?: "none" | {
|
|
130
|
+
readonly maxSize?: number;
|
|
131
|
+
readonly quality?: number;
|
|
132
|
+
readonly overrides?: readonly {
|
|
133
|
+
readonly slot: string;
|
|
134
|
+
readonly codec: "etc1s" | "none" | "uastc";
|
|
135
|
+
}[];
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Mesh simplification. Absent means none at all, which is the default.
|
|
139
|
+
*
|
|
140
|
+
* `ratio` is the fraction of triangles to keep. `error` is a quality guard rather than a
|
|
141
|
+
* target — the largest a vertex may move as a fraction of the mesh's extent — so a loose
|
|
142
|
+
* ratio with a tight error stops short, and the compile step reports the ratio it actually
|
|
143
|
+
* achieved next to the one that was asked for.
|
|
144
|
+
*/
|
|
145
|
+
readonly simplify?: {
|
|
146
|
+
readonly ratio: number;
|
|
147
|
+
readonly error?: number;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Cluster-DAG bake for virtual geometry, or `"none"` to ship every primitive as authored.
|
|
151
|
+
*
|
|
152
|
+
* Absent means on with defaults: any primitive of 65,536 triangles or more bakes to a cluster
|
|
153
|
+
* DAG the loader turns into a `ClusteredMesh`, and everything below that line compiles
|
|
154
|
+
* byte-identically. The payload costs roughly 3-4x the primitive's compiled bytes, which is
|
|
155
|
+
* what `"none"` and `minSourceTriangles` are for.
|
|
156
|
+
*/
|
|
157
|
+
readonly virtual?: "none" | {
|
|
158
|
+
/** Clusters folded together per group, default 4. */
|
|
159
|
+
readonly groupSize?: number;
|
|
160
|
+
/** Upper bound on a cluster's triangles, default 128. */
|
|
161
|
+
readonly maxTriangles?: number;
|
|
162
|
+
/** Lower bound on a cluster's triangles, default 96. */
|
|
163
|
+
readonly minTriangles?: number;
|
|
164
|
+
/** Primitives below this many triangles are left alone, default 65,536. */
|
|
165
|
+
readonly minSourceTriangles?: number;
|
|
166
|
+
/** Fraction of a group's triangles kept per level, default 0.5. */
|
|
167
|
+
readonly simplifyRatio?: number;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
interface IThreeNativeConfig {
|
|
171
|
+
readonly app?: {
|
|
172
|
+
readonly id?: string;
|
|
173
|
+
readonly name?: string;
|
|
174
|
+
readonly version?: string;
|
|
175
|
+
readonly build?: number;
|
|
176
|
+
readonly icon?: string;
|
|
177
|
+
readonly icons?: IThreeNativeIconVariants;
|
|
178
|
+
};
|
|
179
|
+
readonly display?: {
|
|
180
|
+
readonly orientation?: ThreeNativeOrientation;
|
|
181
|
+
readonly fullscreen?: boolean;
|
|
182
|
+
readonly keepScreenOn?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Maximum native presentation rate in frames per second. Defaults to 60; `0` removes the
|
|
185
|
+
* software ceiling. Android also submits this value as the surface's preferred frame rate,
|
|
186
|
+
* which the display policy may decline because of hardware, power, or thermal state. Android
|
|
187
|
+
* uses non-blocking presentation above 60 fps so a missed high-refresh interval does not fall
|
|
188
|
+
* to an integer refresh-rate divisor.
|
|
189
|
+
*/
|
|
190
|
+
readonly maxFps?: number;
|
|
191
|
+
/**
|
|
192
|
+
* What the native host does when the player leaves the app — presses the power button,
|
|
193
|
+
* switches away, minimizes the window. `"pause"` (the default) stops running frames and
|
|
194
|
+
* suspends audio until the app comes back; `"continue"` keeps rendering off-screen, which a
|
|
195
|
+
* server-shaped or split-screen game may genuinely want.
|
|
196
|
+
*
|
|
197
|
+
* Turning the pause off does not turn the reporting off: `TN_LIFECYCLE` markers are emitted
|
|
198
|
+
* either way and name the mode that executed.
|
|
199
|
+
*/
|
|
200
|
+
readonly backgroundMode?: ThreeNativeBackgroundMode;
|
|
201
|
+
};
|
|
202
|
+
readonly window?: {
|
|
203
|
+
readonly title?: string;
|
|
204
|
+
readonly width?: number;
|
|
205
|
+
readonly height?: number;
|
|
206
|
+
/** Start maximized on desktop when `display.fullscreen` is false. */
|
|
207
|
+
readonly maximized?: boolean;
|
|
208
|
+
readonly resizable?: boolean;
|
|
209
|
+
};
|
|
210
|
+
readonly assets?: {
|
|
211
|
+
/**
|
|
212
|
+
* Audio conditioning options, or `"none"` to ship every clip exactly as committed. Absent
|
|
213
|
+
* means conditioning runs with defaults. `"none"` still measures and still reports.
|
|
214
|
+
*/
|
|
215
|
+
readonly audio?: "none" | IThreeNativeAudioConfig;
|
|
216
|
+
/**
|
|
217
|
+
* Byte ceilings: uncooked defaults to 64,000,000; total defaults to "none". Mobile
|
|
218
|
+
* targets without decoders are exempt from uncooked. A number sets uncooked; "none"
|
|
219
|
+
* disables both gates. Measurements still print when a gate is disabled.
|
|
220
|
+
*/
|
|
221
|
+
readonly budget?: number | "none" | {
|
|
222
|
+
readonly uncooked?: number | "none";
|
|
223
|
+
readonly total?: number | "none";
|
|
224
|
+
};
|
|
225
|
+
/** Source-relative globs omitted from builds; excluded bytes are still reported. */
|
|
226
|
+
readonly exclude?: readonly string[];
|
|
227
|
+
readonly models?: "none" | IThreeNativeModelsConfig;
|
|
228
|
+
readonly output?: string;
|
|
229
|
+
readonly source?: string;
|
|
230
|
+
readonly targets?: {
|
|
231
|
+
readonly maxMaterials?: number;
|
|
232
|
+
readonly maxTriangles?: number;
|
|
233
|
+
readonly maxTextureDimension?: number;
|
|
234
|
+
};
|
|
235
|
+
/** Texture compression options, or `"none"` to ship every texture exactly as committed. */
|
|
236
|
+
readonly textures?: "none" | IThreeNativeTexturesConfig;
|
|
237
|
+
};
|
|
238
|
+
readonly bootSplash?: IThreeNativeBootSplash;
|
|
239
|
+
readonly nativeEntry?: string;
|
|
240
|
+
readonly renderer?: {
|
|
241
|
+
readonly preferWebGPU?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Portable drawing-buffer scale. CSS and UI layout dimensions are unchanged.
|
|
244
|
+
*
|
|
245
|
+
* `"auto"` — the default a template ships — lets the engine hold the `display.maxFps` budget
|
|
246
|
+
* without the game hand-authoring a resolution constant. A number in `(0, 1]` pins it and
|
|
247
|
+
* turns the loop off. Either way the active scale is reported in every `TN_FRAME_BUDGET`
|
|
248
|
+
* window: turning the convention off does not turn its measurement off.
|
|
249
|
+
*/
|
|
250
|
+
readonly resolutionScale?: number | "auto";
|
|
251
|
+
/** Portable multisampling. Sampling and resolution are one pixel budget, not two. */
|
|
252
|
+
readonly antialias?: boolean;
|
|
253
|
+
/**
|
|
254
|
+
* Portable alpha antialiasing: resolve alpha-tested cutout silhouettes — foliage, fences,
|
|
255
|
+
* hair — through the multisample coverage mask rather than a binary `discard`. On by default,
|
|
256
|
+
* costing no target and no pass; it buys the cutouts the samples `antialias` already pays
|
|
257
|
+
* for. Set false for a deliberately hard-edged look. It does nothing on a single-sampled
|
|
258
|
+
* surface, and says so in `TN_ALPHA_ANTIALIASING` rather than reporting itself applied.
|
|
259
|
+
*/
|
|
260
|
+
readonly alphaAntialiasing?: boolean;
|
|
261
|
+
/**
|
|
262
|
+
* Android-only rendering overrides selected by the engine.
|
|
263
|
+
*
|
|
264
|
+
* `antialias` belongs here beside `resolutionScale` because they spend the same budget: a
|
|
265
|
+
* tile-based mobile GPU resolves MSAA in tile memory and prices it quite differently from a
|
|
266
|
+
* desktop one, so a platform that scales resolution down must be able to buy sampling back
|
|
267
|
+
* on that same platform rather than accepting whatever the portable value happened to be.
|
|
268
|
+
*/
|
|
269
|
+
readonly android?: {
|
|
270
|
+
readonly resolutionScale?: number | "auto";
|
|
271
|
+
readonly antialias?: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* `alphaAntialiasing` belongs here for the reason `antialias` does, one step on: a phone
|
|
274
|
+
* that bought sampling back must be able to spend it on cutouts, and one that gave sampling
|
|
275
|
+
* up has nothing to spend.
|
|
276
|
+
*/
|
|
277
|
+
readonly alphaAntialiasing?: boolean;
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
readonly ui?: {
|
|
281
|
+
/**
|
|
282
|
+
* Which renderer draws `src/ui/`.
|
|
283
|
+
*
|
|
284
|
+
* `"web"` runs the same React DOM, Tailwind, CSS, SVG and fonts on every target, through
|
|
285
|
+
* that platform's own browser-class renderer composited over the game surface. What is
|
|
286
|
+
* guaranteed is source parity — one `src/ui/` — not browser-binary parity, which no design
|
|
287
|
+
* using the platforms' own engines can offer once iOS is in the set.
|
|
288
|
+
*
|
|
289
|
+
* `"native"` maps React to `CanvasLayer` quads with no web view, no CSS and no second
|
|
290
|
+
* process. Choose it for a UI that is part of the rendered frame, or a target with no web
|
|
291
|
+
* view, or zero extra processes — and own the appearance difference, which is the trade
|
|
292
|
+
* being made rather than something to discover in a screenshot.
|
|
293
|
+
*
|
|
294
|
+
* Which surface `"web"` lands on is the platform's business and never a game's: no config,
|
|
295
|
+
* type or document names the engine underneath.
|
|
296
|
+
*/
|
|
297
|
+
readonly renderer?: ThreeNativeUiRenderer;
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* One action, read either as a button through `pressed`/`justPressed`, a 2D axis through
|
|
303
|
+
* `vector`, or a scalar axis through `axis`. Which one you get depends on the fields you fill in,
|
|
304
|
+
* and mixing the two is what makes bindings confusing to read:
|
|
305
|
+
*
|
|
306
|
+
* ```ts
|
|
307
|
+
* jump: { keys: ["Space"], buttons: [0] } // a button: key or *gamepad* button 0
|
|
308
|
+
* fire: { keys: ["Space"], mouseButtons: [0] } // a button: key or *left mouse*
|
|
309
|
+
* move: { up: ["KeyW"], down: ["KeyS"], left: [...], right: [...] } // an axis
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* **`buttons` is the gamepad, `mouseButtons` is the mouse.** They are separate devices and
|
|
313
|
+
* `buttons: [0]` on a machine with no gamepad plugged in silently never fires.
|
|
314
|
+
*
|
|
315
|
+
* `up`/`down`/`left`/`right` are **directions of an axis**, not "the keys that press this".
|
|
316
|
+
* They are named after the vector they build, so `pressed("move")` is true whenever any
|
|
317
|
+
* direction is held — including `ArrowDown`. For a button, use `keys`.
|
|
318
|
+
*/
|
|
319
|
+
interface IInputAction {
|
|
320
|
+
/** **Gamepad** button indices that press this action. For the mouse, see `mouseButtons`. */
|
|
321
|
+
readonly buttons?: readonly number[];
|
|
322
|
+
/** **Gamepad** axis indices whose values contribute to `axis(name)`. */
|
|
323
|
+
readonly gamepadAxes?: readonly number[];
|
|
324
|
+
/**
|
|
325
|
+
* **Mouse** button indices that press this action, numbered as `MouseEvent.button`:
|
|
326
|
+
* `0` left, `1` middle, `2` right. Binding `2` also suppresses the browser context menu on
|
|
327
|
+
* the pointer target, because a right-click binding is unusable while the menu eats it.
|
|
328
|
+
*/
|
|
329
|
+
readonly mouseButtons?: readonly number[];
|
|
330
|
+
/**
|
|
331
|
+
* Keyboard codes that press this action. Use this for a button — `jump`, `restart`, `fire`.
|
|
332
|
+
* A key listed here never contributes to `vector`.
|
|
333
|
+
*/
|
|
334
|
+
readonly keys?: readonly string[];
|
|
335
|
+
/** The **−y** direction of `vector(name)`. Not "the keys that press this action" — see `keys`. */
|
|
336
|
+
readonly down?: readonly string[];
|
|
337
|
+
/** The −x direction of `vector(name)`. */
|
|
338
|
+
readonly left?: readonly string[];
|
|
339
|
+
/** Any active pointer or touch presses this action. */
|
|
340
|
+
readonly pointer?: boolean;
|
|
341
|
+
/** Add normalized browser/native wheel motion to `axis(name)`; negative DOM deltaY (toward-user) is positive. */
|
|
342
|
+
readonly scroll?: boolean;
|
|
343
|
+
/** Add the signed two-pointer distance change to `axis(name)`; moving apart is positive. */
|
|
344
|
+
readonly pinch?: boolean;
|
|
345
|
+
/**
|
|
346
|
+
* Add raw mouse movement since the last input tick to `vector(name)`. A click on the pointer
|
|
347
|
+
* target automatically asks for pointer capture when this binding is present; call
|
|
348
|
+
* `captureMouse()` explicitly when a game needs to start capture from another named gesture.
|
|
349
|
+
*/
|
|
350
|
+
readonly pointerRelative?: boolean;
|
|
351
|
+
/** Disable the automatic click capture for a relative binding that has its own gesture. */
|
|
352
|
+
readonly captureOnClick?: boolean;
|
|
353
|
+
/** The +x direction of `vector(name)`. */
|
|
354
|
+
readonly right?: readonly string[];
|
|
355
|
+
/** The +y direction of `vector(name)`. */
|
|
356
|
+
readonly up?: readonly string[];
|
|
357
|
+
}
|
|
358
|
+
type InputBindings = Record<string, IInputAction>;
|
|
359
|
+
/**
|
|
360
|
+
* What the browser context menu does over the game surface. `"suppress"` is the default: a
|
|
361
|
+
* right-click binding cannot work while the menu opens on the same press, and a context menu
|
|
362
|
+
* over a game canvas is almost never what anyone wants. `"allow"` restores the browser default.
|
|
363
|
+
*/
|
|
364
|
+
type ContextMenuPolicy = "allow" | "suppress";
|
|
365
|
+
interface IInputGamepad {
|
|
366
|
+
readonly axes: ArrayLike<number>;
|
|
367
|
+
readonly buttons: readonly {
|
|
368
|
+
readonly pressed: boolean;
|
|
369
|
+
}[];
|
|
370
|
+
}
|
|
371
|
+
type InputPlatformSource = () => readonly (IInputGamepad | null)[];
|
|
372
|
+
interface IRawInputPointer {
|
|
373
|
+
readonly id: number;
|
|
374
|
+
buttons: number;
|
|
375
|
+
readonly position: Vector2;
|
|
376
|
+
}
|
|
377
|
+
interface IRawInputPointerEdge {
|
|
378
|
+
readonly buttons: number;
|
|
379
|
+
readonly id: number;
|
|
380
|
+
readonly position: Vector2;
|
|
381
|
+
readonly type: "cancel" | "down" | "up";
|
|
382
|
+
}
|
|
383
|
+
interface IRawInputState {
|
|
384
|
+
readonly keys: ReadonlySet<string>;
|
|
385
|
+
readonly pointer: {
|
|
386
|
+
buttons: number;
|
|
387
|
+
captured: boolean;
|
|
388
|
+
down: boolean;
|
|
389
|
+
readonly position: Vector2;
|
|
390
|
+
/** Accumulated relative mouse motion since the last `tick`, in canvas pixels. */
|
|
391
|
+
readonly relative: Vector2;
|
|
392
|
+
};
|
|
393
|
+
/** Pointer transitions captured since the previous input tick, grouped by pointer id. */
|
|
394
|
+
readonly pointerEdges: ReadonlyMap<number, readonly IRawInputPointerEdge[]>;
|
|
395
|
+
readonly pointers: ReadonlyMap<number, IRawInputPointer>;
|
|
396
|
+
readonly gamepad: {
|
|
397
|
+
axes: readonly number[];
|
|
398
|
+
buttons: readonly boolean[];
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
declare class InputMap {
|
|
402
|
+
#private;
|
|
403
|
+
readonly raw: IRawInputState;
|
|
404
|
+
constructor(bindings?: InputBindings, target?: EventTarget, pointerTarget?: EventTarget, source?: InputPlatformSource, contextMenu?: ContextMenuPolicy);
|
|
405
|
+
/**
|
|
406
|
+
* Returns a 2D action vector where +y is up. On a conventional XZ ground plane whose
|
|
407
|
+
* forward direction is -z, map `vector.y` to `-z`. This differs from Godot's
|
|
408
|
+
* `Input.get_vector`, where up is -y.
|
|
409
|
+
*/
|
|
410
|
+
vector(name: string): Vector2;
|
|
411
|
+
/**
|
|
412
|
+
* Returns a scalar action for this fixed-step tick, clamped to `[-1, 1]`.
|
|
413
|
+
*
|
|
414
|
+
* `scroll: true` normalizes wheel deltas to pixels before scaling them: one line is 16 pixels,
|
|
415
|
+
* one page is 800 pixels, and 100 pixels is one axis unit. Browser and native wheel events use
|
|
416
|
+
* the same DOM sign convention, so scrolling the wheel toward the user (negative DOM `deltaY`) is
|
|
417
|
+
* positive and scrolling away (positive DOM `deltaY`) is negative. `pinch: true` reports the relative distance change of the first two active pointers;
|
|
418
|
+
* a third pointer is ignored and a changing pair starts a new gesture without a jump.
|
|
419
|
+
*/
|
|
420
|
+
axis(name: string): number;
|
|
421
|
+
/** Request pointer capture explicitly from a named game gesture. Relative bindings request it on canvas click by default. */
|
|
422
|
+
captureMouse(): void;
|
|
423
|
+
/** Release pointer capture. A browser refusal remains an unhandled rejection. */
|
|
424
|
+
releaseMouse(): void;
|
|
425
|
+
pressed(name: string): boolean;
|
|
426
|
+
justPressed(name: string): boolean;
|
|
427
|
+
justReleased(name: string): boolean;
|
|
428
|
+
tick(): void;
|
|
429
|
+
clear(): void;
|
|
430
|
+
dispose(): void;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
type AfterPhysicsCallback = (dt: number) => void;
|
|
434
|
+
interface IAfterPhysicsContext {
|
|
435
|
+
readonly afterPhysics: (callback: AfterPhysicsCallback) => () => void;
|
|
436
|
+
}
|
|
437
|
+
/** Register work that reads transforms after every simulation step and before the frame renders. */
|
|
438
|
+
declare function afterPhysics(context: IAfterPhysicsContext, callback: AfterPhysicsCallback): () => void;
|
|
439
|
+
interface IRenderPerformanceMetrics {
|
|
440
|
+
readonly drawCalls?: number;
|
|
441
|
+
readonly triangles?: number;
|
|
442
|
+
}
|
|
443
|
+
interface IRenderPerformanceSample extends IRenderPerformanceMetrics {
|
|
444
|
+
readonly frameMs: number;
|
|
445
|
+
/** Where this frame's milliseconds went. Present whenever a frame budget is installed. */
|
|
446
|
+
readonly phases?: IFramePhaseSample;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
interface IRandom {
|
|
450
|
+
(): number;
|
|
451
|
+
pick<T>(items: readonly T[]): T;
|
|
452
|
+
range(min: number, max: number): number;
|
|
453
|
+
state: number;
|
|
454
|
+
}
|
|
455
|
+
declare function createRandom(seed?: number): IRandom;
|
|
456
|
+
|
|
457
|
+
type EntitySnapshot = Record<string, Record<string, unknown> & {
|
|
458
|
+
tags?: string[];
|
|
459
|
+
}>;
|
|
460
|
+
|
|
461
|
+
declare class Registry {
|
|
462
|
+
#private;
|
|
463
|
+
add<T extends object>(name: string, entity: T): T;
|
|
464
|
+
get<T extends object = object>(name: string): T | undefined;
|
|
465
|
+
forEach(callback: (name: string, entity: object) => void): void;
|
|
466
|
+
remove(name: string): void;
|
|
467
|
+
queueFree(target: string | object): void;
|
|
468
|
+
sweep(): void;
|
|
469
|
+
clear(): void;
|
|
470
|
+
snapshot(): EntitySnapshot;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
interface IRaycastOptions {
|
|
474
|
+
/** Screen point in canvas pixels. Mutually exclusive with `origin` and `direction`. */
|
|
475
|
+
readonly screen?: Vector2;
|
|
476
|
+
/** World-space ray origin. Requires `direction`. */
|
|
477
|
+
readonly origin?: Vector3;
|
|
478
|
+
/** World-space ray direction. The caller must provide a normalised vector. */
|
|
479
|
+
readonly direction?: Vector3;
|
|
480
|
+
/** Maximum distance. Defaults to unbounded. */
|
|
481
|
+
readonly far?: number;
|
|
482
|
+
/** What to test. Defaults to the whole scene. */
|
|
483
|
+
readonly targets?: Object3D | readonly Object3D[];
|
|
484
|
+
/** Subtrees never hit, whatever `targets` says. */
|
|
485
|
+
readonly exclude?: Object3D | readonly Object3D[];
|
|
486
|
+
}
|
|
487
|
+
interface IScenePickerOptions {
|
|
488
|
+
readonly camera: Camera;
|
|
489
|
+
readonly pointer: () => Vector2;
|
|
490
|
+
readonly scene: Object3D;
|
|
491
|
+
readonly viewport: Viewport;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Ray queries against scene geometry, accelerated by a bounding volume hierarchy that is
|
|
495
|
+
* built on first use and rebuilt when the geometry's positions change.
|
|
496
|
+
*
|
|
497
|
+
* The acceleration is an implementation detail: nothing about it reaches the caller, no
|
|
498
|
+
* `three` prototype is patched, and a game that never calls `raycast` never builds a tree.
|
|
499
|
+
* Skinned, instanced, batched and morphed meshes fall back to the stock `three` path,
|
|
500
|
+
* because a hierarchy over their rest positions would report hits in the wrong place.
|
|
501
|
+
*/
|
|
502
|
+
declare class ScenePicker {
|
|
503
|
+
#private;
|
|
504
|
+
constructor(options: IScenePickerOptions);
|
|
505
|
+
/**
|
|
506
|
+
* The closest hit, or `undefined` when the ray hits nothing.
|
|
507
|
+
*
|
|
508
|
+
* Collects at most one hit per target: the traversal runs with `firstHitOnly` so BVH-backed
|
|
509
|
+
* meshes stop at their own closest triangle (PRD-186 phase 1), and anything on the stock
|
|
510
|
+
* fallback path — instanced, skinned, morphed — is reduced to its own nearest before it joins
|
|
511
|
+
* the candidates. The winner is a running minimum, never a full sort: a game asking "what did
|
|
512
|
+
* this round hit" should not pay for every wall behind the answer.
|
|
513
|
+
*/
|
|
514
|
+
raycast(options?: IRaycastOptions): Intersection | undefined;
|
|
515
|
+
/** Every hit, sorted from the nearest to the farthest. */
|
|
516
|
+
raycastAll(options?: IRaycastOptions, target?: Intersection[]): readonly Intersection[];
|
|
517
|
+
/** Drops every cached hierarchy. The next `raycast` rebuilds what it needs. */
|
|
518
|
+
dispose(): void;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
declare const POINTER_EVENT_TYPES: readonly ["pointerEntered", "pointerExited", "pointerPressed", "pointerReleased", "tapped", "dragStarted", "dragged", "dragEnded"];
|
|
522
|
+
type PointerEvent3DType = (typeof POINTER_EVENT_TYPES)[number];
|
|
523
|
+
interface IPointerState {
|
|
524
|
+
readonly id: number;
|
|
525
|
+
readonly buttons: number;
|
|
526
|
+
readonly position: Vector2;
|
|
527
|
+
}
|
|
528
|
+
interface IPointerEvent3D {
|
|
529
|
+
readonly buttons: number;
|
|
530
|
+
readonly intersection: Intersection | undefined;
|
|
531
|
+
readonly object: Object3D;
|
|
532
|
+
readonly point: Vector3;
|
|
533
|
+
readonly pointerId: number;
|
|
534
|
+
readonly target: Object3D;
|
|
535
|
+
readonly type: PointerEvent3DType;
|
|
536
|
+
stopPropagation(): void;
|
|
537
|
+
}
|
|
538
|
+
type PointerEvent3DListener = (event: IPointerEvent3D) => void;
|
|
539
|
+
interface IPointerDragHandle {
|
|
540
|
+
cancel(): void;
|
|
541
|
+
}
|
|
542
|
+
interface IPointerEvents3DPicker {
|
|
543
|
+
raycastAll(options?: IRaycastOptions): readonly Intersection[];
|
|
544
|
+
}
|
|
545
|
+
interface IPointerEvents3D {
|
|
546
|
+
on(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): () => void;
|
|
547
|
+
off(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): void;
|
|
548
|
+
drag(object: Object3D): IPointerDragHandle;
|
|
549
|
+
}
|
|
550
|
+
interface IPointerEvents3DOptions {
|
|
551
|
+
/** Convert an input position into the canvas-relative pixels expected by ScenePicker. */
|
|
552
|
+
readonly screen?: (position: Vector2, target: Vector2) => Vector2;
|
|
553
|
+
}
|
|
554
|
+
type PrimaryPointer = Pick<IPointerState, "buttons" | "position">;
|
|
555
|
+
type PointerEdge = IRawInputPointerEdge;
|
|
556
|
+
type PointerEdges = ReadonlyMap<number, readonly PointerEdge[]>;
|
|
557
|
+
type PointerPicker = Pick<ScenePicker, "raycastAll"> | IPointerEvents3DPicker;
|
|
558
|
+
/**
|
|
559
|
+
* Dispatch portable pointer events from the input stream to registered Three.js objects.
|
|
560
|
+
*
|
|
561
|
+
* Listeners live in a side table, so this never patches Three.js prototypes. The picker receives
|
|
562
|
+
* one target list and one query per active pointer, while the empty registration path does no
|
|
563
|
+
* queries at all.
|
|
564
|
+
*/
|
|
565
|
+
declare class PointerEvents3D implements IPointerEvents3D {
|
|
566
|
+
#private;
|
|
567
|
+
constructor(options?: IPointerEvents3DOptions);
|
|
568
|
+
/** Listen for one event on an object. The returned function removes exactly this listener. */
|
|
569
|
+
on(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): () => void;
|
|
570
|
+
/** Remove a listener previously passed to `on`. */
|
|
571
|
+
off(object: Object3D, type: PointerEvent3DType, listener: PointerEvent3DListener): void;
|
|
572
|
+
/** Make an object capture a pointer for drag events until its handle is cancelled. */
|
|
573
|
+
drag(object: Object3D): IPointerDragHandle;
|
|
574
|
+
/**
|
|
575
|
+
* Advance every pointer by one simulation tick.
|
|
576
|
+
*
|
|
577
|
+
* `primary` is the legacy mouse position: InputMap keeps it even when no button is down, which
|
|
578
|
+
* lets hover work on web without changing the existing held-touch map. It is ignored whenever
|
|
579
|
+
* the per-id map has a live pointer.
|
|
580
|
+
*/
|
|
581
|
+
tick(pointers: ReadonlyMap<number, IPointerState>, picker: PointerPicker, primary?: PrimaryPointer, edges?: PointerEdges): void;
|
|
582
|
+
/** Remove all registrations and release all per-pointer state, usually on scene change. */
|
|
583
|
+
clear(): void;
|
|
584
|
+
dispose(): void;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
type ScheduleHandle = (() => void) & {
|
|
588
|
+
cancel(): void;
|
|
589
|
+
readonly active: boolean;
|
|
590
|
+
};
|
|
591
|
+
type TweenProperties<T extends object> = {
|
|
592
|
+
[K in keyof T]?: number;
|
|
593
|
+
};
|
|
594
|
+
interface ITweenOptions {
|
|
595
|
+
readonly ease?: (t: number) => number;
|
|
596
|
+
}
|
|
597
|
+
declare class Scheduler {
|
|
598
|
+
#private;
|
|
599
|
+
get size(): number;
|
|
600
|
+
after(delay: number, callback: () => void): ScheduleHandle;
|
|
601
|
+
every(callback: (dt: number) => void): ScheduleHandle;
|
|
602
|
+
tween<T extends object>(target: T, properties: TweenProperties<T>, duration: number, options?: ITweenOptions): Promise<void>;
|
|
603
|
+
tick(dt: number): void;
|
|
604
|
+
clear(): void;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
type StatePatch<T extends Record<string, unknown>> = Partial<T> | ((state: T) => Partial<T>);
|
|
608
|
+
type GameStore<T extends Record<string, unknown>> = StoreApi<T> & {
|
|
609
|
+
getPublishedState(): T;
|
|
610
|
+
set(patch: StatePatch<T>): void;
|
|
611
|
+
flush(): void;
|
|
612
|
+
start(): void;
|
|
613
|
+
stop(): void;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Compiles a scene's pipelines before the first frame, in slices, yielding between them.
|
|
618
|
+
*
|
|
619
|
+
* On a phone every distinct pipeline is built the first time something using it is drawn. Nothing
|
|
620
|
+
* spreads that cost: the first `render()` of a fully built scene compiles all of it, on the main
|
|
621
|
+
* loop, inside one frame. Measured on a Pixel 8, `sandbox/fps-framework` spent **24.5 seconds
|
|
622
|
+
* across 107 pipeline compiles** inside a single frame — 92 % of everything the launch-stall
|
|
623
|
+
* budget could attribute (`TN_STALL_SEGMENTS`, PRD-218). For that whole span the loop presented
|
|
624
|
+
* nothing, drained no UI messages and ran no callback, so the loading screen froze mid-animation
|
|
625
|
+
* and the game read as hung. The player's report was "the loading screen takes thirty seconds".
|
|
626
|
+
*
|
|
627
|
+
* This does not make the compiling cheaper — the same pipelines are built either way, and a claim
|
|
628
|
+
* otherwise would be a lie the next measurement catches. What it changes is **who waits and
|
|
629
|
+
* whether anything moves**: the work is cut into slices with a frame between them, so the loop
|
|
630
|
+
* presents, the loading screen animates, the UI bridge delivers, and progress is a number the game
|
|
631
|
+
* can show instead of a static label. A cost that cannot be removed must at least be visible; that
|
|
632
|
+
* is the whole of what this buys, and it is worth saying plainly.
|
|
633
|
+
*
|
|
634
|
+
* The slices are compiled through the renderer's own `compileAsync`, which is stock `three`.
|
|
635
|
+
* Nothing here mutates the scene: no reparenting, no temporary groups, no visibility flipping. A
|
|
636
|
+
* warm-up that edited the graph to compile it would be a correctness risk taken for a progress
|
|
637
|
+
* bar, and the object it handed back would not be the one the game authored.
|
|
638
|
+
*/
|
|
639
|
+
/** One renderable's worth of progress, reported as the warm-up advances. */
|
|
640
|
+
interface IWarmUpProgress {
|
|
641
|
+
/** Renderable candidates handed to the renderer so far. */
|
|
642
|
+
readonly done: number;
|
|
643
|
+
/** Renderable candidates the warm-up will hand to the renderer in total. */
|
|
644
|
+
readonly total: number;
|
|
645
|
+
}
|
|
646
|
+
/** The result of the optional persistent warm-up hint. */
|
|
647
|
+
type WarmUpCacheStatus = "disabled" | "unavailable" | "miss" | "hit" | "stored";
|
|
648
|
+
interface IWarmUpCacheOptions {
|
|
649
|
+
/**
|
|
650
|
+
* A game-owned revision for the scene's materials, shaders and renderer settings.
|
|
651
|
+
*
|
|
652
|
+
* The value is a hint that the native driver's own pipeline cache survived a relaunch; it is
|
|
653
|
+
* not a serialized WebGPU pipeline. Change it whenever those inputs change. A missing or
|
|
654
|
+
* unavailable localStorage implementation never prevents the warm-up from running.
|
|
655
|
+
*/
|
|
656
|
+
readonly key: string;
|
|
657
|
+
}
|
|
658
|
+
interface IWarmUpOptions {
|
|
659
|
+
/** Compute kernels to compile in the same bounded startup window as draw pipelines. */
|
|
660
|
+
readonly computeNodes?: readonly unknown[];
|
|
661
|
+
/**
|
|
662
|
+
* Renderable candidates handed to the renderer between yields. Default 24.
|
|
663
|
+
*
|
|
664
|
+
* The trade is presented frames against total warm-up time: every yield costs one frame's
|
|
665
|
+
* present, and a slice of one would spend more time presenting than compiling. 24 puts a frame
|
|
666
|
+
* on the screen roughly every 30 objects' worth of work while adding a handful of presents to
|
|
667
|
+
* the launch.
|
|
668
|
+
*/
|
|
669
|
+
readonly sliceSize?: number;
|
|
670
|
+
/** Called after each slice. Never called with `done` greater than `total`. */
|
|
671
|
+
readonly onProgress?: (progress: IWarmUpProgress) => void;
|
|
672
|
+
/**
|
|
673
|
+
* How the warm-up lets the host present between slices. Defaults to yielding one macrotask,
|
|
674
|
+
* which is one native-runtime loop iteration and therefore one presented frame. A caller that
|
|
675
|
+
* knows its host wants a different signal passes one; a test passes a resolved promise.
|
|
676
|
+
*/
|
|
677
|
+
readonly yieldFrame?: () => Promise<void>;
|
|
678
|
+
/**
|
|
679
|
+
* How long one pipeline may take before the warm-up gives up on it. Default 2000 ms.
|
|
680
|
+
*
|
|
681
|
+
* A real compile on a Pixel 8 measured ~77 ms; two seconds is a compile that is not coming back.
|
|
682
|
+
*/
|
|
683
|
+
readonly compileTimeoutMs?: number;
|
|
684
|
+
/**
|
|
685
|
+
* How long the whole warm-up may take before it stops and lets the game start. Default 15000 ms.
|
|
686
|
+
*
|
|
687
|
+
* The launch must never be *worse* for having tried to optimize it.
|
|
688
|
+
*/
|
|
689
|
+
readonly budgetMs?: number;
|
|
690
|
+
/**
|
|
691
|
+
* How the scene is handed to the renderer. Default `"scene"`.
|
|
692
|
+
*
|
|
693
|
+
* `"scene"` makes one `compileAsync(scene, camera)` call, which is the shape `three` is built
|
|
694
|
+
* for and the only one measured to be affordable: on a Pixel 8 the renderer builds all 107 of
|
|
695
|
+
* this game's pipelines in **8.1 s** that way.
|
|
696
|
+
*
|
|
697
|
+
* `"object"` walks every renderable candidate and yields between slices, which is the only way
|
|
698
|
+
* to show candidate progress. The renderer owns cache reuse and actual pipeline identity, so the
|
|
699
|
+
* warm-up never drops an object based on a material or geometry guess.
|
|
700
|
+
*/
|
|
701
|
+
readonly granularity?: "scene" | "object";
|
|
702
|
+
/**
|
|
703
|
+
* Remember a completed warm-up so a later launch can trust the driver's persistent cache.
|
|
704
|
+
* This is opt-in because WebGPU does not expose a portable pipeline-serialization API.
|
|
705
|
+
*/
|
|
706
|
+
readonly cache?: IWarmUpCacheOptions;
|
|
707
|
+
}
|
|
708
|
+
/** What the warm-up did, so a caller can report it rather than assume it. */
|
|
709
|
+
type WarmUpObservationStatus = "complete" | "incomplete" | "unavailable";
|
|
710
|
+
interface IWarmUpObservation {
|
|
711
|
+
/** Pipeline creations observed between the census snapshots. */
|
|
712
|
+
readonly created: number;
|
|
713
|
+
/** Pipeline creations that failed between the census snapshots. */
|
|
714
|
+
readonly failed: number;
|
|
715
|
+
/** Pipeline creations still pending at the ending census snapshot, delta-adjusted. */
|
|
716
|
+
readonly pending: number;
|
|
717
|
+
/** New shader-program identities observed between the census snapshots. */
|
|
718
|
+
readonly uniquePrograms: number;
|
|
719
|
+
/** New backend pipeline identities observed between the census snapshots. */
|
|
720
|
+
readonly uniquePipelines: number;
|
|
721
|
+
/** Whether the renderer supplied a complete observation for this warm-up. */
|
|
722
|
+
readonly status: WarmUpObservationStatus;
|
|
723
|
+
}
|
|
724
|
+
interface IWarmUpReport {
|
|
725
|
+
/** Compile calls that settled successfully; retained for compatibility with existing callers. */
|
|
726
|
+
readonly compiled: number;
|
|
727
|
+
/**
|
|
728
|
+
* @deprecated Candidate count retained under the old public name. Use `candidates` for its
|
|
729
|
+
* actual unit and `observed` for backend-created pipelines.
|
|
730
|
+
*/
|
|
731
|
+
readonly pipelines: number;
|
|
732
|
+
/** Renderable objects selected for warm-up, before backend cache identity is known. */
|
|
733
|
+
readonly candidates: number;
|
|
734
|
+
/** Number of compileAsync calls attempted, including rejected and timed-out calls. */
|
|
735
|
+
readonly attempted: number;
|
|
736
|
+
/** Backend creation counts observed between the warm-up's start and end snapshots. */
|
|
737
|
+
readonly observed: IWarmUpObservation;
|
|
738
|
+
/** Slices the work was cut into, and therefore the frames the loop got to present. */
|
|
739
|
+
readonly slices: number;
|
|
740
|
+
/** Wall-clock milliseconds the warm-up took, compiling and yielding together. */
|
|
741
|
+
readonly elapsedMs: number;
|
|
742
|
+
/**
|
|
743
|
+
* True when the renderer had no `compileAsync` to call — a WebGL renderer, or a stub.
|
|
744
|
+
*
|
|
745
|
+
* Reported rather than silently skipped: "the warm-up ran and found nothing to do" and "this
|
|
746
|
+
* renderer cannot warm up" produce the same zero, and only one of them is a reason a launch
|
|
747
|
+
* still stalls.
|
|
748
|
+
*/
|
|
749
|
+
readonly unsupported: boolean;
|
|
750
|
+
/**
|
|
751
|
+
* Pipelines whose compile never came back inside `compileTimeoutMs`, and pipelines never
|
|
752
|
+
* reached because the whole warm-up ran out of budget.
|
|
753
|
+
*
|
|
754
|
+
* Reported, never thrown. A warm-up is an optimization on the launch path: the one thing it must
|
|
755
|
+
* never do is stop the game from starting, and the first version of this did exactly that — a
|
|
756
|
+
* `compileAsync` that never resolved on the device left `#boot` awaiting forever, so the loop
|
|
757
|
+
* stayed held, the simulation never advanced and the game sat on its loading screen. A launch
|
|
758
|
+
* that is slower than it could be is a disappointment; a launch that never finishes is a bug.
|
|
759
|
+
*/
|
|
760
|
+
readonly abandoned: number;
|
|
761
|
+
/** True when the overall budget ran out before every pipeline was warmed. */
|
|
762
|
+
readonly timedOut: boolean;
|
|
763
|
+
/** Compute kernels compiled before the scene pipelines. Present only when computeNodes was set. */
|
|
764
|
+
readonly computeCompiled?: number;
|
|
765
|
+
/** Compute kernels that rejected or exceeded their bound. Present only when computeNodes was set. */
|
|
766
|
+
readonly computeAbandoned?: number;
|
|
767
|
+
/** True when the renderer exposed no computeAsync seam. Present only when computeNodes was set. */
|
|
768
|
+
readonly computeUnsupported?: boolean;
|
|
769
|
+
/** True when compute warm-up consumed the startup budget. Present only when computeNodes was set. */
|
|
770
|
+
readonly computeTimedOut?: boolean;
|
|
771
|
+
/** The optional persistent warm-up hint's outcome. */
|
|
772
|
+
readonly cache?: WarmUpCacheStatus;
|
|
773
|
+
}
|
|
774
|
+
/** The narrow slice of the renderer this needs. Structural so a test needs no renderer. */
|
|
775
|
+
interface IWarmUpRenderer {
|
|
776
|
+
compileAsync?: (scene: Object3D, camera: Camera, targetScene?: Object3D) => Promise<void>;
|
|
777
|
+
computeAsync?: (node: unknown) => Promise<void>;
|
|
778
|
+
pipelineCensus?: () => IPipelineCensus;
|
|
779
|
+
raw?: unknown;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Warms up `scene` for `camera`, in slices, presenting a frame between each.
|
|
783
|
+
*
|
|
784
|
+
* Fail closed on a nonsensical slice size rather than quietly choosing one: a zero or negative
|
|
785
|
+
* slice would loop forever, and a caller that passed it has a bug worth seeing now.
|
|
786
|
+
* @situation compile a scene's shaders during the loading screen instead of on the first frame
|
|
787
|
+
* @situation stop a native launch freezing for seconds inside its first rendered frame
|
|
788
|
+
* @example await warmUpScene(renderer, scene, camera, { onProgress: (p) => setLoading(p) });
|
|
789
|
+
*/
|
|
790
|
+
declare function warmUpScene(renderer: IWarmUpRenderer, scene: Object3D, camera: Camera, options?: IWarmUpOptions): Promise<IWarmUpReport>;
|
|
791
|
+
|
|
792
|
+
declare abstract class Scene<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
793
|
+
static readonly initialState: Record<string, unknown> | undefined;
|
|
794
|
+
load(_ctx: ICtx<TState, TPhysics>): void | Promise<void>;
|
|
795
|
+
enter(_ctx: ICtx<TState, TPhysics>): SceneEnterResult<TState, TPhysics>;
|
|
796
|
+
exit(_ctx: ICtx<TState, TPhysics>): void;
|
|
797
|
+
update(_ctx: ICtx<TState, TPhysics>, _dt: number): void;
|
|
798
|
+
render(_ctx: ICtx<TState, TPhysics>): void;
|
|
799
|
+
}
|
|
800
|
+
type SceneConstructor<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = new () => Scene<TState, TPhysics>;
|
|
801
|
+
type SceneFrame<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: ICtx<TState, TPhysics>, dt: number) => void;
|
|
802
|
+
type SceneEnterResult<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = // biome-ignore lint/suspicious/noConfusingVoidType: void preserves existing Scene.enter overrides.
|
|
803
|
+
void | SceneFrame<TState, TPhysics>;
|
|
804
|
+
/**
|
|
805
|
+
* When the framework's startup milestones happened, in milliseconds on the host's monotonic
|
|
806
|
+
* clock (`performance.now()`: since navigation on the web, since process start on native).
|
|
807
|
+
*
|
|
808
|
+
* Absent members have not happened yet. Published to the playtest bridge so a scenario asserts
|
|
809
|
+
* startup time as an observation rather than reading it off a console log.
|
|
810
|
+
*/
|
|
811
|
+
interface IStartupTimeline {
|
|
812
|
+
/** The start scene's `load()` began. */
|
|
813
|
+
readonly loadStartedMs?: number;
|
|
814
|
+
/** The start scene's `enter()` returned: the world is built and the loop can move. */
|
|
815
|
+
readonly enteredMs?: number;
|
|
816
|
+
/** First-use compilation settled or its budget expired. */
|
|
817
|
+
readonly compileSettledMs?: number;
|
|
818
|
+
/**
|
|
819
|
+
* The framework's own launch work finished: compilation settled and the frame window held.
|
|
820
|
+
*
|
|
821
|
+
* Equal to `readyMs` unless the game registered a `startup.hold()`. Kept separate so that making
|
|
822
|
+
* `readyMs` honest about the player's wait does not delete the only measurement of the
|
|
823
|
+
* framework's own cost — a game with a slow asset tier would otherwise hide a framework
|
|
824
|
+
* regression inside its own loading time.
|
|
825
|
+
*/
|
|
826
|
+
readonly frameworkReadyMs?: number;
|
|
827
|
+
/**
|
|
828
|
+
* `whenReady()` resolved: the world is safe to show, including anything the game held for.
|
|
829
|
+
*
|
|
830
|
+
* This is the number to compare against what a player experiences. It used to be the
|
|
831
|
+
* framework's own readiness and nothing else, which reported 1.5 s on a valley that took 8.8 s
|
|
832
|
+
* to appear — and `assert.startup`'s `maxReadyMs` passed on it.
|
|
833
|
+
*/
|
|
834
|
+
readonly readyMs?: number;
|
|
835
|
+
}
|
|
836
|
+
interface IStartupStatus {
|
|
837
|
+
/** The most recent warm-up accounting, when a warm-up has run. */
|
|
838
|
+
readonly warmup?: {
|
|
839
|
+
readonly attempted?: number;
|
|
840
|
+
readonly candidates?: number;
|
|
841
|
+
readonly observed?: IWarmUpObservation;
|
|
842
|
+
readonly status: WarmUpObservationStatus;
|
|
843
|
+
};
|
|
844
|
+
/**
|
|
845
|
+
* True once first-use compilation has settled — earlier than `phase === "ready"`, which also
|
|
846
|
+
* waits for a sustained in-budget frame window.
|
|
847
|
+
*
|
|
848
|
+
* This is the signal a game wants when something must not run during the launch. `phase` cannot
|
|
849
|
+
* express it: it is binary, and on a software rasteriser the frame window can only ever expire
|
|
850
|
+
* rather than be met, so a game gated on `phase` alone does nothing for tens of seconds there.
|
|
851
|
+
* Measured as a chase route of length `0.000000` against a required `6`, because the scenario
|
|
852
|
+
* ended before the window did.
|
|
853
|
+
*/
|
|
854
|
+
readonly compileSettled: boolean;
|
|
855
|
+
/**
|
|
856
|
+
* `collapsing` until first-use work and a sustained in-budget frame window complete, `ready`
|
|
857
|
+
* once the world is safe to show.
|
|
858
|
+
*/
|
|
859
|
+
readonly phase: "observing" | "collapsing" | "ready";
|
|
860
|
+
/**
|
|
861
|
+
* 0 to 1, monotonic and honest: the loader's settled/requested ratio carries the first 0.7
|
|
862
|
+
* while the start scene loads, 0.8 once the world is entered, 0.9 once first-use compilation
|
|
863
|
+
* settled, 1 when `whenReady()` resolves.
|
|
864
|
+
*/
|
|
865
|
+
readonly progress: number;
|
|
866
|
+
/** When each milestone happened; members appear as they are reached. */
|
|
867
|
+
readonly timeline: IStartupTimeline;
|
|
868
|
+
/** Resolves after first-use work, the sustained frame window, and every game `hold()`. */
|
|
869
|
+
whenReady(): Promise<void>;
|
|
870
|
+
/**
|
|
871
|
+
* Resolves after first-use work and the sustained frame window, and **before** any `hold()`.
|
|
872
|
+
*
|
|
873
|
+
* This is what a game should sequence its own launch work off. `whenReady()` cannot be used for
|
|
874
|
+
* that once the game holds startup: the hold makes readiness wait for the game's work, so work
|
|
875
|
+
* *started* from `whenReady()` waits for a gate that is waiting for it. The cycle is only broken
|
|
876
|
+
* by the hold's budget expiring, which looks exactly like a very slow asset load — measured as a
|
|
877
|
+
* valley revealing its critical tier after a 45 s stall with `trees=1`.
|
|
878
|
+
*
|
|
879
|
+
* Use this when the reason to wait is the framework's launch cost — not competing with first-use
|
|
880
|
+
* compilation, not stealing frames from the stable-frame window — which is the usual reason.
|
|
881
|
+
*
|
|
882
|
+
* @situation start the game's own asset streaming after the framework's launch work, without deadlocking the readiness gate
|
|
883
|
+
*/
|
|
884
|
+
whenFrameworkReady(): Promise<void>;
|
|
885
|
+
/**
|
|
886
|
+
* Add the game's own launch work to the readiness gate, so every framework-owned observation of
|
|
887
|
+
* startup describes the moment the player actually reached the world.
|
|
888
|
+
*
|
|
889
|
+
* For a game that streams a second asset tier after the framework is done. Without it the only
|
|
890
|
+
* options are to show a half-built world or to hold a curtain past `whenReady()`, and the second
|
|
891
|
+
* leaves `progress`, `phase`, `timeline.readyMs` and the playtest bridge's `assert.startup` all
|
|
892
|
+
* describing a moment nobody experienced.
|
|
893
|
+
*
|
|
894
|
+
* Fails open twice: a hold that rejects counts as settled, and `budgetMs` bounds how long it may
|
|
895
|
+
* delay the world (45 s by default). A launch slower than it could be is a disappointment; a
|
|
896
|
+
* launch that never finishes because one asset 404'd is a bug.
|
|
897
|
+
*
|
|
898
|
+
* Throws on an empty or duplicate label, and on a hold registered after startup already
|
|
899
|
+
* resolved — each means the caller believes it is gating something it is not.
|
|
900
|
+
*
|
|
901
|
+
* @situation hold the loading screen until the game's own asset tier has landed
|
|
902
|
+
*/
|
|
903
|
+
hold(label: string, work: Promise<unknown>, budgetMs?: number): void;
|
|
904
|
+
}
|
|
905
|
+
interface ICtx<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
906
|
+
readonly fps: number;
|
|
907
|
+
readonly renderer: IRendererLike;
|
|
908
|
+
readonly viewport: Viewport;
|
|
909
|
+
readonly scene: Scene$1;
|
|
910
|
+
readonly camera: Camera;
|
|
911
|
+
readonly canvasLayer: CanvasLayer;
|
|
912
|
+
readonly entities: Registry;
|
|
913
|
+
/**
|
|
914
|
+
* Adds a node to the scene and hands it straight back, with its own type intact.
|
|
915
|
+
*
|
|
916
|
+
* Generic rather than `Object3D` because a game writes `const sea = ctx.add(new SpectralOcean())`
|
|
917
|
+
* and then calls a method on it. Erasing the type here makes every typed node in every scene need
|
|
918
|
+
* a cast back to what it already was, and that cast is where a game stops noticing it is holding
|
|
919
|
+
* something else.
|
|
920
|
+
*/
|
|
921
|
+
readonly add: <T extends Object3D>(object: T) => T;
|
|
922
|
+
readonly input: InputMap;
|
|
923
|
+
readonly pointer: IPointerEvents3D;
|
|
924
|
+
readonly assets: IAssetLoader;
|
|
925
|
+
readonly after: (delay: number, callback: () => void) => ScheduleHandle;
|
|
926
|
+
/** Register a callback for the engine-owned phase after physics writes solved transforms. */
|
|
927
|
+
readonly afterPhysics: (callback: AfterPhysicsCallback) => () => void;
|
|
928
|
+
readonly every: (callback: (dt: number) => void) => ScheduleHandle;
|
|
929
|
+
readonly state: GameStore<TState>;
|
|
930
|
+
readonly tween: <T extends object>(target: T, properties: {
|
|
931
|
+
[K in keyof T]?: number;
|
|
932
|
+
}, duration: number, options?: ITweenOptions) => Promise<void>;
|
|
933
|
+
readonly random: IRandom;
|
|
934
|
+
readonly raycast: (options?: IRaycastOptions) => Intersection | undefined;
|
|
935
|
+
readonly raycastAll: (options?: IRaycastOptions, target?: Intersection[]) => readonly Intersection[];
|
|
936
|
+
/**
|
|
937
|
+
* The framework's own startup work — what a loading screen waits on.
|
|
938
|
+
*
|
|
939
|
+
* A shader may compile the first time something using it is drawn, and the render projection may
|
|
940
|
+
* do its first build in that same frame. Both costs are real and belong before the world is shown.
|
|
941
|
+
*
|
|
942
|
+
* Keeping the world hidden until `whenReady()` resolves does more than hide the mess: the
|
|
943
|
+
* shaders that would have been compiled for geometry the projection then discards are never
|
|
944
|
+
* compiled at all, so waiting is *faster* than not waiting.
|
|
945
|
+
*/
|
|
946
|
+
readonly startup: IStartupStatus;
|
|
947
|
+
readonly goto: (name: string) => Promise<void>;
|
|
948
|
+
physics: TPhysics;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
type PluginCleanup = () => void;
|
|
952
|
+
interface IGameObservationSampleRequest {
|
|
953
|
+
readonly entities?: readonly string[];
|
|
954
|
+
readonly include?: readonly string[];
|
|
955
|
+
readonly label?: string;
|
|
956
|
+
readonly resources?: readonly string[];
|
|
957
|
+
}
|
|
958
|
+
interface IGameObservationContribution {
|
|
959
|
+
readonly capabilities: readonly string[];
|
|
960
|
+
readonly sample: (request: IGameObservationSampleRequest) => Readonly<Record<string, unknown>>;
|
|
961
|
+
}
|
|
962
|
+
interface IGameRuntimeObservations {
|
|
963
|
+
contribute(contribution: IGameObservationContribution): PluginCleanup;
|
|
964
|
+
contributions(): readonly IGameObservationContribution[];
|
|
965
|
+
}
|
|
966
|
+
interface IGamePluginRuntime {
|
|
967
|
+
readonly fixedStep: (ticks: number) => number;
|
|
968
|
+
/**
|
|
969
|
+
* Announces that a diagnostics consumer is going to read render metrics, turning per-frame
|
|
970
|
+
* sample collection on for the rest of the run. Optional: a runtime without collection
|
|
971
|
+
* support just never enables. Games collect nothing until this fires — the samples exist for
|
|
972
|
+
* assertions, not for every frame of every game.
|
|
973
|
+
*/
|
|
974
|
+
readonly enableRuntimeDiagnostics?: () => void;
|
|
975
|
+
/** The frame's cost attribution so far, or undefined when the game turned the budget off. */
|
|
976
|
+
readonly frameBudgetWindow?: () => IFrameBudgetWindow | undefined;
|
|
977
|
+
/**
|
|
978
|
+
* Hold start-scene entry until `gate` settles.
|
|
979
|
+
*
|
|
980
|
+
* The returned promise settles after `Scene.enter()` has run. A runner can therefore release the
|
|
981
|
+
* gate after applying pre-entry setup, then await the returned promise before describing
|
|
982
|
+
* entity-derived capabilities. The frame loop remains held throughout.
|
|
983
|
+
*/
|
|
984
|
+
readonly holdStart?: (gate: Promise<void>) => Promise<void>;
|
|
985
|
+
readonly observations: IGameRuntimeObservations;
|
|
986
|
+
readonly tick: () => number;
|
|
987
|
+
readonly runtimeDiagnosticsSeries?: () => readonly IRenderPerformanceSample[];
|
|
988
|
+
readonly random?: Pick<IRandom, "state">;
|
|
989
|
+
rapier?: string | null;
|
|
990
|
+
readonly seed: number | null;
|
|
991
|
+
/**
|
|
992
|
+
* Whether first-use compilation has settled, separately from full readiness.
|
|
993
|
+
*
|
|
994
|
+
* Readiness also requires a sustained in-budget frame window, which is a player-experience
|
|
995
|
+
* gate — a CPU rasteriser never meets it and resolves on the window's own timeout instead. A
|
|
996
|
+
* harness that has been told the machine has no GPU needs the earlier, cheaper signal, and it
|
|
997
|
+
* must be reported rather than inferred from a phase that cannot distinguish the two.
|
|
998
|
+
*/
|
|
999
|
+
readonly startupCompileSettled?: () => boolean;
|
|
1000
|
+
/** When the startup milestones happened, for the playtest bridge's startup observation. */
|
|
1001
|
+
readonly startupTimeline?: () => IStartupTimeline;
|
|
1002
|
+
/** The renderer-owned bounded pipeline capture, when the renderer has not been opted out. */
|
|
1003
|
+
readonly pipelineCensus?: () => IPipelineCensus;
|
|
1004
|
+
readonly step: number;
|
|
1005
|
+
}
|
|
1006
|
+
interface IGamePlatformSource {
|
|
1007
|
+
readonly devToolsHost?: Record<string, unknown>;
|
|
1008
|
+
readonly input: NonNullable<ConstructorParameters<typeof InputMap>[3]>;
|
|
1009
|
+
readonly inputTarget?: EventTarget;
|
|
1010
|
+
readonly renderer: NonNullable<IRendererOptions["source"]>;
|
|
1011
|
+
readonly viewport: NonNullable<IViewportOptions["source"]>;
|
|
1012
|
+
mountCanvas(canvas: HTMLCanvasElement, container?: HTMLElement): void;
|
|
1013
|
+
unmountCanvas(canvas: HTMLCanvasElement): void;
|
|
1014
|
+
}
|
|
1015
|
+
type GamePluginFunction<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: ICtx<TState, TPhysics>) => undefined | PluginCleanup;
|
|
1016
|
+
interface IGamePluginHooks<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
1017
|
+
setup?(ctx: ICtx<TState, TPhysics>, runtime?: IGamePluginRuntime): undefined | PluginCleanup | Promise<undefined | PluginCleanup>;
|
|
1018
|
+
beforeUpdate?(ctx: ICtx<TState, TPhysics>, dt: number): void;
|
|
1019
|
+
update?(ctx: ICtx<TState, TPhysics>, dt: number): void;
|
|
1020
|
+
sceneExit?(ctx: ICtx<TState, TPhysics>): void;
|
|
1021
|
+
dispose?(ctx: ICtx<TState, TPhysics>): void;
|
|
1022
|
+
}
|
|
1023
|
+
type GamePlugin<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = GamePluginFunction<TState, TPhysics> | IGamePluginHooks<TState, TPhysics>;
|
|
1024
|
+
interface IGameConfig<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
1025
|
+
readonly assets?: IAssetLoaderOptions;
|
|
1026
|
+
readonly camera?: CameraConfig;
|
|
1027
|
+
readonly canvas?: HTMLCanvasElement;
|
|
1028
|
+
readonly container?: HTMLElement;
|
|
1029
|
+
readonly input?: InputBindings;
|
|
1030
|
+
/**
|
|
1031
|
+
* Browser context menu over the game surface. Defaults to `"suppress"`, which is what a game
|
|
1032
|
+
* wants: right-click is a binding, not a menu. Set `"allow"` only if your game genuinely needs
|
|
1033
|
+
* the browser menu over its canvas.
|
|
1034
|
+
*/
|
|
1035
|
+
readonly contextMenu?: ContextMenuPolicy;
|
|
1036
|
+
/**
|
|
1037
|
+
* Per-frame cost attribution, on by default. Every `reportEvery` presented frames the game
|
|
1038
|
+
* prints one `TN_FRAME_BUDGET` line naming where the frame went — present wait, simulation,
|
|
1039
|
+
* three.js render, overlay, the rest — which is what a device lane reads instead of guessing.
|
|
1040
|
+
* Pass `false` to silence the marker; the same numbers still reach a playtest `performance`
|
|
1041
|
+
* assertion, because turning a convention off must not turn its measurement off.
|
|
1042
|
+
*/
|
|
1043
|
+
readonly frameBudget?: IFrameBudgetOptions | false;
|
|
1044
|
+
readonly initialState?: TState;
|
|
1045
|
+
/**
|
|
1046
|
+
* The **pre-start** shader warm-up. Off by default, and that is not the same as no warm-up.
|
|
1047
|
+
*
|
|
1048
|
+
* Every distinct pipeline is otherwise built the first time something using it is drawn, inside
|
|
1049
|
+
* the first rendered frame of a fully built scene. On a Pixel 8 that frame lasted **12.0 s, of
|
|
1050
|
+
* which 8.0 s was 105 pipeline compiles** — a launch the player reads as a hang, because the
|
|
1051
|
+
* loop presents nothing for the whole span. Warming those pipelines while the loading screen is
|
|
1052
|
+
* up is the obvious fix, and it is the one this option exists for.
|
|
1053
|
+
*
|
|
1054
|
+
* **A game with this unset still warms up.** `startupCompile` runs the same `warmUpScene` from
|
|
1055
|
+
* inside the loading layer's bounded readiness gate, where the opaque layer is on screen and the
|
|
1056
|
+
* loop is turning. This option only moves that work *earlier*, to before `start()` releases the
|
|
1057
|
+
* loop, where nothing is presenting — which is a thing to choose deliberately, not a default.
|
|
1058
|
+
*
|
|
1059
|
+
* **What PRD-327 changed is the mechanism, not this default.** The native host used to answer
|
|
1060
|
+
* `createRenderPipelineAsync` with the synchronous create wrapped in a resolved promise, so
|
|
1061
|
+
* `compileAsync` compiled on the main loop and was abandoned by its own budget having finished
|
|
1062
|
+
* nothing — `TN_WARMUP:{"compiled":0,"abandoned":1,"timedOut":true,"elapsedMs":15325}` — while
|
|
1063
|
+
* the first frame compiled the identical pipelines in 8.0 s anyway. Both entries are native
|
|
1064
|
+
* handlers now, handing the descriptor to a host compile pool and holding the main thread for
|
|
1065
|
+
* 0.27 ms of a 70 ms compile (ratio 0.0038 against a pre-registered bar of 0.25, asserted by
|
|
1066
|
+
* `threenative-async-pipeline-thread-test`). The default path started working without its
|
|
1067
|
+
* default moving.
|
|
1068
|
+
*
|
|
1069
|
+
* Pass `{}` or an options object to opt in, or `false` to opt out of warm-up entirely. Either
|
|
1070
|
+
* way `TN_WARMUP` and `TN_STARTUP_WARMUP` report what happened, because turning a convention off
|
|
1071
|
+
* must not turn its measurement off.
|
|
1072
|
+
*/
|
|
1073
|
+
readonly warmUp?: IWarmUpOptions | false | true;
|
|
1074
|
+
readonly inputTarget?: EventTarget;
|
|
1075
|
+
/**
|
|
1076
|
+
* Maximum simulation steps per rendered frame. Default 5. Caps the catch-up burst after a
|
|
1077
|
+
* stall so a slow frame cannot cascade into a spiral of longer frames.
|
|
1078
|
+
*/
|
|
1079
|
+
readonly maxSteps?: number;
|
|
1080
|
+
readonly platform?: IGamePlatformSource;
|
|
1081
|
+
readonly plugins?: readonly GamePlugin<TState, TPhysics>[];
|
|
1082
|
+
/**
|
|
1083
|
+
* The project's `display` block, passed straight through from `threenative.config.ts`. The
|
|
1084
|
+
* adaptive scale holds `maxFps` as its budget, so a game that does not pass this gets the
|
|
1085
|
+
* 60 fps default rather than a scaler with no target.
|
|
1086
|
+
*/
|
|
1087
|
+
readonly display?: NonNullable<IThreeNativeConfig["display"]>;
|
|
1088
|
+
readonly render?: NonNullable<IThreeNativeConfig["renderer"]>;
|
|
1089
|
+
readonly renderer?: IRendererOptions;
|
|
1090
|
+
readonly seed?: number;
|
|
1091
|
+
readonly scenes: Record<string, SceneConstructor<TState, TPhysics>>;
|
|
1092
|
+
/**
|
|
1093
|
+
* Fixed simulation step in seconds, e.g. `1 / 60`. **This is the fixed-step knob a game
|
|
1094
|
+
* wants**; every `update(ctx, dt)` receives exactly this `dt`, never a variable frame time,
|
|
1095
|
+
* so gameplay and physics advance together and never see a stall.
|
|
1096
|
+
*
|
|
1097
|
+
* Do not write your own accumulator on top of this. Doing so runs the scene's update several
|
|
1098
|
+
* times per already-fixed step and decouples gameplay from the simulation — a real build lost
|
|
1099
|
+
* its largest wrong turn to exactly that, because this field carried no documentation.
|
|
1100
|
+
*/
|
|
1101
|
+
readonly step?: number;
|
|
1102
|
+
readonly start: string;
|
|
1103
|
+
readonly stateFlushMs?: number;
|
|
1104
|
+
}
|
|
1105
|
+
interface IPerspectiveCameraConfig {
|
|
1106
|
+
readonly projection: "perspective";
|
|
1107
|
+
readonly fov?: number;
|
|
1108
|
+
readonly near?: number;
|
|
1109
|
+
readonly far?: number;
|
|
1110
|
+
}
|
|
1111
|
+
interface IOrthogonalCameraConfig {
|
|
1112
|
+
readonly projection: "orthogonal";
|
|
1113
|
+
readonly size: number;
|
|
1114
|
+
readonly near?: number;
|
|
1115
|
+
readonly far?: number;
|
|
1116
|
+
}
|
|
1117
|
+
type CameraConfig = IPerspectiveCameraConfig | IOrthogonalCameraConfig;
|
|
1118
|
+
/**
|
|
1119
|
+
* The game's end of the UI bridge.
|
|
1120
|
+
*
|
|
1121
|
+
* The UI renders through the platform's own browser-class renderer, which on every native
|
|
1122
|
+
* target is a different realm from this one — so it holds a mirror of the game's published
|
|
1123
|
+
* state and sends intents back rather than calling into the game. The web target uses the same
|
|
1124
|
+
* two channels through an in-process broker, which is what keeps one `src/ui/` honest: a HUD
|
|
1125
|
+
* that works here works on a phone.
|
|
1126
|
+
*
|
|
1127
|
+
* Publication is automatic and throttled to the store's own published cadence, and it stops
|
|
1128
|
+
* entirely when nothing is listening — a game whose `ui.renderer` is `native` pays nothing.
|
|
1129
|
+
*/
|
|
1130
|
+
interface IGameUi {
|
|
1131
|
+
/**
|
|
1132
|
+
* Whether a UI layer has announced itself.
|
|
1133
|
+
*
|
|
1134
|
+
* Stricter than "a transport exists": the UI sends `tn:ready` once its tree has rendered and its
|
|
1135
|
+
* interactive rectangles are published, and only then is this true. A transport with nothing on
|
|
1136
|
+
* the other end and a UI that failed to render look the same to a game otherwise.
|
|
1137
|
+
*/
|
|
1138
|
+
readonly connected: boolean;
|
|
1139
|
+
/** Handle an intent the UI sent — `restart`, `pause`, whatever the game defines. */
|
|
1140
|
+
onIntent(listener: (intent: string, payload: unknown) => void): () => void;
|
|
1141
|
+
/** Publish the current state now, whether or not it changed. */
|
|
1142
|
+
publish(): void;
|
|
1143
|
+
}
|
|
1144
|
+
interface IGotoOptions<TState extends Record<string, unknown>> {
|
|
1145
|
+
readonly carry?: Partial<TState>;
|
|
1146
|
+
}
|
|
1147
|
+
interface IGame<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
|
|
1148
|
+
readonly ctx: ICtx<TState, TPhysics> | undefined;
|
|
1149
|
+
readonly scene: Scene<TState, TPhysics> | undefined;
|
|
1150
|
+
/** The name of the entered scene, or undefined before `start()`. */
|
|
1151
|
+
readonly sceneName: string | undefined;
|
|
1152
|
+
readonly state: GameStore<TState>;
|
|
1153
|
+
/** The seam between the game and its UI layer, on every target. @see IGameUi */
|
|
1154
|
+
readonly ui: IGameUi;
|
|
1155
|
+
/** Rebuilds the requested scene from its initial state, then merges an optional carry patch. */
|
|
1156
|
+
goto(name: string, options?: IGotoOptions<TState>): Promise<void>;
|
|
1157
|
+
/** Boot into `name` instead of `config.start` on the next `start()`. Hot reload's restore path. */
|
|
1158
|
+
resumeScene(name: string): void;
|
|
1159
|
+
start(): Promise<void>;
|
|
1160
|
+
pause(): void;
|
|
1161
|
+
resume(): void;
|
|
1162
|
+
stop(): void;
|
|
1163
|
+
}
|
|
1164
|
+
declare function defineGame<TState extends Record<string, unknown>, TPhysics = undefined>(config: IGameConfig<TState, TPhysics>): IGame<TState, TPhysics>;
|
|
1165
|
+
|
|
1166
|
+
export { afterPhysics as $, type AfterPhysicsCallback as A, type IThreeNativeIconVariants as B, type ContextMenuPolicy as C, type IThreeNativeTexturesConfig as D, type ITweenOptions as E, type IWarmUpCacheOptions as F, type IWarmUpObservation as G, type IWarmUpOptions as H, type IGame as I, type IWarmUpProgress as J, type IWarmUpRenderer as K, type IWarmUpReport as L, type InputBindings as M, type InputPlatformSource as N, type PointerEvent3DType as O, type PointerEvent3DListener as P, PointerEvents3D as Q, type SceneFrame as R, Scene as S, ScenePicker as T, type ScheduleHandle as U, Scheduler as V, type ThreeNativeBackgroundMode as W, type ThreeNativeOrientation as X, type ThreeNativeUiRenderer as Y, type WarmUpCacheStatus as Z, type WarmUpObservationStatus as _, type IGamePluginRuntime as a, createRandom as a0, defineGame as a1, warmUpScene as a2, type IGamePluginHooks as b, type ICtx as c, type IGameObservationContribution as d, type IGameObservationSampleRequest as e, type IGamePlatformSource as f, type IInputAction as g, type IInputGamepad as h, type IPointerDragHandle as i, type IPointerEvent3D as j, type IPointerEvents3D as k, type IPointerEvents3DOptions as l, type IPointerEvents3DPicker as m, type IPointerState as n, type IRandom as o, type IRawInputPointer as p, type IRawInputPointerEdge as q, type IRawInputState as r, type IRaycastOptions as s, type IScenePickerOptions as t, type IThreeNativeAudioConfig as u, type IThreeNativeAudioLoop as v, type IThreeNativeAudioOverride as w, type IThreeNativeAudioSpectrum as x, type IThreeNativeBootSplash as y, type IThreeNativeConfig as z };
|