@nayan-ui/engine 1.0.0-beta.3 → 1.0.0-beta.4

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.
Files changed (71) hide show
  1. package/README.md +167 -69
  2. package/lib/module/assets/AssetLoader.js +1 -1
  3. package/lib/module/assets/ProceduralCache.js +31 -4
  4. package/lib/module/assets/font.js +10 -7
  5. package/lib/module/assets/model.js +41 -19
  6. package/lib/module/assets/storage.shared.js +2 -0
  7. package/lib/module/assets/storage.web.js +1 -0
  8. package/lib/module/assets/texture.js +14 -11
  9. package/lib/module/index.js +4 -1
  10. package/lib/module/media/audio.js +6 -3
  11. package/lib/module/native/BrowserEngine.js +17 -0
  12. package/lib/module/render/GameView.js +86 -18
  13. package/lib/module/render/light.js +24 -0
  14. package/lib/module/render/quality.js +12 -5
  15. package/lib/module/render/useWorld.js +72 -0
  16. package/lib/module/world/Scoreboard.js +21 -3
  17. package/lib/module/world/TransformBatch.js +5 -3
  18. package/lib/module/world/World.js +27 -13
  19. package/lib/module/world/color.js +80 -0
  20. package/lib/module/world/desc.js +6 -5
  21. package/lib/module/world/effects.js +20 -8
  22. package/lib/typescript/src/assets/AssetLoader.d.ts +1 -1
  23. package/lib/typescript/src/assets/ProceduralCache.d.ts +12 -0
  24. package/lib/typescript/src/assets/model.d.ts +10 -0
  25. package/lib/typescript/src/index.d.ts +4 -1
  26. package/lib/typescript/src/native/EngineBridge.d.ts +6 -2
  27. package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +29 -0
  28. package/lib/typescript/src/render/GameView.d.ts +19 -7
  29. package/lib/typescript/src/render/light.d.ts +12 -0
  30. package/lib/typescript/src/render/quality.d.ts +4 -2
  31. package/lib/typescript/src/render/useWorld.d.ts +20 -0
  32. package/lib/typescript/src/types.d.ts +13 -4
  33. package/lib/typescript/src/world/Scoreboard.d.ts +12 -2
  34. package/lib/typescript/src/world/TransformBatch.d.ts +2 -2
  35. package/lib/typescript/src/world/World.d.ts +10 -2
  36. package/lib/typescript/src/world/color.d.ts +18 -0
  37. package/lib/typescript/src/world/effects.d.ts +13 -12
  38. package/lib/typescript/src/world/types.d.ts +11 -6
  39. package/lib/web/engine_core_bg.wasm +0 -0
  40. package/lib/web/index.js +521 -255
  41. package/lib/web/types/src/assets/AssetLoader.d.ts +1 -1
  42. package/lib/web/types/src/assets/ProceduralCache.d.ts +12 -0
  43. package/lib/web/types/src/assets/model.d.ts +10 -0
  44. package/lib/web/types/src/index.d.ts +4 -1
  45. package/lib/web/types/src/native/EngineBridge.d.ts +6 -2
  46. package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +29 -0
  47. package/lib/web/types/src/render/GameView.d.ts +19 -7
  48. package/lib/web/types/src/render/light.d.ts +11 -0
  49. package/lib/web/types/src/render/quality.d.ts +4 -2
  50. package/lib/web/types/src/render/useWorld.d.ts +19 -0
  51. package/lib/web/types/src/types.d.ts +13 -4
  52. package/lib/web/types/src/world/Scoreboard.d.ts +12 -2
  53. package/lib/web/types/src/world/TransformBatch.d.ts +2 -2
  54. package/lib/web/types/src/world/World.d.ts +10 -2
  55. package/lib/web/types/src/world/color.d.ts +17 -0
  56. package/lib/web/types/src/world/effects.d.ts +13 -12
  57. package/lib/web/types/src/world/types.d.ts +11 -6
  58. package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +87 -26
  59. package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
  60. package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
  61. package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
  62. package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
  63. package/native/core/build/android/x86/libengine_core.a +0 -0
  64. package/native/core/build/android/x86_64/libengine_core.a +0 -0
  65. package/native/core/include/engine_core.h +9 -0
  66. package/native/cpp/NayanEngineModule.cpp +171 -0
  67. package/native/cpp/NayanEngineModule.h +30 -0
  68. package/native/cpp/generated/NayanEngineSpecJSI.h +105 -0
  69. package/native/ios/NayanEngineSurface.mm +30 -1
  70. package/package.json +2 -2
  71. package/src/native/spec/NativeNayanEngine.ts +39 -0
