@threenative/core 0.2.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.
@@ -1,429 +0,0 @@
1
- import * as three from 'three';
2
- import { Texture, Vector2, Object3D, Camera, Vector3, Scene as Scene$1, OrthographicCamera, Intersection } from 'three';
3
- import { StoreApi } from 'zustand/vanilla';
4
-
5
- interface IAssetLoaderOptions {
6
- readonly basePath?: string;
7
- readonly model?: (url: string) => Promise<unknown>;
8
- readonly texture?: (url: string) => Promise<Texture>;
9
- readonly audio?: (url: string) => Promise<AudioBuffer>;
10
- }
11
- interface IAssetLoader {
12
- model<T = unknown>(path: string): Promise<T>;
13
- texture(path: string): Promise<Texture>;
14
- audio(path: string): Promise<AudioBuffer>;
15
- release(kind: "audio" | "model" | "texture", path: string): boolean;
16
- clear(): void;
17
- }
18
-
19
- /**
20
- * One action, read either as a button through `pressed`/`justPressed` or as a 2D axis through
21
- * `vector`. Which one you get depends on the fields you fill in, and mixing the two is what
22
- * makes bindings confusing to read:
23
- *
24
- * ```ts
25
- * jump: { keys: ["Space"], buttons: [0] } // a button
26
- * move: { up: ["KeyW"], down: ["KeyS"], left: [...], right: [...] } // an axis
27
- * ```
28
- *
29
- * `up`/`down`/`left`/`right` are **directions of an axis**, not "the keys that press this".
30
- * They are named after the vector they build, so `pressed("move")` is true whenever any
31
- * direction is held — including `ArrowDown`. For a button, use `keys`.
32
- */
33
- interface IInputAction {
34
- /** Gamepad button indices that press this action. */
35
- readonly buttons?: readonly number[];
36
- /**
37
- * Keyboard codes that press this action. Use this for a button — `jump`, `restart`, `fire`.
38
- * A key listed here never contributes to `vector`.
39
- */
40
- readonly keys?: readonly string[];
41
- /** The **−y** direction of `vector(name)`. Not "the keys that press this action" — see `keys`. */
42
- readonly down?: readonly string[];
43
- /** The −x direction of `vector(name)`. */
44
- readonly left?: readonly string[];
45
- /** Any active pointer or touch presses this action. */
46
- readonly pointer?: boolean;
47
- /** The +x direction of `vector(name)`. */
48
- readonly right?: readonly string[];
49
- /** The +y direction of `vector(name)`. */
50
- readonly up?: readonly string[];
51
- }
52
- type InputBindings = Record<string, IInputAction>;
53
- interface IInputGamepad {
54
- readonly axes: ArrayLike<number>;
55
- readonly buttons: readonly {
56
- readonly pressed: boolean;
57
- }[];
58
- }
59
- type InputPlatformSource = () => readonly (IInputGamepad | null)[];
60
- interface IRawInputPointer {
61
- readonly id: number;
62
- buttons: number;
63
- readonly position: Vector2;
64
- }
65
- interface IRawInputState {
66
- readonly keys: ReadonlySet<string>;
67
- readonly pointer: {
68
- buttons: number;
69
- down: boolean;
70
- readonly position: Vector2;
71
- };
72
- readonly pointers: ReadonlyMap<number, IRawInputPointer>;
73
- readonly gamepad: {
74
- axes: readonly number[];
75
- buttons: readonly boolean[];
76
- };
77
- }
78
- declare class InputMap {
79
- #private;
80
- readonly raw: IRawInputState;
81
- constructor(bindings?: InputBindings, target?: EventTarget, pointerTarget?: EventTarget, source?: InputPlatformSource);
82
- /**
83
- * Returns a 2D action vector where +y is up. On a conventional XZ ground plane whose
84
- * forward direction is -z, map `vector.y` to `-z`. This differs from Godot's
85
- * `Input.get_vector`, where up is -y.
86
- */
87
- vector(name: string): Vector2;
88
- pressed(name: string): boolean;
89
- justPressed(name: string): boolean;
90
- justReleased(name: string): boolean;
91
- tick(): void;
92
- clear(): void;
93
- dispose(): void;
94
- }
95
-
96
- interface IRenderPerformanceMetrics {
97
- readonly drawCalls?: number;
98
- readonly triangles?: number;
99
- }
100
- interface IRenderPerformanceSample extends IRenderPerformanceMetrics {
101
- readonly frameMs: number;
102
- }
103
-
104
- interface IRandom {
105
- (): number;
106
- pick<T>(items: readonly T[]): T;
107
- range(min: number, max: number): number;
108
- state: number;
109
- }
110
- declare function createRandom(seed?: number): IRandom;
111
-
112
- type RendererKind = "webgpu" | "webgl2";
113
- interface IRendererLike {
114
- readonly domElement: HTMLCanvasElement;
115
- readonly kind: RendererKind;
116
- readonly raw: unknown;
117
- /**
118
- * Builds and compiles a scene's pipelines before anything draws it.
119
- *
120
- * On a phone each distinct shader is compiled the first time something using it is drawn, which
121
- * happens inside a frame the player is watching: 2,500 ms of a 2,882 ms Pixel 8 cold start sits
122
- * between the bundle finishing and the first frame reaching the display. Calling this during
123
- * load moves that cost somewhere the player is already waiting.
124
- *
125
- * It is on the wrapper for one reason: without it a game must cast through `.raw` to warm up,
126
- * and a game that cannot warm up without a cast will not warm up.
127
- */
128
- compileAsync(scene: Object3D, camera: Camera): Promise<void>;
129
- compute(node: unknown): void;
130
- render(scene: Object3D, camera: Camera): void;
131
- /** Draws after the world without clearing or passing through the world's output pipeline. */
132
- renderOverlay(scene: Object3D, camera: Camera): void;
133
- setOutputNode(node: unknown): void;
134
- setSize(width: number, height: number, updateStyle?: boolean): void;
135
- dispose(): void;
136
- }
137
- interface IRendererPlatformSource {
138
- createCanvas(): HTMLCanvasElement;
139
- hasWebGPU(): boolean;
140
- observeResize(canvas: HTMLCanvasElement, resize: () => void): () => void;
141
- readSize(canvas: HTMLCanvasElement): readonly [width: number, height: number];
142
- }
143
- interface IRendererOptions {
144
- canvas?: HTMLCanvasElement;
145
- preferWebGPU?: boolean;
146
- /** CSS-pixel multiplier for the drawing buffer. The default is intentional DPR 1. */
147
- resolutionScale?: number;
148
- source?: IRendererPlatformSource;
149
- webgpuFactory?: (canvas: HTMLCanvasElement) => Promise<unknown> | unknown;
150
- webgl2Factory?: (canvas: HTMLCanvasElement) => unknown;
151
- }
152
-
153
- interface IViewportSize {
154
- readonly aspect: number;
155
- readonly height: number;
156
- readonly width: number;
157
- }
158
- interface IViewportOptions {
159
- readonly camera: Camera;
160
- readonly renderer: IRendererLike;
161
- readonly source?: IViewportPlatformSource;
162
- }
163
- type ViewportResizeHandler = (size: IViewportSize) => void;
164
- interface IViewportPlatformSource {
165
- observeResize(canvas: HTMLCanvasElement, resize: () => void): () => void;
166
- readSize(canvas: HTMLCanvasElement): IViewportSize;
167
- }
168
- declare class Viewport {
169
- #private;
170
- readonly camera: Camera;
171
- readonly renderer: IRendererLike;
172
- constructor(options: IViewportOptions);
173
- get size(): IViewportSize;
174
- projectPosition(screen: Vector2, z?: number): Vector3;
175
- unprojectPosition(world: Vector3): Vector2;
176
- onResize(handler: ViewportResizeHandler): () => void;
177
- resize(): void;
178
- dispose(): void;
179
- }
180
-
181
- /** A Godot-shaped render surface that is independent of the world camera and post pipeline. */
182
- declare class CanvasLayer {
183
- #private;
184
- readonly scene: Scene$1<three.Object3DEventMap>;
185
- readonly camera: OrthographicCamera;
186
- /** Declares that this layer covers the framebuffer, allowing the world pass to be skipped. */
187
- opaque: boolean;
188
- constructor(viewport: Pick<Viewport, "onResize" | "size">);
189
- dispose(): void;
190
- }
191
-
192
- type EntitySnapshot = Record<string, Record<string, unknown> & {
193
- tags?: string[];
194
- }>;
195
- declare class Registry {
196
- #private;
197
- add<T extends object>(name: string, entity: T): T;
198
- get<T extends object = object>(name: string): T | undefined;
199
- remove(name: string): void;
200
- queueFree(target: string | object): void;
201
- sweep(): void;
202
- clear(): void;
203
- snapshot(): EntitySnapshot;
204
- }
205
-
206
- interface IRaycastOptions {
207
- /** Screen point in canvas pixels. Defaults to the current pointer position. */
208
- readonly screen?: Vector2;
209
- /** What to test. Defaults to the whole scene. */
210
- readonly targets?: Object3D | readonly Object3D[];
211
- }
212
- interface IScenePickerOptions {
213
- readonly camera: Camera;
214
- readonly pointer: () => Vector2;
215
- readonly scene: Object3D;
216
- readonly viewport: Viewport;
217
- }
218
- /**
219
- * Ray queries against scene geometry, accelerated by a bounding volume hierarchy that is
220
- * built on first use and rebuilt when the geometry's positions change.
221
- *
222
- * The acceleration is an implementation detail: nothing about it reaches the caller, no
223
- * `three` prototype is patched, and a game that never calls `raycast` never builds a tree.
224
- * Skinned, instanced, batched and morphed meshes fall back to the stock `three` path,
225
- * because a hierarchy over their rest positions would report hits in the wrong place.
226
- */
227
- declare class ScenePicker {
228
- #private;
229
- constructor(options: IScenePickerOptions);
230
- /** The closest hit under the screen point, or `undefined` when the ray hits nothing. */
231
- raycast(options?: IRaycastOptions): Intersection | undefined;
232
- /** Drops every cached hierarchy. The next `raycast` rebuilds what it needs. */
233
- dispose(): void;
234
- }
235
-
236
- type ScheduleHandle = (() => void) & {
237
- cancel(): void;
238
- readonly active: boolean;
239
- };
240
- type TweenProperties<T extends object> = {
241
- [K in keyof T]?: number;
242
- };
243
- declare class Scheduler {
244
- #private;
245
- get size(): number;
246
- after(delay: number, callback: () => void): ScheduleHandle;
247
- every(callback: (dt: number) => void): ScheduleHandle;
248
- tween<T extends object>(target: T, properties: TweenProperties<T>, duration: number): Promise<void>;
249
- tick(dt: number): void;
250
- clear(): void;
251
- }
252
-
253
- type StatePatch<T extends Record<string, unknown>> = Partial<T> | ((state: T) => Partial<T>);
254
- type GameStore<T extends Record<string, unknown>> = StoreApi<T> & {
255
- set(patch: StatePatch<T>): void;
256
- flush(): void;
257
- start(): void;
258
- stop(): void;
259
- };
260
-
261
- declare abstract class Scene<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
262
- static readonly initialState: Record<string, unknown> | undefined;
263
- load(_ctx: ICtx<TState, TPhysics>): void | Promise<void>;
264
- enter(_ctx: ICtx<TState, TPhysics>): SceneEnterResult<TState, TPhysics>;
265
- exit(_ctx: ICtx<TState, TPhysics>): void;
266
- update(_ctx: ICtx<TState, TPhysics>, _dt: number): void;
267
- render(_ctx: ICtx<TState, TPhysics>): void;
268
- }
269
- type SceneConstructor<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = new () => Scene<TState, TPhysics>;
270
- type SceneFrame<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: ICtx<TState, TPhysics>, dt: number) => void;
271
- type SceneEnterResult<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = // biome-ignore lint/suspicious/noConfusingVoidType: void preserves existing Scene.enter overrides.
272
- void | SceneFrame<TState, TPhysics>;
273
- interface IStartupStatus {
274
- /**
275
- * `observing` while the collapse watches what moves, `collapsing` during the single frame it
276
- * bakes in, `ready` once the world is safe to show.
277
- */
278
- readonly phase: "observing" | "collapsing" | "ready";
279
- /** 0 to 1 across the observation window, then 1. Real progress for the part that has any. */
280
- readonly progress: number;
281
- /** Resolves on every path, including a scene too small to collapse, so it is always awaitable. */
282
- whenReady(): Promise<void>;
283
- }
284
- interface ICtx<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
285
- readonly fps: number;
286
- readonly renderer: IRendererLike;
287
- readonly viewport: Viewport;
288
- readonly scene: Scene$1;
289
- readonly camera: Camera;
290
- readonly canvasLayer: CanvasLayer;
291
- readonly entities: Registry;
292
- readonly add: (object: Object3D) => Object3D;
293
- readonly input: InputMap;
294
- readonly assets: IAssetLoader;
295
- readonly after: (delay: number, callback: () => void) => ScheduleHandle;
296
- readonly every: (callback: (dt: number) => void) => ScheduleHandle;
297
- readonly state: GameStore<TState>;
298
- readonly tween: <T extends object>(target: T, properties: {
299
- [K in keyof T]?: number;
300
- }, duration: number) => Promise<void>;
301
- readonly random: IRandom;
302
- readonly raycast: (options?: IRaycastOptions) => Intersection | undefined;
303
- /**
304
- * The framework's own startup work — what a loading screen waits on.
305
- *
306
- * Two costs land before a game is ready and both are real: each shader is compiled the first
307
- * time something using it is drawn, and the scene collapse runs inside a single frame. On a
308
- * Pixel 8 that was 2.5 s of half-drawn map followed by a 3.2 s stall, measured.
309
- *
310
- * Keeping the world hidden until `whenReady()` resolves does more than hide the mess: the
311
- * shaders that would have been compiled for geometry the collapse then throws away are never
312
- * compiled at all, so waiting is *faster* than not waiting.
313
- */
314
- readonly startup: IStartupStatus;
315
- readonly goto: (name: string) => Promise<void>;
316
- physics: TPhysics;
317
- }
318
-
319
- type PluginCleanup = () => void;
320
- interface IGameObservationSampleRequest {
321
- readonly entities?: readonly string[];
322
- readonly include?: readonly string[];
323
- readonly label?: string;
324
- readonly resources?: readonly string[];
325
- }
326
- interface IGameObservationContribution {
327
- readonly capabilities: readonly string[];
328
- readonly sample: (request: IGameObservationSampleRequest) => Readonly<Record<string, unknown>>;
329
- }
330
- interface IGameRuntimeObservations {
331
- contribute(contribution: IGameObservationContribution): PluginCleanup;
332
- contributions(): readonly IGameObservationContribution[];
333
- }
334
- interface IGamePluginRuntime {
335
- readonly fixedStep: (ticks: number) => number;
336
- /**
337
- * Hold the frame loop until `gate` settles, after the start scene has entered.
338
- *
339
- * A plugin that blocks its own `setup` blocks it too early: entity-derived capabilities are
340
- * registered by the scene, which runs after plugin setup, so a runner reading `describe()`
341
- * during that hold sees a description missing them. Handing the gate here instead holds the
342
- * loop at the last possible moment — everything is registered, nothing has stepped.
343
- */
344
- readonly holdStart?: (gate: Promise<void>) => void;
345
- readonly observations: IGameRuntimeObservations;
346
- readonly tick: () => number;
347
- readonly runtimeDiagnosticsSeries?: () => readonly IRenderPerformanceSample[];
348
- readonly random?: Pick<IRandom, "state">;
349
- rapier?: string | null;
350
- readonly seed: number | null;
351
- readonly step: number;
352
- }
353
- interface IGamePlatformSource {
354
- readonly devToolsHost?: Record<string, unknown>;
355
- readonly input: NonNullable<ConstructorParameters<typeof InputMap>[3]>;
356
- readonly inputTarget?: EventTarget;
357
- readonly renderer: NonNullable<IRendererOptions["source"]>;
358
- readonly viewport: NonNullable<IViewportOptions["source"]>;
359
- mountCanvas(canvas: HTMLCanvasElement, container?: HTMLElement): void;
360
- unmountCanvas(canvas: HTMLCanvasElement): void;
361
- }
362
- type GamePluginFunction<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = (ctx: ICtx<TState, TPhysics>) => undefined | PluginCleanup;
363
- interface IGamePluginHooks<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
364
- setup?(ctx: ICtx<TState, TPhysics>, runtime?: IGamePluginRuntime): undefined | PluginCleanup | Promise<undefined | PluginCleanup>;
365
- beforeUpdate?(ctx: ICtx<TState, TPhysics>, dt: number): void;
366
- update?(ctx: ICtx<TState, TPhysics>, dt: number): void;
367
- sceneExit?(ctx: ICtx<TState, TPhysics>): void;
368
- dispose?(ctx: ICtx<TState, TPhysics>): void;
369
- }
370
- type GamePlugin<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> = GamePluginFunction<TState, TPhysics> | IGamePluginHooks<TState, TPhysics>;
371
- interface IGameConfig<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
372
- readonly assets?: IAssetLoaderOptions;
373
- readonly camera?: CameraConfig;
374
- readonly canvas?: HTMLCanvasElement;
375
- readonly container?: HTMLElement;
376
- readonly input?: InputBindings;
377
- readonly initialState?: TState;
378
- readonly inputTarget?: EventTarget;
379
- /**
380
- * Maximum simulation steps per rendered frame. Default 5. Caps the catch-up burst after a
381
- * stall so a slow frame cannot cascade into a spiral of longer frames.
382
- */
383
- readonly maxSteps?: number;
384
- readonly platform?: IGamePlatformSource;
385
- readonly plugins?: readonly GamePlugin<TState, TPhysics>[];
386
- readonly render?: Pick<IRendererOptions, "preferWebGPU">;
387
- readonly renderer?: IRendererOptions;
388
- readonly seed?: number;
389
- readonly scenes: Record<string, SceneConstructor<TState, TPhysics>>;
390
- /**
391
- * Fixed simulation step in seconds, e.g. `1 / 60`. **This is the fixed-step knob a game
392
- * wants**; every `update(ctx, dt)` receives exactly this `dt`, never a variable frame time,
393
- * so gameplay and physics advance together and never see a stall.
394
- *
395
- * Do not write your own accumulator on top of this. Doing so runs the scene's update several
396
- * times per already-fixed step and decouples gameplay from the simulation — a real build lost
397
- * its largest wrong turn to exactly that, because this field carried no documentation.
398
- */
399
- readonly step?: number;
400
- readonly start: string;
401
- readonly stateFlushMs?: number;
402
- }
403
- interface IPerspectiveCameraConfig {
404
- readonly projection: "perspective";
405
- readonly fov?: number;
406
- readonly near?: number;
407
- readonly far?: number;
408
- }
409
- interface IOrthogonalCameraConfig {
410
- readonly projection: "orthogonal";
411
- readonly size: number;
412
- readonly near?: number;
413
- readonly far?: number;
414
- }
415
- type CameraConfig = IPerspectiveCameraConfig | IOrthogonalCameraConfig;
416
- interface IGame<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined> {
417
- readonly ctx: ICtx<TState, TPhysics> | undefined;
418
- readonly scene: Scene<TState, TPhysics> | undefined;
419
- readonly state: GameStore<TState>;
420
- /** Rebuilds the requested scene from the game's declared initial state. */
421
- goto(name: string): Promise<void>;
422
- start(): Promise<void>;
423
- pause(): void;
424
- resume(): void;
425
- stop(): void;
426
- }
427
- declare function defineGame<TState extends Record<string, unknown>, TPhysics = undefined>(config: IGameConfig<TState, TPhysics>): IGame<TState, TPhysics>;
428
-
429
- export { CanvasLayer as C, type IGame as I, Scene as S, type IRendererLike as a, type IGamePluginRuntime as b, type IGamePluginHooks as c, type ICtx as d, type IGameObservationContribution as e, type IGameObservationSampleRequest as f, type IGamePlatformSource as g, type IRandom as h, type IRawInputPointer as i, type IRaycastOptions as j, type IScenePickerOptions as k, type SceneFrame as l, ScenePicker as m, type ScheduleHandle as n, Scheduler as o, createRandom as p, defineGame as q };