@nayan-ui/engine 1.0.0-beta.5 → 1.0.0-beta.6

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 (77) hide show
  1. package/README.md +95 -4
  2. package/lib/module/assets/ProceduralCache.js +5 -0
  3. package/lib/module/assets/loft.js +15 -63
  4. package/lib/module/assets/procedural.js +500 -0
  5. package/lib/module/assets/shape.js +147 -245
  6. package/lib/module/assets/texture.js +65 -0
  7. package/lib/module/drawing/DrawingDocument.js +1 -1
  8. package/lib/module/index.js +4 -1
  9. package/lib/module/media/audio-worklet.js +27 -0
  10. package/lib/module/media/audio.js +20 -0
  11. package/lib/module/media/input.web.js +107 -0
  12. package/lib/module/media/microphone.js +89 -0
  13. package/lib/module/media/speech.js +80 -0
  14. package/lib/module/media/speechPlatform.js +12 -0
  15. package/lib/module/media/speechPlatform.web.js +16 -0
  16. package/lib/module/native/BrowserEngine.js +122 -0
  17. package/lib/module/native/NativeNayanEngine.web.js +2 -0
  18. package/lib/module/render/GameView.js +45 -13
  19. package/lib/module/render/quality.js +151 -17
  20. package/lib/module/world/World.js +63 -0
  21. package/lib/typescript/src/assets/ProceduralCache.d.ts +2 -0
  22. package/lib/typescript/src/assets/loft.d.ts +3 -1
  23. package/lib/typescript/src/assets/procedural.d.ts +394 -0
  24. package/lib/typescript/src/assets/shape.d.ts +13 -7
  25. package/lib/typescript/src/assets/texture.d.ts +63 -0
  26. package/lib/typescript/src/index.d.ts +8 -2
  27. package/lib/typescript/src/media/audio.d.ts +13 -0
  28. package/lib/typescript/src/media/input.web.d.ts +4 -0
  29. package/lib/typescript/src/media/microphone.d.ts +51 -0
  30. package/lib/typescript/src/media/speech.d.ts +38 -0
  31. package/lib/typescript/src/media/speechPlatform.d.ts +5 -0
  32. package/lib/typescript/src/media/speechPlatform.web.d.ts +5 -0
  33. package/lib/typescript/src/native/EngineBridge.d.ts +7 -1
  34. package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +72 -0
  35. package/lib/typescript/src/render/GameView.d.ts +30 -3
  36. package/lib/typescript/src/render/quality.d.ts +54 -2
  37. package/lib/typescript/src/types.d.ts +9 -2
  38. package/lib/typescript/src/world/World.d.ts +16 -0
  39. package/lib/typescript/src/world/types.d.ts +8 -0
  40. package/lib/web/audio-worklet.js +27 -0
  41. package/lib/web/engine_core_bg.wasm +0 -0
  42. package/lib/web/index.js +1181 -422
  43. package/lib/web/types/src/assets/ProceduralCache.d.ts +2 -0
  44. package/lib/web/types/src/assets/loft.d.ts +3 -1
  45. package/lib/web/types/src/assets/procedural.d.ts +393 -0
  46. package/lib/web/types/src/assets/shape.d.ts +13 -7
  47. package/lib/web/types/src/assets/texture.d.ts +63 -0
  48. package/lib/web/types/src/index.d.ts +8 -2
  49. package/lib/web/types/src/media/audio.d.ts +13 -0
  50. package/lib/web/types/src/media/input.d.ts +3 -0
  51. package/lib/web/types/src/media/microphone.d.ts +50 -0
  52. package/lib/web/types/src/media/speech.d.ts +37 -0
  53. package/lib/web/types/src/media/speechPlatform.d.ts +4 -0
  54. package/lib/web/types/src/native/EngineBridge.d.ts +7 -1
  55. package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +72 -0
  56. package/lib/web/types/src/render/GameView.d.ts +30 -3
  57. package/lib/web/types/src/render/quality.d.ts +54 -2
  58. package/lib/web/types/src/types.d.ts +9 -2
  59. package/lib/web/types/src/world/World.d.ts +16 -0
  60. package/lib/web/types/src/world/types.d.ts +8 -0
  61. package/native/android/src/main/AndroidManifest.xml +10 -0
  62. package/native/android/src/main/java/com/nayanui/engine/EngineMicrophone.java +60 -0
  63. package/native/android/src/main/java/com/nayanui/engine/EngineSpeech.java +28 -0
  64. package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +1 -0
  65. package/native/android/src/main/java/com/nayanui/engine/NayanEnginePackage.java +2 -0
  66. package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
  67. package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
  68. package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
  69. package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
  70. package/native/core/build/android/x86/libengine_core.a +0 -0
  71. package/native/core/build/android/x86_64/libengine_core.a +0 -0
  72. package/native/core/include/engine_core.h +63 -1
  73. package/native/cpp/NayanEngineModule.cpp +296 -0
  74. package/native/cpp/NayanEngineModule.h +40 -0
  75. package/native/cpp/generated/NayanEngineSpecJSI.h +397 -0
  76. package/package.json +1 -1
  77. package/src/native/spec/NativeNayanEngine.ts +77 -0