@@ -5,6 +5,6 @@ export declare class AssetLoader<T> {
5
5
  private readonly name;
6
6
  private readonly loads;
7
7
  constructor(name: string);
8
- load(source: AssetSource, decode: (native: EngineBridge, bytes: ArrayBuffer) => T, variant?: readonly (string | number | boolean)[]): Promise<T>;
8
+ load(source: AssetSource, decode: (native: EngineBridge, bytes: ArrayBuffer) => T | Promise<T>, variant?: readonly (string | number | boolean)[]): Promise<T>;
9
9
  private error;
10
10
  }
@@ -2,6 +2,12 @@
2
2
  export interface ProceduralStorage {
3
3
  read(key: string): Uint8Array | null;
4
4
  write(key: string, bytes: Uint8Array): void | Promise<void>;
5
+ /**
6
+ * The store itself verifies every record's integrity (the engine's own Rust store checksums each
7
+ * one with SHA-256), so the cache needn't checksum and scan them again in JS. Geometry is still
8
+ * validated by the core when it's registered.
9
+ */
10
+ readonly verified?: boolean;
5
11
  }
6
12
  /**
7
13
  * Persists the output of a deterministic model factory, including nested model arrays/objects.
@@ -26,6 +32,12 @@ export declare class ProceduralCache {
26
32
  pendingWrites: number;
27
33
  };
28
34
  flush(): Promise<void>;
35
+ /**
36
+ * A value already cached (in memory, or saved by an earlier launch), without building anything:
37
+ * undefined when there is none yet. Lets work that's spread over many frames (a level generated
38
+ * step by step) skip itself entirely when a saved result exists, then save with `get`.
39
+ */
40
+ peek<T>(key: string): T | undefined;
29
41
  get<T>(key: string, build: () => T): T;
30
42
  private encode;
31
43
  private restore;
@@ -1,3 +1,4 @@
1
+ import { type MeshGeometry } from '../render/meshes';
1
2
  import type { Vec3 } from '../types';
2
3
  import type { AssetSource } from './source';
3
4
  export type ModelOptions = {
@@ -32,6 +33,15 @@ export declare function nativeGeometry(mesh: number): {
32
33
  vertices: Float32Array<ArrayBuffer>;
33
34
  indices: Uint32Array<ArrayBuffer>;
34
35
  };
36
+ /**
37
+ * Registers a mesh the core already holds (a created, cached or loaded model, a glyph, an alias):
38
+ * the renderer copies it inside Rust, and JS reads its geometry only if something asks for it
39
+ * (merging, caching). Apps built before `renderModel` upload it through JS as before.
40
+ */
41
+ export declare function registerCoreMesh(mesh: number, info?: {
42
+ texture?: number;
43
+ shine?: number;
44
+ }, geometry?: () => MeshGeometry): void;
35
45
  /** Independent materials share core geometry without overwriting an existing model's appearance. */
36
46
  export declare function registerModelMesh(mesh: number, shine: number): number;
37
47
  /**
@@ -1,9 +1,12 @@
1
1
  export { World, isEngineAvailable } from './world/World';
2
2
  export { quat, type MutableQuat } from './world/quat';
3
+ export { toColor, type ColorInput } from './world/color';
3
4
  export { effects, type DustOptions, type EffectOptions, type FlashOptions, type PopOptions } from './world/effects';
4
5
  export { Scoreboard, type ScoreboardHandlers } from './world/Scoreboard';
5
6
  export type { AnimateOptions, AnimateTarget, BodyType, Bounds, BurstOptions, CollisionInfo, ClipOptions, ScreenPlane, Color, Easing, Entity, EntityOptions, ImpactFeedback, Keyframe, PhysicsOptions, PickHit, Quat, RaycastHit, SpringOptions } from './world/types';
6
- export { GameView } from './render/GameView';
7
+ export { afterNextFrame, GameView } from './render/GameView';
8
+ export { useWorld, type WorldSetup } from './render/useWorld';
9
+ export { frontLight } from './render/light';
7
10
  export { warmUpEngine, getMeshIdLimit } from './render/meshes';
8
11
  export type { TransformBatch, TransformOptions } from './world/TransformBatch';
9
12
  export type { DragEvent, GameViewProps, GameViewStats, OverlayTouchPhase, PinchEvent, SwipeDirection } from './render/GameView';
@@ -4,11 +4,13 @@ export interface NativeGeometry {
4
4
  vertices: ArrayBuffer;
5
5
  indices: ArrayBuffer;
6
6
  }
7
- type BufferValue<T, Buffer> = T extends readonly number[] ? T : T extends object ? Buffer : T;
7
+ type BufferValue<T, Buffer> = T extends readonly number[] ? T : T extends Promise<unknown> ? T : T extends (...args: never[]) => unknown ? T : T extends object ? Buffer : T;
8
8
  type BufferMethod<T> = T extends (...args: infer A) => infer R ? (...args: {
9
9
  [K in keyof A]: BufferValue<A[K], ArrayBufferLike>;
10
10
  }) => BufferValue<R, ArrayBuffer> : T;
11
- type BinarySpec = Omit<Spec, 'geometrySolid' | 'geometryOptimize' | 'modelSize' | 'textureSize'>;
11
+ /** Native-only (a display clock and a render thread): absent on the web core and on older app builds. */
12
+ type NativeFrames = 'publishFrame' | 'renderStats' | 'hasFrameClock' | 'startFrames' | 'stopFrames';
13
+ type BinarySpec = Omit<Spec, 'geometrySolid' | 'geometryOptimize' | 'modelSize' | 'textureSize' | NativeFrames>;
12
14
  /** The same runtime ABI on JSI and WASM, with precise TypeScript buffer and tuple contracts. */
13
15
  export type EngineBridge = {
14
16
  [K in keyof BinarySpec]: BufferMethod<BinarySpec[K]>;
@@ -17,5 +19,7 @@ export type EngineBridge = {
17
19
  geometryOptimize(vertices: ArrayBufferLike, indices: ArrayBufferLike, ratio: number, error: number, lockBorders: boolean): NativeGeometry;
18
20
  modelSize(mesh: number): [x: number, y: number, z: number];
19
21
  textureSize(texture: number): [width: number, height: number];
22
+ } & {
23
+ [K in NativeFrames]?: BufferMethod<Spec[K]>;
20
24
  };
21
25
  export {};
@@ -90,6 +90,8 @@ export interface Spec extends TurboModule {
90
90
  shapePrepare(vertices: Object, indices: Object, options: string): number;
91
91
  solidPrepare(recipe: string, options: string): number;
92
92
  modelLoad(data: Object, center: boolean, fit: number): number;
93
+ /** `modelLoad` on a background native thread (the data is copied first): resolves to the mesh id, or -1. */
94
+ modelLoadAsync(data: Object, center: boolean, fit: number): Promise<number>;
93
95
  /** 11 floats per vertex (position, normal, color, uv); aliases native memory. */
94
96
  modelVertices(mesh: number): Object;
95
97
  /** u32 triangle indices; aliases native memory. */
@@ -102,6 +104,11 @@ export interface Spec extends TurboModule {
102
104
  meshAlias(base: number): number;
103
105
  /** Registers geometry (copied): 11 floats per vertex, u32 triangle indices. Returns the mesh id, or -1. */
104
106
  meshCreate(vertices: Object, indices: Object): number;
107
+ /**
108
+ * Hands a core model's own geometry (from `meshCreate`, `modelLoad`, a font) to the renderer, in
109
+ * Rust: nothing is copied through JS. `texture` -1 for none. False if the mesh is unknown.
110
+ */
111
+ renderModel(mesh: number, texture: number, shine: number): boolean;
105
112
  /**
106
113
  * Fills 2D contours ([x, y] f32 pairs, `counts[k]` points per contour) by the non-zero or even-odd
107
114
  * rule and extrudes them `depth` deep. Returns an ArrayBuffer of 11 floats per vertex (unindexed).
@@ -122,12 +129,16 @@ export interface Spec extends TurboModule {
122
129
  geometryPlace(vertices: Object, indices: Object, placement: Array<number>, first: number, outVertices: Object, vertexOffset: number, outIndices: Object, indexOffset: number): void;
123
130
  /** Decodes a PNG/JPEG ArrayBuffer. Returns the texture id, or -1. */
124
131
  textureLoad(data: Object): number;
132
+ /** `textureLoad` on a background native thread: resolves to the texture id, or -1. */
133
+ textureLoadAsync(data: Object): Promise<number>;
125
134
  /** RGBA8 pixels, top row first; aliases native memory. */
126
135
  texturePixels(texture: number): Object;
127
136
  /** [width, height]. */
128
137
  textureSize(texture: number): Array<number>;
129
138
  /** Builds extruded glyph meshes for `chars`. Returns the font id, or -1. */
130
139
  fontLoad(data: Object, depth: number, chars: string, weight: number): number;
140
+ /** `fontLoad` (outlines and glyph meshes) on a background native thread: resolves to the font id, or -1. */
141
+ fontLoadAsync(data: Object, depth: number, chars: string, weight: number): Promise<number>;
131
142
  /** [codepoint, mesh (-1 = nothing drawn), advance, ...] followed by the cap height. */
132
143
  fontGlyphs(font: number): Array<number>;
133
144
  /** Why the last model / texture / font / alias load failed ("" after a success). */
@@ -151,6 +162,22 @@ export interface Spec extends TurboModule {
151
162
  * Returns 1 (drawn), 0 (surface not ready yet / frame skipped) or -1 (see `renderError`).
152
163
  */
153
164
  renderFrame(surface: number, frame: Object, matrices: Object, colors: Object, regions: Object, ranges: Object, count: number, viewProj: Object, world: number): number;
165
+ /**
166
+ * `renderFrame`'s arguments, but the frame is copied and drawn on the native render thread: returns
167
+ * at once, with `viewProj` written. 1 once the surface has presented a frame, 0 before, -1 if the
168
+ * last frame failed (see `renderError`).
169
+ */
170
+ publishFrame(surface: number, frame: Object, matrices: Object, colors: Object, regions: Object, ranges: Object, count: number, viewProj: Object, world: number): number;
171
+ /** [frames presented, smoothed render milliseconds] for a surface drawn by the render thread. */
172
+ renderStats(surface: number): Array<number>;
173
+ /** True where the platform drives frames from its display clock (`startFrames`); else use requestAnimationFrame. */
174
+ hasFrameClock(): boolean;
175
+ /**
176
+ * Calls `onFrame` on the JS thread once per display refresh while the surface is attached and
177
+ * visible, with the frame time in milliseconds. A frame JS hasn't started yet isn't queued again.
178
+ */
179
+ startFrames(surface: number, onFrame: (timeMs: number) => void): void;
180
+ stopFrames(surface: number): void;
154
181
  /**
155
182
  * The screen-space overlay drawn over surface `surface` from its next frame on, copied from a world's
156
183
  * buffers. In points: x right and y up from the view's top-left (on screen y <= 0), z toward the
@@ -167,6 +194,8 @@ export interface Spec extends TurboModule {
167
194
  drawingDestroy(document: number): void;
168
195
  /** Decodes a WAV ArrayBuffer. Returns a sound id, or -1. */
169
196
  audioLoad(data: Object): number;
197
+ /** `audioLoad` on a background native thread: resolves to the sound id, or -1. */
198
+ audioLoadAsync(data: Object): Promise<number>;
170
199
  /** Returns a voice id (> 0), or 0 if nothing played. */
171
200
  audioPlay(sound: number, volume: number, pan: number, pitch: number, loop: boolean): number;
172
201
  audioStop(voice: number): void;
@@ -1,7 +1,18 @@
1
1
  import { type Camera, type Light, type RenderSource } from '../types';
2
- import type { Color } from '../world/types';
2
+ import { type ColorInput } from '../world/color';
3
3
  import { type ViewStyle } from './host';
4
4
  import { type RenderQuality, type RenderQualitySetting } from './quality';
5
+ /**
6
+ * Runs `fn` right after the next frame a GameView hands to the screen, on the same display clock
7
+ * (the native one where the platform has it): the time a frame leaves is the best moment for
8
+ * background work, such as loading in slices. With no GameView running it falls back to
9
+ * requestAnimationFrame. Returns a function that cancels it.
10
+ *
11
+ * ```ts
12
+ * const cancel = afterNextFrame(() => buildNextChunk());
13
+ * ```
14
+ */
15
+ export declare function afterNextFrame(fn: () => void): () => void;
5
16
  export type GameViewStats = {
6
17
  fps: number;
7
18
  /** Mean time spent in `onUpdate` per frame, in milliseconds. */
@@ -41,15 +52,16 @@ export type GameViewProps = {
41
52
  source: RenderSource;
42
53
  /**
43
54
  * Your game loop, called once per frame before drawing, with the elapsed seconds (capped at 0.1;
44
- * 0 until the first frame is on screen, so nothing plays out unseen).
45
- * Step your simulation here, e.g. `world.update(dt)`, then read input, spawn, move the camera.
55
+ * 0 until the first frame is on screen, so nothing plays out unseen): read input, spawn, move the
56
+ * camera. The world (and `overlay`) is stepped for you right after; call `world.update(dt)` here
57
+ * yourself only to step it at a different point or with a different `dt`.
46
58
  */
47
59
  onUpdate?: (dt: number) => void;
48
60
  /** Mutate this object (e.g. `camera.eye`) from `onUpdate`; it is read every frame. */
49
61
  camera?: Camera;
50
62
  light?: Light;
51
- /** Sky / clear color, linear 0..1 RGB (alpha ignored). Also the fog color. Default dark navy. */
52
- background?: Color;
63
+ /** Sky / clear color: a CSS color or linear 0..1 RGB (alpha ignored). Also the fog color. Default dark navy. */
64
+ background?: ColorInput;
53
65
  /** Distance fog density towards `background` (e.g. 0.02). Default 0 (off). */
54
66
  fog?: number;
55
67
  /**
@@ -76,8 +88,8 @@ export type GameViewProps = {
76
88
  /**
77
89
  * A screen-space layer drawn over the scene by the renderer (scoreboards, buttons): usually a second
78
90
  * `World`, in points from the view's top-left with y up (so on screen y <= 0) and z toward the viewer
79
- * within -100..100. Unlit (flat faces show their exact color) and always in front. Step it yourself
80
- * (`overlay.update(dt)` in `onUpdate`).
91
+ * within -100..100. Unlit (flat faces show their exact color) and always in front. Stepped each
92
+ * frame like `source`.
81
93
  */
82
94
  overlay?: RenderSource;
83
95
  /**
@@ -0,0 +1,11 @@
1
+ import type { Light } from '../types';
2
+ /**
3
+ * A light for flat, front-facing scenes (boards, puzzles, 2D games looking down -z): surfaces facing
4
+ * the camera (+z) are lit at exactly 1, so flat panels show their color unchanged, while rounded
5
+ * shapes keep their shading (lighter toward the light, darker away). Shadows are off.
6
+ *
7
+ * ```tsx
8
+ * <GameView source={world} light={frontLight([-0.45, 0.6, 1], 0.5)} />
9
+ * ```
10
+ */
11
+ export declare function frontLight(direction: [number, number, number], ambient: number): Light;
@@ -23,8 +23,9 @@ export type FrameReport = {
23
23
  };
24
24
  /**
25
25
  * Picks the level for "auto": starts at the best, steps down one level after `SLOW_SECONDS`
26
- * consecutive seconds whose typical frame misses ~50 fps while drawing took most of it, and steps
27
- * back up after `FAST_SECONDS` smooth seconds. A level that fails again soon after being restored
26
+ * consecutive seconds whose typical frame misses ~50 fps while drawing took most of it (after a
27
+ * single second when it misses ~33 fps, so a struggling phone recovers in seconds rather than
28
+ * stuttering through every level), and steps back up after `FAST_SECONDS` smooth seconds. A level that fails again soon after being restored
28
29
  * becomes the ceiling, so it never bounces.
29
30
  */
30
31
  export declare class AutoQuality {
@@ -35,6 +36,7 @@ export declare class AutoQuality {
35
36
  private changed;
36
37
  private raisedAt;
37
38
  private slow;
39
+ private verySlow;
38
40
  private fast;
39
41
  get quality(): RenderQuality;
40
42
  /** Feeds one second of frames; returns true when the level changed. */
@@ -0,0 +1,19 @@
1
+ import { type DependencyList } from 'react';
2
+ import { World } from '../world/World';
3
+ /** What `useWorld`'s setup may return: a cleanup, run just before the world is disposed. */
4
+ export type WorldSetup = (world: World) => void | (() => void);
5
+ /**
6
+ * A `World` owned by a component: the engine is warmed up (needed on the web, cheap on mobile), the
7
+ * world is created with `capacity` entity slots and handed to `setup` to fill, and it is disposed on
8
+ * unmount. A change in `deps` replaces it with a fresh one. Returns null until it's ready (one frame),
9
+ * and while the engine is unavailable (Expo Go) or failed to start (`onError`, console by default).
10
+ *
11
+ * ```tsx
12
+ * const world = useWorld(100, (w) => {
13
+ * w.spawn({ mesh: 'plane', scale: [20, 1, 20], color: '#4C9A4C', physics: 'fixed' });
14
+ * w.spawn({ mesh: 'sphere', position: [0, 4, 0], color: '#3399FF', physics: 'dynamic' });
15
+ * });
16
+ * return world && <GameView source={world} style={{ flex: 1 }} />;
17
+ * ```
18
+ */
19
+ export declare function useWorld(capacity: number, setup?: WorldSetup, deps?: DependencyList, onError?: (error: Error) => void): World | null;
@@ -1,4 +1,5 @@
1
- import type { Color, Entity } from './world/types';
1
+ import type { ColorInput } from './world/color';
2
+ import type { Entity } from './world/types';
2
3
  export type Vec3 = readonly [number, number, number];
3
4
  /** Number of mesh ids: built-in shapes (0..15), then loaded models, glyphs and textured variants. */
4
5
  export declare const MAX_MESHES = 4096;
@@ -23,6 +24,14 @@ export interface RenderSource {
23
24
  readonly capacity: number;
24
25
  /** Called by GameView after each frame with the camera it drew with (for picking and `toScreen`). */
25
26
  setView?(viewProj: Float32Array, width: number, height: number): void;
27
+ /**
28
+ * Steps the source. When present, GameView calls it once a frame after `onUpdate`, unless
29
+ * `onUpdate` already did (`updateCount` changed) or the source is disposed.
30
+ */
31
+ update?(dt: number): void;
32
+ /** How many times `update` has run. */
33
+ readonly updateCount?: number;
34
+ readonly isDisposed?: boolean;
26
35
  }
27
36
  /**
28
37
  * Built-in shapes, each 1 unit across before scaling. "none" isn't drawn (groups, pivots, trigger zones).
@@ -64,8 +73,8 @@ export type Light = {
64
73
  shadowExtent?: number;
65
74
  /**
66
75
  * Color and brightness of the light, multiplying both sun and ambient: e.g. [0.35, 0.38, 0.6] for
67
- * moonlight. Glowing colors (channels above 1) aren't affected. Alpha, if given, is ignored.
68
- * Default [1, 1, 1].
76
+ * moonlight, or a CSS color. Glowing colors (channels above 1) aren't affected. Alpha, if given, is
77
+ * ignored. Default [1, 1, 1].
69
78
  */
70
- color?: Color;
79
+ color?: ColorInput;
71
80
  };
@@ -12,10 +12,12 @@ export type ScoreboardHandlers = {
12
12
  /**
13
13
  * Score, lives and the game clock, kept and advanced in Rust by `world.update`: countdowns, elapsed
14
14
  * time and time-based points (survival, distance) cost no JS per frame. Handlers run from `update`
15
- * only when something they show changed.
15
+ * only when something they show changed. Set `handlers` for the game's own rules, and `listen` for
16
+ * anything else that follows along (a UI showing the score), so neither replaces the other.
16
17
  *
17
18
  * ```ts
18
- * world.scoreboard.handlers = { onScore: setScore, onSecond: setTime, onTimeUp: () => end(true) };
19
+ * world.scoreboard.handlers = { onTimeUp: () => end(true) };
20
+ * const stop = world.scoreboard.listen({ onScore: setScore, onSecond: setTime });
19
21
  * world.scoreboard.reset(3);
20
22
  * world.scoreboard.startCountdown(60);
21
23
  * world.scoreboard.add(10);
@@ -23,7 +25,9 @@ export type ScoreboardHandlers = {
23
25
  */
24
26
  export declare class Scoreboard {
25
27
  private readonly world;
28
+ /** The game's own handlers. `listen` adds more without replacing these. */
26
29
  handlers: ScoreboardHandlers;
30
+ private readonly listeners;
27
31
  private readonly state;
28
32
  /** @internal */
29
33
  constructor(world: number);
@@ -59,6 +63,12 @@ export declare class Scoreboard {
59
63
  * that countdown in full (a ready screen); `resume()` starts it.
60
64
  */
61
65
  reset(lives?: number, countdownSeconds?: number): void;
66
+ /**
67
+ * Adds handlers alongside `handlers` (and other listeners); they run after them, in order of
68
+ * listening. Returns a function that removes them.
69
+ */
70
+ listen(handlers: ScoreboardHandlers): () => void;
62
71
  /** @internal Runs handlers for what changed since the last call. */
63
72
  dispatch(): void;
73
+ private run;
64
74
  }
@@ -2,8 +2,8 @@ import type { Entity, EntityOptions } from './types';
2
2
  export type TransformOptions = Pick<EntityOptions, 'position' | 'rotation' | 'scale' | 'color'>;
3
3
  export declare const TRANSFORM_STRIDE = 16;
4
4
  /**
5
- * Explicit draw-only updates, applied in one native call by `flush()`. Flush before reading
6
- * positions, animating, or stepping the world. Records and scratch storage are reused; values are
5
+ * Explicit draw-only updates, applied in one native call by `flush()`. `world.update` flushes
6
+ * `world.transforms` before stepping; flush yourself before reading positions or animating. Records and scratch storage are reused; values are
7
7
  * copied when queued, never retained by reference. The scalar methods (`position`, `rotation`,
8
8
  * `scale`, `color`, `transform`) allocate nothing; consecutive calls for the same entity share one
9
9
  * record.
@@ -44,6 +44,7 @@ export declare class World implements RenderSource {
44
44
  private gravityValue;
45
45
  private boundsValue;
46
46
  private listenerValue;
47
+ private updates;
47
48
  constructor(capacity: number);
48
49
  private get native();
49
50
  /** True once `dispose()` has run. Cleanup calls (despawn, set, has, queries, stopping animations
@@ -61,7 +62,8 @@ export declare class World implements RenderSource {
61
62
  set(e: Entity, options: EntityOptions): boolean;
62
63
  /**
63
64
  * This world's shared draw-only transform batch (created on first use). Queue per-frame moves with
64
- * its allocation-free methods after `update()`, then `flush()` once before the frame is drawn.
65
+ * its allocation-free methods; the next `update()` applies them first (or `flush()` them yourself
66
+ * before reading positions or animating).
65
67
  */
66
68
  get transforms(): TransformBatch;
67
69
  /** Reusable draw-only transforms; use set() for text entities and flush before animations/queries. */
@@ -131,6 +133,8 @@ export declare class World implements RenderSource {
131
133
  cancelTimers(): void;
132
134
  /** Score, lives and the game clock, kept in Rust (created on first use). */
133
135
  get scoreboard(): Scoreboard;
136
+ /** How many times `update` has run. GameView reads it to step the world only when `onUpdate` didn't. */
137
+ get updateCount(): number;
134
138
  /** Timers waiting to fire. */
135
139
  get timerCount(): number;
136
140
  /** Resolves the promises of animations the core reports as ended, then runs due timers. */
@@ -183,7 +187,11 @@ export declare class World implements RenderSource {
183
187
  /** Impact sounds pan and fade relative to this entity (usually the player). */
184
188
  get listener(): Entity | null;
185
189
  set listener(e: Entity | null);
186
- /** Advances the simulation (fixed 60 Hz steps), resolves finished animations, refreshes what GameView draws. */
190
+ /**
191
+ * Advances the simulation (fixed 60 Hz steps), resolves finished animations, runs due timers and
192
+ * scoreboard handlers, and refreshes what GameView draws. `GameView` calls it for you each frame;
193
+ * call it yourself from `onUpdate` only to step with a different `dt` (it is then not stepped twice).
194
+ */
187
195
  update(dt: number): void;
188
196
  /**
189
197
  * Calls `fn` for each touch that started or ended during the last `update`. `info` is reused:
@@ -0,0 +1,17 @@
1
+ import type { Color } from './types';
2
+ /** A color as an option takes it: `[r, g, b]` / `[r, g, b, a]` (0..1, channels above 1 glow) or a CSS string. */
3
+ export type ColorInput = Color | string;
4
+ /**
5
+ * Any color input as an `[r, g, b, a]` tuple (0..1), with its alpha multiplied by `alpha`. Use it when
6
+ * you need the numbers (to brighten, mix or fade a theme color); options take strings directly.
7
+ *
8
+ * ```ts
9
+ * toColor('#FF4757'); // [1, 0.278, 0.341, 1]
10
+ * toColor('#FF4757', 0.5); // half see-through
11
+ * ```
12
+ */
13
+ export declare function toColor(color: ColorInput, alpha?: number): [number, number, number, number];
14
+ /** A color input as a tuple, without copying tuples (the hot path of `spawn`, `set` and `animate`). */
15
+ export declare function resolveColor(color: ColorInput): Color;
16
+ /** One color input, or a list to pick from, as tuples. */
17
+ export declare function resolveColors(colors: ColorInput | readonly ColorInput[]): readonly Color[];
@@ -1,11 +1,12 @@
1
1
  import type { Vec3 } from '../types';
2
- import type { BurstOptions, Color, Entity } from './types';
2
+ import { type ColorInput } from './color';
3
+ import type { BurstOptions, Entity } from './types';
3
4
  import type { World } from './World';
4
5
  /** Shared options of the layered effects (confetti, sparkle, shatter, splash, sparks, explosion). */
5
6
  export type EffectOptions = {
6
7
  position: Vec3;
7
- /** One color, or several to pick from at random. */
8
- colors: Color | readonly Color[];
8
+ /** One color (CSS string or tuple), or several to pick from at random. */
9
+ colors: ColorInput | readonly ColorInput[];
9
10
  /** Typical particle size. */
10
11
  size: number;
11
12
  /** Top speed. */
@@ -27,7 +28,7 @@ export type EffectOptions = {
27
28
  export type DustOptions = {
28
29
  position: Vec3;
29
30
  /** Start color (alpha is how thick it looks), or several to pick from. */
30
- color: Color | readonly Color[];
31
+ color: ColorInput | readonly ColorInput[];
31
32
  /** Puff size at the start. */
32
33
  size: number;
33
34
  /** Top speed. */
@@ -44,7 +45,7 @@ export type DustOptions = {
44
45
  /** How much each puff grows by the end. Default 2. */
45
46
  endSize?: number;
46
47
  /** Color at the end. Default: a single `color` faded to transparent; several colors keep theirs. */
47
- endColor?: Color;
48
+ endColor?: ColorInput;
48
49
  /** Default "puff" (blended); "flame" adds light instead. */
49
50
  mesh?: BurstOptions['mesh'];
50
51
  };
@@ -52,14 +53,14 @@ export type DustOptions = {
52
53
  export type FlashOptions = {
53
54
  position: Vec3;
54
55
  /** Use channels above 1 to glow (see `effects.hot`). */
55
- color: Color;
56
+ color: ColorInput;
56
57
  size: number;
57
58
  /** Seconds. Default 0.22. */
58
59
  lifetime?: number;
59
60
  /** How much it grows by the end. Default 2.2. */
60
61
  endSize?: number;
61
62
  /** Default: `color` faded to transparent. */
62
- endColor?: Color;
63
+ endColor?: ColorInput;
63
64
  /** Default "flame" (adds light); "sphere" or "puff" for a solid or soft flash. */
64
65
  mesh?: BurstOptions['mesh'];
65
66
  /** Glows drawn at once (slightly scattered when `speed` > 0). Default 1. */
@@ -72,7 +73,7 @@ export type PopOptions = {
72
73
  /** Its scale at rest (what it has now): one number, or per axis. */
73
74
  scale: number | Vec3;
74
75
  /** Its color at rest, to return to after the white flash. Omit to flash `flash` only (or nothing). */
75
- color?: Color;
76
+ color?: ColorInput;
76
77
  /** Parts to flash white as well (a group's visible children, whose rest color is white). */
77
78
  flash?: readonly Entity[];
78
79
  /** Seconds before it starts. Default 0. */
@@ -82,9 +83,9 @@ export type PopOptions = {
82
83
  };
83
84
  type Rgba = [number, number, number, number];
84
85
  /** The same hue, self-lit: its brightest channel lifted to `peak` (above 1 glows, through light, shadow and fog). */
85
- declare function hot(c: Color, peak?: number, alpha?: number): Rgba;
86
+ declare function hot(input: ColorInput, peak?: number, alpha?: number): Rgba;
86
87
  /** Part way (`t`, 0..1) to white, with `alpha`. */
87
- declare function pale(c: Color, t: number, alpha?: number): Rgba;
88
+ declare function pale(input: ColorInput, t: number, alpha?: number): Rgba;
88
89
  declare function flash(world: World, o: FlashOptions): number;
89
90
  declare function dust(world: World, o: DustOptions): number;
90
91
  /** Celebration confetti: tumbling paper in two layers, a few glinting sparkles, and a pop of light. */
@@ -110,8 +111,8 @@ declare function sparks(world: World, e: EffectOptions): void;
110
111
  */
111
112
  declare function explosion(world: World, e: Omit<EffectOptions, 'colors'> & {
112
113
  colors?: EffectOptions['colors'];
113
- debris?: readonly Color[];
114
- smoke?: Color;
114
+ debris?: readonly ColorInput[];
115
+ smoke?: ColorInput;
115
116
  }): void;
116
117
  /** The piece swells, flashes white, settles and shrinks away. Resolves when it's gone (despawning it is the caller's). */
117
118
  declare function pop(world: World, entity: Entity, o: PopOptions): Promise<void>;
@@ -3,6 +3,7 @@ import type { Model } from '../assets/model';
3
3
  import type { Texture } from '../assets/texture';
4
4
  import type { Sound } from '../media/audio';
5
5
  import type { Vec3 } from '../types';
6
+ import type { ColorInput } from './color';
6
7
  /** Opaque entity handle. Handles of despawned entities are safely ignored. */
7
8
  export type Entity = number & {
8
9
  readonly __entity: unique symbol;
@@ -105,8 +106,11 @@ export type EntityOptions = {
105
106
  rotation?: Quat;
106
107
  /** A number scales uniformly. Children scale with their parent. Visual only after the body exists. */
107
108
  scale?: Vec3 | number;
108
- /** Alpha below 1 makes it see-through. Channels above 1 glow: at 2 it's self-lit, ignoring light and shadow (fire, lamps). */
109
- color?: Color;
109
+ /**
110
+ * A CSS color (`'#FF4757'`, `'rgba(…)'`) or a 0..1 tuple. Alpha below 1 makes it see-through. Tuple
111
+ * channels above 1 glow: at 2 it's self-lit, ignoring light and shadow (fire, lamps).
112
+ */
113
+ color?: ColorInput;
110
114
  /** Units per second. */
111
115
  velocity?: Vec3;
112
116
  /** Sets horizontal speed [x, z] and keeps the vertical one: steer a body and it still falls. */
@@ -141,7 +145,8 @@ export type AnimateTarget = {
141
145
  position?: Vec3;
142
146
  rotation?: Quat;
143
147
  scale?: Vec3 | number;
144
- color?: Color;
148
+ /** A CSS color or a 0..1 tuple. */
149
+ color?: ColorInput;
145
150
  /** Move by this offset from where the animation starts (instead of to a `position`). */
146
151
  moveBy?: Vec3;
147
152
  /** Turn by these angles (radians around x, y, z) from the starting rotation. 2π spins once; more spins more. */
@@ -200,12 +205,12 @@ export type BurstOptions = {
200
205
  lifetime?: number;
201
206
  /** Vertical acceleration. Default -9.81 (falls); 0 floats. */
202
207
  gravity?: number;
203
- /** One color, or up to 4 to pick from at random. Default white. */
204
- color?: Color | readonly Color[];
208
+ /** One color (CSS string or tuple), or up to 4 to pick from at random. Default white. */
209
+ color?: ColorInput | readonly ColorInput[];
205
210
  /** Size at the end of each particle's life, as a multiple of its start size: above 1 grows (smoke), below shrinks (embers). Default 1. */
206
211
  endSize?: number;
207
212
  /** Color every particle shifts to over its life, e.g. flame to dark red; an alpha below 1 fades it out. Default: keeps its color. */
208
- endColor?: Color;
213
+ endColor?: ColorInput;
209
214
  };
210
215
  export type CollisionInfo = {
211
216
  /** True when the touch started; false when it ended. */