@@ -0,0 +1,37 @@
1
+ export type SpeechOptions = {
2
+ /** BCP 47 language tag, such as en-US or hi-IN. Defaults to the system language. */
3
+ language?: string;
4
+ /** Identifier returned by getAvailableVoicesAsync. */
5
+ voice?: string;
6
+ /** Speed relative to the platform default, 0.1..2. Default 1. */
7
+ rate?: number;
8
+ /** Pitch multiplier, 0.5..2. Default 1. */
9
+ pitch?: number;
10
+ /** Loudness, 0..1. Default 1. */
11
+ volume?: number;
12
+ };
13
+ export type SpeechVoice = {
14
+ identifier: string;
15
+ name: string;
16
+ language: string;
17
+ quality: 'Default' | 'Enhanced';
18
+ };
19
+ /** System text-to-speech. Independent of the engine mixer, microphone and asset loading. */
20
+ export declare const speech: {
21
+ /** API availability; installed voices and Android TTS initialization may still fail. */
22
+ readonly supported: boolean;
23
+ /**
24
+ * Queues text using the system synthesizer. Resolves when queued, not when speech finishes.
25
+ * No audio files are created. Call stop on leaving a screen to cancel its queued speech.
26
+ */
27
+ speak(text: string, options?: SpeechOptions): Promise<void>;
28
+ /** Stops current speech and clears the entire speech queue. */
29
+ stop(): Promise<void>;
30
+ /** iOS and web only; rejects on Android, which has no system pause API. */
31
+ pause(): Promise<void>;
32
+ /** iOS and web only; rejects on Android. */
33
+ resume(): Promise<void>;
34
+ /** Includes speech paused on iOS/web. */
35
+ isSpeakingAsync(): Promise<boolean>;
36
+ getAvailableVoicesAsync(): Promise<SpeechVoice[]>;
37
+ };
@@ -0,0 +1,4 @@
1
+ export declare const speechPlatform: {
2
+ readonly supported: boolean;
3
+ request(command: string): Promise<string>;
4
+ };
@@ -10,15 +10,21 @@ type BufferMethod<T> = T extends (...args: infer A) => infer R ? (...args: {
10
10
  }) => BufferValue<R, ArrayBuffer> : T;
11
11
  /** Native-only (a display clock and a render thread): absent on the web core and on older app builds. */
12
12
  type NativeFrames = 'publishFrame' | 'renderStats' | 'hasFrameClock' | 'startFrames' | 'stopFrames';
13
- type BinarySpec = Omit<Spec, 'geometrySolid' | 'geometryOptimize' | 'modelSize' | 'textureSize' | NativeFrames>;
13
+ type BinarySpec = Omit<Spec, 'speechRequest' | 'geometrySolid' | 'geometryOptimize' | 'modelSize' | 'modelBake' | 'textureSize' | 'worldgenTerrain' | 'worldgenMesh' | 'worldgenTilt' | 'worldgenMove' | NativeFrames>;
14
14
  /** The same runtime ABI on JSI and WASM, with precise TypeScript buffer and tuple contracts. */
15
15
  export type EngineBridge = {
16
16
  [K in keyof BinarySpec]: BufferMethod<BinarySpec[K]>;
17
17
  } & {
18
+ speechRequest?: Spec['speechRequest'];
18
19
  geometrySolid(recipe: string): NativeGeometry;
19
20
  geometryOptimize(vertices: ArrayBufferLike, indices: ArrayBufferLike, ratio: number, error: number, lockBorders: boolean): NativeGeometry;
20
21
  modelSize(mesh: number): [x: number, y: number, z: number];
22
+ modelBake(mesh: number, spec: string): [] | [texture: number, x: number, y: number, extent: number];
21
23
  textureSize(texture: number): [width: number, height: number];
24
+ worldgenTerrain(world: number, material: number): NativeGeometry;
25
+ worldgenMesh(recipe: string): NativeGeometry;
26
+ worldgenTilt(world: number, x: number, z: number, yaw: number): [x: number, y: number, z: number, w: number];
27
+ worldgenMove(world: number, x: number, z: number, dx: number, dz: number, radius: number, bodies: ArrayBufferLike): [x: number, z: number];
22
28
  } & {
23
29
  [K in NativeFrames]?: BufferMethod<Spec[K]>;
24
30
  };
@@ -9,6 +9,8 @@ import { type TurboModule } from 'react-native';
9
9
  * and stay valid until `destroyWorld`.
10
10
  */
11
11
  export interface Spec extends TurboModule {
12
+ /** System speech commands; JSON envelope contains value or error. */
13
+ speechRequest(command: string): Promise<string>;
12
14
  createWorld(capacity: number): number;
13
15
  destroyWorld(world: number): void;
14
16
  /** Returns the entity id, or -1 if the world is full or the description is rejected. */
@@ -98,6 +100,10 @@ export interface Spec extends TurboModule {
98
100
  modelIndices(mesh: number): Object;
99
101
  /** Full bounding-box size [x, y, z]. */
100
102
  modelSize(mesh: number): Array<number>;
103
+ /** Bakes a model into a new texture (spec: BakeSpec JSON): [texture, center x, center y, extent], or [] on failure (`loadError`). */
104
+ modelBake(mesh: number, spec: string): Array<number>;
105
+ /** Whether a baked picture is kept under `key`. */
106
+ bakeKept(key: string): boolean;
101
107
  /** The model's base color texture id, or -1. */
102
108
  modelTexture(mesh: number): number;
103
109
  /** A new mesh id with `base`'s geometry (to give it another texture), or -1. */
@@ -185,6 +191,12 @@ export interface Spec extends TurboModule {
185
191
  * scene. count 0 removes it.
186
192
  */
187
193
  renderOverlay(surface: number, matrices: Object, colors: Object, regions: Object, ranges: Object, count: number, width: number, height: number): void;
194
+ /**
195
+ * Static scenery behind the scene (the world's instance buffers, as `renderOverlay`): drawn into a
196
+ * cached texture with each frame's camera and light, and again only when this is called again or
197
+ * the view, light, size or quality change. count 0 removes it.
198
+ */
199
+ renderBackdrop(surface: number, matrices: Object, colors: Object, regions: Object, ranges: Object, count: number): void;
188
200
  renderError(): string;
189
201
  /** "vulkan", "gl" or "metal" ("" before the first surface). */
190
202
  renderBackend(): string;
@@ -202,9 +214,69 @@ export interface Spec extends TurboModule {
202
214
  audioSetVolume(volume: number): void;
203
215
  audioSetMuted(muted: boolean): void;
204
216
  audioIsRunning(): boolean;
217
+ /** Frees a sound's samples (a played recording); its id then plays nothing. */
218
+ audioRelease(sound: number): void;
219
+ /** A sound's length in seconds (0 if unknown or released). */
220
+ audioDuration(sound: number): number;
221
+ /** Loudness per `1 / perSecond` s of a sound, as a Float32 ArrayBuffer. */
222
+ audioLevels(sound: number, perSecond: number): Object;
223
+ /** 1 granted, 0 not asked yet (or not granted on Android), -1 denied. */
224
+ micPermission(): number;
225
+ /** Shows the system prompt the first time; resolves to whether recording is allowed. */
226
+ micRequestPermission(): Promise<boolean>;
227
+ /** Starts listening, keeping the last `maxSeconds` of input; true if it's opening. */
228
+ micStart(maxSeconds: number): boolean;
229
+ micStop(): void;
230
+ /** 0 off, 1 opening, 2 listening, -1 failed. */
231
+ micState(): number;
232
+ /** Input loudness now, 0..1. */
233
+ micLevel(): number;
234
+ /** Seconds of input held. */
235
+ micHeld(): number;
236
+ /** A sound id of the `seconds` of input that ended `skipEnd` seconds ago, or -1; forgets what was heard. */
237
+ micTake(seconds: number, skipEnd: number): number;
205
238
  hapticsSupported(): boolean;
206
239
  /** Taps as flat [time, intensity, sharpness, ...] triples. */
207
240
  hapticsPlay(taps: Array<number>, throttle: boolean): void;
241
+ /** Builds (or, with a `key` saved from the same recipe, restores) a world. 0 on failure (`worldgenError`). */
242
+ worldgenBuild(recipe: string, key: string, prepareTerrain: boolean): number;
243
+ /** `worldgenBuild` on a background native thread. */
244
+ worldgenBuildAsync(recipe: string, key: string, prepareTerrain: boolean): Promise<number>;
245
+ worldgenError(): string;
246
+ /** Whether a world was saved under `key`. */
247
+ worldgenKept(key: string): boolean;
248
+ worldgenFree(world: number): void;
249
+ /** Spawn, restored flag, layer sizes and picked spots as JSON. */
250
+ worldgenInfo(world: number): string;
251
+ /** A scatter layer's items (f64, stride from `worldgenInfo`). */
252
+ worldgenLayer(world: number, name: string): Object;
253
+ /** Reachable cells as f64 [x, z] pairs. */
254
+ worldgenReachable(world: number): Object;
255
+ /** Overview surface codes (u8, size × size). */
256
+ worldgenOverview(world: number): Object;
257
+ /** Re-picks the spots for `run`; false keeps the previous ones. */
258
+ worldgenPick(world: number, run: number): boolean;
259
+ /** The terrain mesh of a material, handed over once: { vertices, indices }. */
260
+ worldgenTerrain(world: number, material: number): Object;
261
+ /** Builds a procedural mesh recipe: { vertices, indices }. */
262
+ worldgenMesh(recipe: string): Object;
263
+ /** Recolors 11-float vertices in place by a field function (`{ fields, out }`) of each surface point. */
264
+ worldgenPaint(vertices: Object, recipe: string): boolean;
265
+ worldgenGround(world: number, x: number, z: number): number;
266
+ worldgenSurface(world: number, x: number, z: number): number;
267
+ /** The platform under a point, or -1. */
268
+ worldgenPlatform(world: number, x: number, z: number): number;
269
+ worldgenWalkable(world: number, x: number, z: number, radius: number): boolean;
270
+ worldgenClearance(world: number, x: number, z: number, radius: number): number;
271
+ worldgenSolidClearance(world: number, x: number, z: number, radius: number, bottom: number, top: number): number;
272
+ /** A named field's index for `worldgenField`, or -1. */
273
+ worldgenFieldIndex(world: number, name: string): number;
274
+ worldgenField(world: number, field: number, x: number, z: number): number;
275
+ /** Ground tilt as a quaternion [x, y, z, w]. */
276
+ worldgenTilt(world: number, x: number, z: number, yaw: number): Array<number>;
277
+ worldgenSight(world: number, ax: number, az: number, bx: number, bz: number, radius: number, step: number, rise: number): boolean;
278
+ /** Swept move-and-slide; `bodies` is an f64 buffer of [x, z, radius] triples. Returns [x, z]. */
279
+ worldgenMove(world: number, x: number, z: number, dx: number, dz: number, radius: number, bodies: Object): Array<number>;
208
280
  }
209
281
  declare const _default: Spec | null | undefined;
210
282
  export default _default;
@@ -15,12 +15,19 @@ import { type RenderQuality, type RenderQualitySetting } from './quality';
15
15
  export declare function afterNextFrame(fn: () => void): () => void;
16
16
  export type GameViewStats = {
17
17
  fps: number;
18
- /** Mean time spent in `onUpdate` per frame, in milliseconds. */
18
+ /** Mean time spent in `onUpdate` and stepping the worlds per frame, in milliseconds. */
19
19
  updateMs: number;
20
+ /**
21
+ * Mean JS-thread time per frame spent handing the frame and overlay to the renderer, in
22
+ * milliseconds. With a render thread this is copying only; otherwise it includes drawing.
23
+ */
24
+ publishMs: number;
20
25
  /** Average milliseconds per frame spent drawing (CPU side, including waiting on the GPU). */
21
26
  renderMs: number;
22
27
  /** The quality level drawn with (with `quality="auto"`, the one it settled on so far). */
23
28
  quality: RenderQuality;
29
+ /** The 3D scene's resolution per axis relative to the screen's: 1, or less under a pixel budget. */
30
+ sceneScale: number;
24
31
  /** The graphics API drawing: "vulkan", "gl" (OpenGL ES) or "metal". */
25
32
  backend: string;
26
33
  };
@@ -65,11 +72,22 @@ export type GameViewProps = {
65
72
  /** Distance fog density towards `background` (e.g. 0.02). Default 0 (off). */
66
73
  fog?: number;
67
74
  /**
68
- * Resolution, anti-aliasing and shadow quality. "auto" (default) starts at "high", steps down only
69
- * while the GPU really can't keep up (shadow detail first, then anti-aliasing, resolution last) and
75
+ * Resolution, anti-aliasing and shadows. "auto" (default) starts at "high", steps down only while
76
+ * the GPU really can't keep up (crisper shadows, then no shadows or anti-aliasing, resolution last) and
70
77
  * steps back up once frames are smooth again, so every phone gets the best look it can hold.
71
78
  */
72
79
  quality?: RenderQualitySetting;
80
+ /**
81
+ * Remembers the level "auto" settles on under this key (e.g. the game's id) in the engine's durable
82
+ * store, so every later view with the same key, after app restarts too, starts there rather than at "high".
83
+ */
84
+ qualityKey?: string;
85
+ /**
86
+ * Whether "auto" judges the device by the frames now (default true). Pass false while what's on
87
+ * screen isn't the game being played (a results card, a menu, a ready screen): their cost says
88
+ * nothing about the game, and a level chosen there would be remembered for the next visit.
89
+ */
90
+ adaptQuality?: boolean;
73
91
  /** Suspend updates and drawing while keeping the native surface mounted. */
74
92
  paused?: boolean;
75
93
  style?: ViewStyle;
@@ -92,6 +110,15 @@ export type GameViewProps = {
92
110
  * frame like `source`.
93
111
  */
94
112
  overlay?: RenderSource;
113
+ /**
114
+ * Static scenery behind `source` (a meadow, sky and hills; a board's backdrop): the renderer draws it
115
+ * once into a cached image with this view's camera and light, and draws it again only when it
116
+ * changes or the camera, light, size or quality do. Each frame then costs one full-screen image
117
+ * instead of the whole scenery, and `source` is always drawn in front of it. It isn't stepped (its
118
+ * velocities and animations stand still): call `backdrop.update(0)` after spawning or changing it.
119
+ * Defaults to `source.backdrop`: a World's still entities (`still: true`, `backdropLayer`).
120
+ */
121
+ backdrop?: RenderSource;
95
122
  /**
96
123
  * Touches for the overlay, in points in the view. Return true from "start" to claim the touch (a
97
124
  * button was hit): its moves and end come here, and the game gets no drag, tap or swipe for it.
@@ -6,14 +6,53 @@ export type QualityLevel = {
6
6
  maxPixelRatio: number;
7
7
  /** MSAA samples: 4 or 1 (off). */
8
8
  samples: 1 | 4;
9
+ /**
10
+ * Smooths edges after drawing (FXAA), in the pass that stretches a reduced scene to the screen (so
11
+ * only with `maxPixels`): there it costs a few texture reads per pixel. At full resolution it would
12
+ * need that whole extra pass, which costs a tile-based phone GPU more than 4x MSAA. The overlay (HUD
13
+ * text) is drawn after it, untouched.
14
+ */
15
+ fxaa?: boolean;
16
+ /** Whether shadows are drawn at all (a scene's light may still turn them off). */
17
+ shadows: boolean;
9
18
  /** Shadow map width and height in texels. */
10
19
  shadowSize: number;
11
20
  /** 3x3 filtered (soft) shadow edges, or a single tap (hard). */
12
21
  softShadows: boolean;
22
+ /**
23
+ * Most device pixels drawn per frame, whatever the screen's ratio: a cap on the pixel ratio alone
24
+ * leaves a low-density tablet (1.75x on 1200x2000) drawing its full resolution even at "lowest".
25
+ */
26
+ maxPixels?: number;
13
27
  };
14
28
  /** From best looking to cheapest; "auto" walks down this list only as far as it must. */
15
29
  export declare const QUALITIES: readonly RenderQuality[];
16
30
  export declare const QUALITY_LEVELS: Record<RenderQuality, QualityLevel>;
31
+ /**
32
+ * What "auto" walks through, from best to cheapest: a level, and the most pixels its 3D scene is drawn
33
+ * with. Before giving up an effect it draws the scene a little smaller, as native games scale their
34
+ * resolution: stretched over the screen under the full-resolution HUD, 1.5 MP (0.8x a 1200x2000
35
+ * tablet) can't be told from the screen's own resolution and 1.1 MP barely, while every effect stays.
36
+ * On the Adreno 610 tablet that took Crossy Road from 47 to 60 fps and Whack a Mole from 53 to 59 at
37
+ * "high": a tile-based GPU's cost under 4x MSAA grows with pixels times the triangles in each tile.
38
+ */
39
+ export type QualityStep = {
40
+ quality: RenderQuality;
41
+ maxPixels?: number;
42
+ };
43
+ export declare const AUTO_STEPS: readonly QualityStep[];
44
+ /** A step's level: its quality's, with the step's pixel budget when that is smaller. */
45
+ export declare function stepLevel(step: QualityStep): QualityLevel;
46
+ /** The pixel ratio a level draws a `width` x `height` point view at, on a screen of ratio `screen`. */
47
+ export declare function levelPixelRatio(level: QualityLevel, screen: number, width: number, height: number): number;
48
+ /** Durable records (the engine's Rust store); absent on builds without it, which then remember per session. */
49
+ export type QualityStore = {
50
+ cacheRead?: (key: string) => ArrayBuffer;
51
+ cacheWrite?: (key: string, bytes: ArrayBuffer) => boolean;
52
+ };
53
+ export declare function settledQuality(key: string | undefined, store?: QualityStore): number | undefined;
54
+ /** Records the step for `key`, writing it to `store` only when it changes. */
55
+ export declare function rememberQuality(key: string | undefined, step: number, store?: QualityStore): void;
17
56
  /** One second of frames, as the frame loop measured it. */
18
57
  export type FrameReport = {
19
58
  /** The typical (median) time between frames, in ms: hiccups (garbage collection, a busy JS frame) don't move it. */
@@ -22,10 +61,11 @@ export type FrameReport = {
22
61
  renderMs: number;
23
62
  };
24
63
  /**
25
- * Picks the level for "auto": starts at the best, steps down one level after `SLOW_SECONDS`
64
+ * Picks the step (of AUTO_STEPS) for "auto": starts at the best, steps down one after `SLOW_SECONDS`
26
65
  * consecutive seconds whose typical frame misses ~50 fps while drawing took most of it (after a
27
66
  * 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
67
+ * stuttering through every level), and steps back up after `FAST_SECONDS` smooth seconds (`HEADROOM_SECONDS` when
68
+ * the GPU has plenty of time to spare). A level that fails again soon after being restored
29
69
  * becomes the ceiling, so it never bounces.
30
70
  */
31
71
  export declare class AutoQuality {
@@ -38,9 +78,21 @@ export declare class AutoQuality {
38
78
  private slow;
39
79
  private verySlow;
40
80
  private fast;
81
+ private headroom;
82
+ /** `start`: the step to begin at (one remembered from an earlier visit), or a quality's first step. */
83
+ constructor(start?: number | RenderQuality);
84
+ /** The current step's index in AUTO_STEPS. */
85
+ get step(): number;
41
86
  get quality(): RenderQuality;
87
+ /** What to draw with: the step's level and pixel budget. */
88
+ get level(): QualityLevel;
42
89
  /** Feeds one second of frames; returns true when the level changed. */
43
90
  report(now: number, frames: FrameReport): boolean;
91
+ /**
92
+ * Frames that don't count (a results card over the game): forgets the streaks and, once frames
93
+ * count again, waits `SETTLE_MS` before judging, as after a change of level.
94
+ */
95
+ hold(now: number): void;
44
96
  private moved;
45
97
  }
46
98
  /** The middle value of `values` (sorted in place). */
@@ -7,6 +7,8 @@ export declare const MAX_MESHES = 4096;
7
7
  export interface RenderSource {
8
8
  /** Native world id, when this source supports skeletal characters. */
9
9
  readonly nativeWorld?: number;
10
+ /** Still scenery drawn once into a cached image behind this source (a World's still entities). */
11
+ readonly backdrop?: RenderSource | null;
10
12
  /** Column-major mat4 per instance (16 floats), grouped by mesh. Only the first `count` are valid. */
11
13
  readonly matrices: Float32Array;
12
14
  /** RGBA per instance (4 floats), same order as `matrices`. Alpha below 1 draws see-through. */
@@ -67,8 +69,13 @@ export type Light = {
67
69
  direction: [number, number, number];
68
70
  /** 0..1 ambient term. */
69
71
  ambient: number;
70
- /** Cast shadows (one directional shadow map centred on the camera target). Default true. */
71
- shadows?: boolean;
72
+ /**
73
+ * Cast shadows (one directional shadow map centred on the camera target). Default true.
74
+ * `'backdrop'`: only the cached backdrop layer draws its shadow map (and its own shadows); live
75
+ * entities still receive those shadows but cast none, so a frame skips the shadow pass. For scenes
76
+ * whose ground is all backdrop, where live shadows would land on nothing that shows them.
77
+ */
78
+ shadows?: boolean | 'backdrop';
72
79
  /** Half-size in world units of the shadowed area around the camera target. Default 30. */
73
80
  shadowExtent?: number;
74
81
  /**
@@ -45,6 +45,10 @@ export declare class World implements RenderSource {
45
45
  private boundsValue;
46
46
  private listenerValue;
47
47
  private updates;
48
+ /** The layer still entities live in, the stand-in each one has here, and whether one changed. */
49
+ private backdropValue;
50
+ private readonly stills;
51
+ private stillChanged;
48
52
  constructor(capacity: number);
49
53
  private get native();
50
54
  /** True once `dispose()` has run. Cleanup calls (despawn, set, has, queries, stopping animations
@@ -55,6 +59,18 @@ export declare class World implements RenderSource {
55
59
  get count(): number;
56
60
  /** Adds an entity (one native call; text adds one per letter). Throws if the world is full or an option is invalid. */
57
61
  spawn(options?: EntityOptions): Entity;
62
+ /**
63
+ * The still entities' layer (see `EntityOptions.still`), drawn once into a cached image behind this
64
+ * world; null until it's first used. GameView draws it; it's disposed with this world.
65
+ */
66
+ get backdrop(): World | null;
67
+ /**
68
+ * The still layer itself, made on first use with room for `capacity` entities: for a whole still
69
+ * scene (a meadow) spawned straight into it. Step it (`update(0)`) after changing it, which is
70
+ * what tells GameView to draw it again; it isn't stepped otherwise, so nothing in it moves.
71
+ */
72
+ backdropLayer(capacity?: number): World;
73
+ private spawnStill;
58
74
  /**
59
75
  * Changes an entity (one native call). Only the options you pass change; `null` removes.
60
76
  * Returns false if the entity is gone, the world is disposed, or an option was rejected (the others still apply).
@@ -138,6 +138,14 @@ export type EntityOptions = {
138
138
  impact?: ImpactFeedback | null;
139
139
  /** Whether `pick` can hit it. Default true. */
140
140
  pickable?: boolean;
141
+ /**
142
+ * Scenery that never moves (a board's background, a sky): drawn with the world's other still
143
+ * entities into one cached image behind everything else (`World.backdrop`), redrawn only when one
144
+ * of them changes, the camera moves or the light changes, instead of every frame. Spawn-only. The
145
+ * returned entity takes `set` and `despawn` (and goes with the world); it can't be attached, have
146
+ * physics or text, be animated or be picked, and everything live draws in front of it.
147
+ */
148
+ still?: boolean;
141
149
  };
142
150
  export type Easing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'back' | 'bounce' | 'elastic';
143
151
  /** What `animate` changes. Anything left out stays as it is (and keeps any animation it already has). */
@@ -0,0 +1,10 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.VIBRATE" />
3
+
4
+ <!-- Android 11+ package visibility for installed system speech engines. -->
5
+ <queries>
6
+ <intent>
7
+ <action android:name="android.intent.action.TTS_SERVICE" />
8
+ </intent>
9
+ </queries>
10
+ </manifest>
@@ -0,0 +1,60 @@
1
+ package com.nayanui.engine;
2
+
3
+ import android.Manifest;
4
+ import android.app.Activity;
5
+ import android.content.Context;
6
+ import android.content.pm.PackageManager;
7
+ import com.facebook.react.bridge.ReactContext;
8
+ import com.facebook.react.bridge.UiThreadUtil;
9
+ import com.facebook.react.modules.core.PermissionAwareActivity;
10
+ import java.lang.ref.WeakReference;
11
+
12
+ /**
13
+ * The microphone permission for the engine's recording (`microphone` in JS). The app declares
14
+ * RECORD_AUDIO in its manifest; this asks for it at runtime through React Native's
15
+ * permission-aware activity and reports the answer to the C++ module, which resolves the JS promise.
16
+ */
17
+ public final class EngineMicrophone {
18
+ private static final int REQUEST_CODE = 0x4e4d; // "NM"
19
+ private static WeakReference<ReactContext> react = new WeakReference<>(null);
20
+
21
+ private EngineMicrophone() {}
22
+
23
+ /** The React context to ask through (set by the package and by every engine view). */
24
+ static void remember(Context context) {
25
+ if (context instanceof ReactContext) react = new WeakReference<>((ReactContext) context);
26
+ }
27
+
28
+ /** 1 if recording is allowed, 0 if not (yet). */
29
+ public static int permission() {
30
+ ReactContext context = react.get();
31
+ if (context == null) return 0;
32
+ return context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED ? 1 : 0;
33
+ }
34
+
35
+ /** Asks for the microphone (the system prompt) and reports through nativePermissionResult(token, granted). */
36
+ public static void request(final long token) {
37
+ UiThreadUtil.runOnUiThread(() -> {
38
+ if (permission() == 1) {
39
+ nativePermissionResult(token, true);
40
+ return;
41
+ }
42
+ ReactContext context = react.get();
43
+ Activity activity = context == null ? null : context.getCurrentActivity();
44
+ if (!(activity instanceof PermissionAwareActivity)) {
45
+ nativePermissionResult(token, false);
46
+ return;
47
+ }
48
+ ((PermissionAwareActivity) activity).requestPermissions(
49
+ new String[] {Manifest.permission.RECORD_AUDIO},
50
+ REQUEST_CODE,
51
+ (requestCode, permissions, results) -> {
52
+ if (requestCode != REQUEST_CODE) return false;
53
+ nativePermissionResult(token, results.length > 0 && results[0] == PackageManager.PERMISSION_GRANTED);
54
+ return true;
55
+ });
56
+ });
57
+ }
58
+
59
+ private static native void nativePermissionResult(long token, boolean granted);
60
+ }
@@ -0,0 +1,28 @@
1
+ package com.nayanui.engine;
2
+
3
+ import android.content.Context;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
6
+ import android.speech.tts.TextToSpeech;
7
+
8
+ /** Main-thread and Java interface glue only. Speech commands and lifecycle live in Rust. */
9
+ public final class EngineSpeech {
10
+ private static final Handler MAIN = new Handler(Looper.getMainLooper());
11
+ private EngineSpeech() {}
12
+
13
+ public static boolean dispatch(long token) {
14
+ return MAIN.post(() -> nativeRun(token));
15
+ }
16
+
17
+ public static TextToSpeech create(Context context, long generation) {
18
+ final TextToSpeech[] holder = new TextToSpeech[1];
19
+ holder[0] = new TextToSpeech(context, status ->
20
+ MAIN.post(() -> nativeInitialized(holder[0], status, generation)));
21
+ MAIN.postDelayed(() -> nativeInitTimeout(generation), 15000);
22
+ return holder[0];
23
+ }
24
+
25
+ private static native void nativeRun(long token);
26
+ private static native void nativeInitTimeout(long generation);
27
+ private static native void nativeInitialized(TextToSpeech synth, int status, long generation);
28
+ }
@@ -73,6 +73,7 @@ public class EngineSurfaceView extends SurfaceView implements SurfaceHolder.Call
73
73
 
74
74
  public EngineSurfaceView(Context context) {
75
75
  super(context);
76
+ EngineMicrophone.remember(context);
76
77
  getHolder().addCallback(this);
77
78
  // Frames are opaque: the compositor can skip blending the layer.
78
79
  getHolder().setFormat(PixelFormat.OPAQUE);
@@ -13,12 +13,14 @@ public class NayanEnginePackage implements ReactPackage {
13
13
  @NonNull
14
14
  @Override
15
15
  public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext context) {
16
+ EngineMicrophone.remember(context);
16
17
  return Collections.emptyList();
17
18
  }
18
19
 
19
20
  @NonNull
20
21
  @Override
21
22
  public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext context) {
23
+ EngineMicrophone.remember(context);
22
24
  return Collections.singletonList(new EngineSurfaceViewManager());
23
25
  }
24
26
  }
@@ -184,6 +184,9 @@ uint32_t engine_model_vertex_count(uint32_t mesh);
184
184
  const uint32_t *engine_model_indices(uint32_t mesh);
185
185
  uint32_t engine_model_index_count(uint32_t mesh);
186
186
  int32_t engine_model_size(uint32_t mesh, float *out_xyz); // full bounding-box size; 1 if known
187
+ // Bakes a model into a new texture (BakeSpec JSON); out = [texture, center x, center y, extent]. 1, or 0 (engine_load_error).
188
+ int32_t engine_model_bake(uint32_t mesh, const uint8_t *spec, size_t len, float *out);
189
+ int32_t engine_bake_kept(const uint8_t *key, size_t len); // 1 if a picture is kept under the key
187
190
  int32_t engine_model_texture(uint32_t mesh); // base color texture id, or -1
188
191
  // A new mesh id drawn with base's geometry (so it can have its own texture). Returns it, or -1.
189
192
  int32_t engine_mesh_alias(uint32_t base);
@@ -251,6 +254,35 @@ int32_t engine_audio_load_wav(const uint8_t *data, size_t len); // sound id, or
251
254
  uint64_t engine_audio_play(uint32_t sound, float volume, float pan, float pitch, int32_t looping); // voice id, 0 = none
252
255
  void engine_audio_stop(uint64_t voice);
253
256
  void engine_audio_set_volume(float volume);
257
+ // Frees a sound's samples (a played recording); its id then plays nothing.
258
+ void engine_audio_release(uint32_t sound);
259
+ // A sound's length in seconds (0 if unknown or released).
260
+ float engine_audio_duration(uint32_t sound);
261
+ // Writes up to `capacity` loudness values (one per 1/per_second s) into `out`; returns how many the
262
+ // sound has (pass a null `out` to ask).
263
+ size_t engine_audio_levels(uint32_t sound, float per_second, float *out, size_t capacity);
264
+ /* System speech: reply bytes are borrowed for the callback; completion means queued, not spoken. */
265
+ void engine_speech_request(const uint8_t *data, size_t len, void (*done)(void *context, const uint8_t *data, size_t len), void *context);
266
+ void engine_speech_shutdown(void);
267
+
268
+ // Microphone. Permission: 1 granted, 0 not asked yet, -1 denied (iOS; 1 elsewhere, the host asks).
269
+ int32_t engine_audio_mic_permission(void);
270
+ // Asks for the microphone and calls done(context, granted) once, possibly on another thread.
271
+ void engine_audio_mic_request(void (*done)(void *context, int32_t granted), void *context);
272
+ // Starts listening, keeping the last `max_seconds` of input. 1 if it's opening.
273
+ int32_t engine_audio_mic_start(float max_seconds);
274
+ void engine_audio_mic_stop(void);
275
+ // 0 off, 1 opening, 2 listening, -1 failed (no device or permission).
276
+ int32_t engine_audio_mic_state(void);
277
+ // How loud the input is now: 0 (-60 dBFS or quieter) .. 1 (full scale).
278
+ float engine_audio_mic_level(void);
279
+ // Seconds of input held.
280
+ float engine_audio_mic_held(void);
281
+ // A sound of the `seconds` of input that ended `skip_end` seconds ago; forgets what was heard.
282
+ // Returns its sound id, or -1.
283
+ int32_t engine_audio_mic_take(float seconds, float skip_end);
284
+ // Browser input: `count` mono samples at `rate`.
285
+ void engine_audio_mic_push(const float *samples, size_t count, uint32_t rate);
254
286
  void engine_audio_set_muted(int32_t muted);
255
287
  int32_t engine_audio_is_running(void);
256
288
 
@@ -281,7 +313,7 @@ size_t engine_render_mesh_vertices(uint32_t mesh, float *out, size_t cap);
281
313
  size_t engine_render_mesh_indices(uint32_t mesh, uint32_t *out, size_t cap);
282
314
 
283
315
  // ── Renderer (wgpu: Vulkan, else GLES 3.0, on Android; Metal on iOS) ───────
284
- #define ENGINE_FRAME_LEN 26
316
+ #define ENGINE_FRAME_LEN 26 // at least: the scene scale (26) and FXAA (27) are optional
285
317
  // Geometry (copied) drawn for a mesh id: vertex_count * 11 floats, u32 indices; texture -1 = none.
286
318
  void engine_render_mesh(uint32_t mesh, const float *vertices, size_t vertex_count, const uint32_t *indices, size_t index_count,
287
319
  int32_t texture, float shine);
@@ -305,6 +337,8 @@ int32_t engine_render_frame(int32_t surface, const double *frame, size_t frame_l
305
337
  // Unlit and always in front of the scene. count 0 (or NULL buffers) removes it.
306
338
  void engine_render_overlay(int32_t surface, const float *matrices, const float *colors, const float *regions, size_t capacity,
307
339
  const uint32_t *ranges, size_t ranges_len, size_t count, float width, float height);
340
+ // Static scenery behind the scene, cached in a texture (same buffers as engine_render_overlay; count 0 removes it).
341
+ void engine_render_backdrop(int32_t surface, const float *matrices, const float *colors, const float *regions, size_t capacity, const uint32_t *ranges, size_t ranges_len, size_t count);
308
342
  size_t engine_render_error(char *out, size_t cap);
309
343
  // "vulkan", "gl", "metal", or "" before the first attach.
310
344
  size_t engine_render_backend(char *out, size_t cap);
@@ -326,6 +360,34 @@ void engine_drawing_transform(int32_t document, float dx, float dy, float scale)
326
360
  void engine_drawing_invalidate(int32_t document);
327
361
  int32_t engine_drawing_present(int32_t document, int32_t surface, float width, float height, uint32_t background_rgb);
328
362
 
363
+ // Data-driven procedural worlds (JSON recipes of field graphs, terrain, solids and generation steps).
364
+ // Handles are u32 (0 = failed; see engine_worldgen_error). Building is reentrant on any thread. With
365
+ // a non-empty key, a world saved under it from the same recipe is restored, and a new one is saved.
366
+ // Out-buffer functions return the full length and copy only when `capacity` is large enough.
367
+ uint32_t engine_worldgen_build(const uint8_t *recipe, size_t len, const uint8_t *key, size_t key_len, int32_t prepare_terrain);
368
+ size_t engine_worldgen_error(uint8_t *out, size_t capacity);
369
+ int32_t engine_worldgen_kept(const uint8_t *key, size_t len);
370
+ void engine_worldgen_free(uint32_t world);
371
+ size_t engine_worldgen_info(uint32_t world, uint8_t *out, size_t capacity);
372
+ size_t engine_worldgen_layer(uint32_t world, const uint8_t *name, size_t len, double *out, size_t capacity);
373
+ size_t engine_worldgen_reachable(uint32_t world, double *out, size_t capacity);
374
+ size_t engine_worldgen_overview(uint32_t world, uint8_t *out, size_t capacity);
375
+ int32_t engine_worldgen_pick(uint32_t world, uint32_t run);
376
+ EngineGeometry *engine_worldgen_terrain(uint32_t world, uint32_t material);
377
+ EngineGeometry *engine_worldgen_mesh(const uint8_t *recipe, size_t len);
378
+ int32_t engine_worldgen_paint(float *vertices, size_t vertex_count, const uint8_t *recipe, size_t len);
379
+ double engine_worldgen_ground(uint32_t world, double x, double z);
380
+ int32_t engine_worldgen_surface(uint32_t world, double x, double z);
381
+ int32_t engine_worldgen_platform(uint32_t world, double x, double z);
382
+ int32_t engine_worldgen_walkable(uint32_t world, double x, double z, double radius);
383
+ double engine_worldgen_clearance(uint32_t world, double x, double z, double radius);
384
+ double engine_worldgen_solid_clearance(uint32_t world, double x, double z, double radius, double bottom, double top);
385
+ int32_t engine_worldgen_field_index(uint32_t world, const uint8_t *name, size_t len);
386
+ double engine_worldgen_field(uint32_t world, uint32_t field, double x, double z);
387
+ void engine_worldgen_tilt(uint32_t world, double x, double z, double yaw, double *out);
388
+ int32_t engine_worldgen_sight(uint32_t world, double ax, double az, double bx, double bz, double radius, double step, double rise);
389
+ void engine_worldgen_move(uint32_t world, double *point, double dx, double dz, double radius, const double *bodies, size_t count);
390
+
329
391
  #ifdef __cplusplus
330
392
  }
331
393
  #endif