@nayan-ui/engine 0.0.0-stage → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NayanEngine.podspec +28 -0
- package/README.md +210 -2
- package/lib/module/assets/AssetBytesCache.js +76 -0
- package/lib/module/assets/AssetCache.js +19 -0
- package/lib/module/assets/AssetLoader.js +52 -0
- package/lib/module/assets/CacheGeneration.js +7 -0
- package/lib/module/assets/IndexedDbCache.web.js +149 -0
- package/lib/module/assets/ProceduralCache.js +222 -0
- package/lib/module/assets/font.js +136 -0
- package/lib/module/assets/loft.js +142 -0
- package/lib/module/assets/model.js +74 -0
- package/lib/module/assets/modelling.js +254 -0
- package/lib/module/assets/shape.js +926 -0
- package/lib/module/assets/source.js +58 -0
- package/lib/module/assets/source.web.js +22 -0
- package/lib/module/assets/storage.js +4 -0
- package/lib/module/assets/storage.shared.js +36 -0
- package/lib/module/assets/storage.web.js +37 -0
- package/lib/module/assets/surface.js +47 -0
- package/lib/module/assets/texture.js +66 -0
- package/lib/module/drawing/DrawingCanvas.js +38 -0
- package/lib/module/drawing/DrawingCanvas.web.js +206 -0
- package/lib/module/drawing/DrawingDocument.js +256 -0
- package/lib/module/drawing/types.js +4 -0
- package/lib/module/globals.d.ts +12 -0
- package/lib/module/index.js +36 -0
- package/lib/module/input/Joystick.js +84 -0
- package/lib/module/input/Joystick.web.js +93 -0
- package/lib/module/input/keyboard.js +250 -0
- package/lib/module/media/audio-worklet.js +34 -0
- package/lib/module/media/audio.js +94 -0
- package/lib/module/media/haptics.js +82 -0
- package/lib/module/media/output.web.js +66 -0
- package/lib/module/native/BrowserEngine.js +551 -0
- package/lib/module/native/EngineBridge.js +4 -0
- package/lib/module/native/NativeNayanEngine.js +7 -0
- package/lib/module/native/NativeNayanEngine.web.js +62 -0
- package/lib/module/native/prepare.js +7 -0
- package/lib/module/native/prepare.web.js +17 -0
- package/lib/module/native/spec/NativeNayanEngine.js +17 -0
- package/lib/module/package.json +1 -0
- package/lib/module/render/GameView.js +249 -0
- package/lib/module/render/camera.js +34 -0
- package/lib/module/render/host.js +39 -0
- package/lib/module/render/host.web.js +104 -0
- package/lib/module/render/meshes.js +159 -0
- package/lib/module/render/quality.js +112 -0
- package/lib/module/render/responder.js +4 -0
- package/lib/module/render/responder.web.js +97 -0
- package/lib/module/render/touch.js +185 -0
- package/lib/module/types.js +30 -0
- package/lib/module/world/Scoreboard.js +143 -0
- package/lib/module/world/TransformBatch.js +126 -0
- package/lib/module/world/World.js +690 -0
- package/lib/module/world/desc.js +186 -0
- package/lib/module/world/effects.js +451 -0
- package/lib/module/world/quat.js +86 -0
- package/lib/module/world/types.js +4 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/assets/AssetBytesCache.d.ts +7 -0
- package/lib/typescript/src/assets/AssetCache.d.ts +9 -0
- package/lib/typescript/src/assets/AssetLoader.d.ts +11 -0
- package/lib/typescript/src/assets/CacheGeneration.d.ts +3 -0
- package/lib/typescript/src/assets/IndexedDbCache.web.d.ts +20 -0
- package/lib/typescript/src/assets/ProceduralCache.d.ts +33 -0
- package/lib/typescript/src/assets/font.d.ts +53 -0
- package/lib/typescript/src/assets/loft.d.ts +31 -0
- package/lib/typescript/src/assets/model.d.ts +48 -0
- package/lib/typescript/src/assets/modelling.d.ts +157 -0
- package/lib/typescript/src/assets/shape.d.ts +203 -0
- package/lib/typescript/src/assets/source.d.ts +10 -0
- package/lib/typescript/src/assets/source.web.d.ts +8 -0
- package/lib/typescript/src/assets/storage.d.ts +2 -0
- package/lib/typescript/src/assets/storage.shared.d.ts +7 -0
- package/lib/typescript/src/assets/storage.web.d.ts +7 -0
- package/lib/typescript/src/assets/surface.d.ts +17 -0
- package/lib/typescript/src/assets/texture.d.ts +30 -0
- package/lib/typescript/src/drawing/DrawingCanvas.d.ts +5 -0
- package/lib/typescript/src/drawing/DrawingCanvas.web.d.ts +4 -0
- package/lib/typescript/src/drawing/DrawingDocument.d.ts +210 -0
- package/lib/typescript/src/drawing/types.d.ts +12 -0
- package/lib/typescript/src/index.d.ts +42 -0
- package/lib/typescript/src/input/Joystick.d.ts +20 -0
- package/lib/typescript/src/input/Joystick.web.d.ts +13 -0
- package/lib/typescript/src/input/keyboard.d.ts +17 -0
- package/lib/typescript/src/media/audio.d.ts +46 -0
- package/lib/typescript/src/media/haptics.d.ts +22 -0
- package/lib/typescript/src/media/output.web.d.ts +4 -0
- package/lib/typescript/src/native/BrowserEngine.d.ts +8 -0
- package/lib/typescript/src/native/EngineBridge.d.ts +22 -0
- package/lib/typescript/src/native/NativeNayanEngine.d.ts +5 -0
- package/lib/typescript/src/native/NativeNayanEngine.web.d.ts +10 -0
- package/lib/typescript/src/native/prepare.d.ts +3 -0
- package/lib/typescript/src/native/prepare.web.d.ts +2 -0
- package/lib/typescript/src/native/spec/NativeNayanEngine.d.ts +182 -0
- package/lib/typescript/src/render/GameView.d.ts +95 -0
- package/lib/typescript/src/render/camera.d.ts +6 -0
- package/lib/typescript/src/render/host.d.ts +29 -0
- package/lib/typescript/src/render/host.web.d.ts +16 -0
- package/lib/typescript/src/render/meshes.d.ts +45 -0
- package/lib/typescript/src/render/quality.d.ts +46 -0
- package/lib/typescript/src/render/responder.d.ts +2 -0
- package/lib/typescript/src/render/responder.web.d.ts +40 -0
- package/lib/typescript/src/render/touch.d.ts +26 -0
- package/lib/typescript/src/types.d.ts +72 -0
- package/lib/typescript/src/world/Scoreboard.d.ts +65 -0
- package/lib/typescript/src/world/TransformBatch.d.ts +39 -0
- package/lib/typescript/src/world/World.d.ts +199 -0
- package/lib/typescript/src/world/desc.d.ts +17 -0
- package/lib/typescript/src/world/effects.d.ts +156 -0
- package/lib/typescript/src/world/quat.d.ts +31 -0
- package/lib/typescript/src/world/types.d.ts +233 -0
- package/lib/web/audio-worklet.js +29 -0
- package/lib/web/engine_core_bg.wasm +0 -0
- package/lib/web/index.js +7974 -0
- package/lib/web/types/src/assets/AssetBytesCache.d.ts +6 -0
- package/lib/web/types/src/assets/AssetCache.d.ts +8 -0
- package/lib/web/types/src/assets/AssetLoader.d.ts +10 -0
- package/lib/web/types/src/assets/CacheGeneration.d.ts +2 -0
- package/lib/web/types/src/assets/IndexedDbCache.d.ts +19 -0
- package/lib/web/types/src/assets/ProceduralCache.d.ts +32 -0
- package/lib/web/types/src/assets/font.d.ts +52 -0
- package/lib/web/types/src/assets/loft.d.ts +30 -0
- package/lib/web/types/src/assets/model.d.ts +47 -0
- package/lib/web/types/src/assets/modelling.d.ts +156 -0
- package/lib/web/types/src/assets/shape.d.ts +202 -0
- package/lib/web/types/src/assets/source.d.ts +7 -0
- package/lib/web/types/src/assets/storage.d.ts +6 -0
- package/lib/web/types/src/assets/storage.shared.d.ts +6 -0
- package/lib/web/types/src/assets/surface.d.ts +16 -0
- package/lib/web/types/src/assets/texture.d.ts +29 -0
- package/lib/web/types/src/drawing/DrawingCanvas.d.ts +3 -0
- package/lib/web/types/src/drawing/DrawingDocument.d.ts +209 -0
- package/lib/web/types/src/drawing/types.d.ts +11 -0
- package/lib/web/types/src/index.d.ts +41 -0
- package/lib/web/types/src/input/Joystick.d.ts +12 -0
- package/lib/web/types/src/input/keyboard.d.ts +16 -0
- package/lib/web/types/src/media/audio.d.ts +45 -0
- package/lib/web/types/src/media/haptics.d.ts +21 -0
- package/lib/web/types/src/media/output.d.ts +3 -0
- package/lib/web/types/src/native/BrowserEngine.d.ts +7 -0
- package/lib/web/types/src/native/EngineBridge.d.ts +21 -0
- package/lib/web/types/src/native/NativeNayanEngine.d.ts +9 -0
- package/lib/web/types/src/native/prepare.d.ts +1 -0
- package/lib/web/types/src/native/spec/NativeNayanEngine.d.ts +181 -0
- package/lib/web/types/src/render/GameView.d.ts +94 -0
- package/lib/web/types/src/render/camera.d.ts +5 -0
- package/lib/web/types/src/render/host.d.ts +15 -0
- package/lib/web/types/src/render/meshes.d.ts +44 -0
- package/lib/web/types/src/render/quality.d.ts +45 -0
- package/lib/web/types/src/render/responder.d.ts +39 -0
- package/lib/web/types/src/render/touch.d.ts +25 -0
- package/lib/web/types/src/types.d.ts +71 -0
- package/lib/web/types/src/world/Scoreboard.d.ts +64 -0
- package/lib/web/types/src/world/TransformBatch.d.ts +38 -0
- package/lib/web/types/src/world/World.d.ts +198 -0
- package/lib/web/types/src/world/desc.d.ts +16 -0
- package/lib/web/types/src/world/effects.d.ts +155 -0
- package/lib/web/types/src/world/quat.d.ts +30 -0
- package/lib/web/types/src/world/types.d.ts +232 -0
- package/native/android/CMakeLists.txt +34 -0
- package/native/android/NayanEngineSpec.h +15 -0
- package/native/android/build.gradle +26 -0
- package/native/android/codegen/CMakeLists.txt +3 -0
- package/native/android/proguard-rules.pro +2 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceView.java +234 -0
- package/native/android/src/main/java/com/nayanui/engine/EngineSurfaceViewManager.java +45 -0
- package/native/android/src/main/java/com/nayanui/engine/NayanEnginePackage.java +24 -0
- package/native/core/build/EngineCore.xcframework/Info.plist +43 -0
- package/native/core/build/EngineCore.xcframework/ios-arm64/libengine_core.a +0 -0
- package/native/core/build/EngineCore.xcframework/ios-arm64-simulator/libengine_core.a +0 -0
- package/native/core/build/android/arm64-v8a/libengine_core.a +0 -0
- package/native/core/build/android/armeabi-v7a/libengine_core.a +0 -0
- package/native/core/build/android/x86/libengine_core.a +0 -0
- package/native/core/build/android/x86_64/libengine_core.a +0 -0
- package/native/core/include/drawing-licenses.txt +58 -0
- package/native/core/include/engine_core.h +322 -0
- package/native/cpp/NayanEngineModule.cpp +827 -0
- package/native/cpp/NayanEngineModule.h +144 -0
- package/native/cpp/generated/NayanEngineSpecJSI.cpp +2 -0
- package/native/cpp/generated/NayanEngineSpecJSI.h +921 -0
- package/native/ios/NayanEngineProvider.h +5 -0
- package/native/ios/NayanEngineProvider.mm +21 -0
- package/native/ios/NayanEngineSurface.mm +223 -0
- package/native/scripts/check-package.mjs +39 -0
- package/package.json +162 -4
- package/react-native.config.js +18 -0
- package/src/native/spec/NativeNayanEngine.ts +226 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { layoutText } from "../assets/font.js";
|
|
4
|
+
import { texturedMesh } from "../assets/texture.js";
|
|
5
|
+
import NativeEngine from '../native/NativeNayanEngine';
|
|
6
|
+
import { SHAPE_MESH } from "../types.js";
|
|
7
|
+
import { ANIM_HEADER, ANIM_KEY_LEN, BURST_LEN, DESC_LEN, EASING, encode } from "./desc.js";
|
|
8
|
+
import { Scoreboard } from "./Scoreboard.js";
|
|
9
|
+
import { TRANSFORM_STRIDE, TransformBatch } from "./TransformBatch.js";
|
|
10
|
+
/** True when the engine's native code is linked into this build (it is not in Expo Go). */
|
|
11
|
+
export const isEngineAvailable = NativeEngine != null;
|
|
12
|
+
const KEY_FLAG = {
|
|
13
|
+
position: 1,
|
|
14
|
+
rotation: 2,
|
|
15
|
+
scale: 4,
|
|
16
|
+
color: 8,
|
|
17
|
+
moveBy: 16,
|
|
18
|
+
turn: 32,
|
|
19
|
+
shake: 64,
|
|
20
|
+
at: 128
|
|
21
|
+
};
|
|
22
|
+
const EVENT_STRIDE = 4;
|
|
23
|
+
const EVENT_STARTED = 1;
|
|
24
|
+
const EVENT_SENSOR = 2;
|
|
25
|
+
const meshId = mesh => typeof mesh === 'string' ? SHAPE_MESH[mesh] : mesh.id;
|
|
26
|
+
const WHITE = [1, 1, 1];
|
|
27
|
+
const ORIGIN = [0, 0, 0];
|
|
28
|
+
const UP = [0, 1, 0];
|
|
29
|
+
function writeRgba(target, offset, c) {
|
|
30
|
+
target[offset] = c[0];
|
|
31
|
+
target[offset + 1] = c[1];
|
|
32
|
+
target[offset + 2] = c[2];
|
|
33
|
+
target[offset + 3] = c[3] ?? 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Writes into a hit's tuple in place (typed readonly for callers, owned by the `out` object). */
|
|
37
|
+
function writeVec3(target, x, y, z) {
|
|
38
|
+
const t = target;
|
|
39
|
+
t[0] = x;
|
|
40
|
+
t[1] = y;
|
|
41
|
+
t[2] = z;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A game world, simulated by the Rust core: entities, motion, Rapier physics, collisions,
|
|
46
|
+
* animations and raycasts at a fixed 60 Hz, with rendering smoothed between steps. Pass it to `<GameView>`.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const world = new World(500);
|
|
50
|
+
* const ball = world.spawn({ mesh: "sphere", position: [0, 5, 0], physics: "dynamic" });
|
|
51
|
+
* world.set(ball, { color: [1, 0, 0] });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export class World {
|
|
55
|
+
desc = new Float64Array(DESC_LEN);
|
|
56
|
+
anim = new Float64Array(ANIM_HEADER + 1 + ANIM_KEY_LEN);
|
|
57
|
+
burstDesc = new Float64Array(BURST_LEN);
|
|
58
|
+
info = {
|
|
59
|
+
started: false,
|
|
60
|
+
sensor: false,
|
|
61
|
+
speed: 0
|
|
62
|
+
};
|
|
63
|
+
/** Promise resolvers of running animations, by animation id. */
|
|
64
|
+
pending = new Map();
|
|
65
|
+
/** Callbacks of pending `after` timers, by timer id (the core counts the time). */
|
|
66
|
+
timers = new Map();
|
|
67
|
+
/** Timers for a native core built before `after` existed: [fire time, callback], on `clock`. */
|
|
68
|
+
fallbackTimers = [];
|
|
69
|
+
fallbackTimerId = 0;
|
|
70
|
+
transformsValue = null;
|
|
71
|
+
clock = 0;
|
|
72
|
+
scoreboardValue = null;
|
|
73
|
+
texts = new Map();
|
|
74
|
+
disposed = false;
|
|
75
|
+
gravityValue = [0, -9.81, 0];
|
|
76
|
+
boundsValue = null;
|
|
77
|
+
listenerValue = null;
|
|
78
|
+
constructor(capacity) {
|
|
79
|
+
this.capacity = capacity;
|
|
80
|
+
if (!NativeEngine) {
|
|
81
|
+
throw new Error('@nayan-ui/engine: the native core is not linked into this build (Expo Go?).');
|
|
82
|
+
}
|
|
83
|
+
this.id = NativeEngine.createWorld(capacity);
|
|
84
|
+
this.matrices = new Float32Array(NativeEngine.getMatrices(this.id));
|
|
85
|
+
this.colors = new Float32Array(NativeEngine.getColors(this.id));
|
|
86
|
+
this.regions = new Float32Array(NativeEngine.getRegions(this.id));
|
|
87
|
+
this.ranges = new Uint32Array(NativeEngine.getRanges(this.id));
|
|
88
|
+
const events = NativeEngine.getEvents(this.id);
|
|
89
|
+
this.events = new Uint32Array(events);
|
|
90
|
+
this.eventFloats = new Float32Array(events);
|
|
91
|
+
this.finished = new Uint32Array(NativeEngine.getDone(this.id));
|
|
92
|
+
this.scratch = new Float32Array(NativeEngine.getScratch(this.id));
|
|
93
|
+
}
|
|
94
|
+
get native() {
|
|
95
|
+
if (this.disposed) throw new Error('World: used after dispose()');
|
|
96
|
+
return NativeEngine;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True once `dispose()` has run. Cleanup calls (despawn, set, has, queries, stopping animations
|
|
100
|
+
* and timers) are safe no-ops afterwards, so late animation promises and timers need no guard.
|
|
101
|
+
*/
|
|
102
|
+
get isDisposed() {
|
|
103
|
+
return this.disposed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Live entities (text letters count too). */
|
|
107
|
+
get count() {
|
|
108
|
+
return this.disposed ? 0 : this.native.count(this.id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Entities ─────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/** Adds an entity (one native call; text adds one per letter). Throws if the world is full or an option is invalid. */
|
|
114
|
+
spawn(options = {}) {
|
|
115
|
+
if (options.parent != null && options.physics != null) {
|
|
116
|
+
throw new Error("World.spawn: attached entities can't have physics (put it on the parent)");
|
|
117
|
+
}
|
|
118
|
+
if (options.text !== undefined && !options.font) throw new Error('World.spawn: text needs a font (see loadFont)');
|
|
119
|
+
encode(this.desc, options, this.resolveMesh(options, 'spawn'));
|
|
120
|
+
const e = this.native.spawnDesc(this.id, this.desc.buffer);
|
|
121
|
+
if (e < 0) {
|
|
122
|
+
throw new Error(this.count >= this.capacity ? `World.spawn: the world is full (capacity ${this.capacity})` : 'World.spawn: invalid options (sizes and density must be positive, the parent must exist)');
|
|
123
|
+
}
|
|
124
|
+
if (options.text !== undefined) {
|
|
125
|
+
const state = {
|
|
126
|
+
font: options.font,
|
|
127
|
+
text: options.text,
|
|
128
|
+
align: options.align ?? 'center',
|
|
129
|
+
layout: {
|
|
130
|
+
letterSpacing: options.letterSpacing,
|
|
131
|
+
tabular: options.tabular
|
|
132
|
+
},
|
|
133
|
+
color: options.color ?? [1, 1, 1],
|
|
134
|
+
glyphs: []
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
this.buildText(e, state);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.native.despawn(this.id, e); // takes the letters spawned so far with it
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
this.texts.set(e, state);
|
|
143
|
+
}
|
|
144
|
+
return e;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Changes an entity (one native call). Only the options you pass change; `null` removes.
|
|
149
|
+
* Returns false if the entity is gone, the world is disposed, or an option was rejected (the others still apply).
|
|
150
|
+
*/
|
|
151
|
+
set(e, options) {
|
|
152
|
+
if (this.disposed) return false;
|
|
153
|
+
const text = this.texts.get(e);
|
|
154
|
+
// A text entity stays an invisible root; its letters are rebuilt or recolored below.
|
|
155
|
+
encode(this.desc, options, text ? undefined : this.resolveMesh(options, 'set'));
|
|
156
|
+
const ok = this.native.setDesc(this.id, e, this.desc.buffer);
|
|
157
|
+
if (ok && text) {
|
|
158
|
+
const rebuild = options.text !== undefined && options.text !== text.text || options.font !== undefined && options.font !== text.font || options.align !== undefined && options.align !== text.align || options.letterSpacing !== undefined && options.letterSpacing !== text.layout.letterSpacing || options.tabular !== undefined && options.tabular !== text.layout.tabular;
|
|
159
|
+
text.text = options.text ?? text.text;
|
|
160
|
+
text.font = options.font ?? text.font;
|
|
161
|
+
text.align = options.align ?? text.align;
|
|
162
|
+
text.layout = {
|
|
163
|
+
letterSpacing: options.letterSpacing ?? text.layout.letterSpacing,
|
|
164
|
+
tabular: options.tabular ?? text.layout.tabular
|
|
165
|
+
};
|
|
166
|
+
if (options.color) text.color = options.color;
|
|
167
|
+
if (rebuild) this.buildText(e, text);else if (options.color) {
|
|
168
|
+
const desc = this.encodeColor(options.color);
|
|
169
|
+
for (const g of text.glyphs) this.native.setDesc(this.id, g, desc);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return ok;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* This world's shared draw-only transform batch (created on first use). Queue per-frame moves with
|
|
177
|
+
* its allocation-free methods after `update()`, then `flush()` once before the frame is drawn.
|
|
178
|
+
*/
|
|
179
|
+
get transforms() {
|
|
180
|
+
return this.transformsValue ??= this.createTransformBatch();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Reusable draw-only transforms; use set() for text entities and flush before animations/queries. */
|
|
184
|
+
createTransformBatch() {
|
|
185
|
+
return new TransformBatch((data, count) => {
|
|
186
|
+
const native = this.native;
|
|
187
|
+
if (typeof native.setTransforms === 'function') return native.setTransforms(this.id, data.buffer, count);
|
|
188
|
+
// Existing native builds remain usable until the host app is rebuilt.
|
|
189
|
+
let applied = 0;
|
|
190
|
+
for (let i = 0; i < count; i++) {
|
|
191
|
+
const offset = i * TRANSFORM_STRIDE;
|
|
192
|
+
this.desc[0] = data[offset + 1];
|
|
193
|
+
for (let k = 2; k < TRANSFORM_STRIDE; k++) this.desc[k] = data[offset + k];
|
|
194
|
+
if (native.setDesc(this.id, data[offset], this.desc.buffer)) applied++;
|
|
195
|
+
}
|
|
196
|
+
return applied;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Removes an entity and anything attached to it. Returns false if it was already gone. */
|
|
201
|
+
despawn(e) {
|
|
202
|
+
if (this.disposed) return false;
|
|
203
|
+
const existed = this.native.despawn(this.id, e);
|
|
204
|
+
this.texts.delete(e);
|
|
205
|
+
this.resolveFinished(); // its animations, and those of attached entities that went with it
|
|
206
|
+
return existed;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A one-off push: jump, explosion, knockback (dynamic bodies only). */
|
|
210
|
+
impulse(e, impulse) {
|
|
211
|
+
if (this.disposed) return;
|
|
212
|
+
this.native.impulse(this.id, e, impulse[0], impulse[1], impulse[2]);
|
|
213
|
+
}
|
|
214
|
+
resolveMesh(o, method) {
|
|
215
|
+
if (o.text !== undefined) return SHAPE_MESH.none;
|
|
216
|
+
if (o.texture) {
|
|
217
|
+
if (o.mesh === undefined && method === 'set') throw new Error('World.set: pass `mesh` together with `texture`');
|
|
218
|
+
return texturedMesh(meshId(o.mesh ?? 'cube'), o.texture);
|
|
219
|
+
}
|
|
220
|
+
return o.mesh === undefined ? undefined : meshId(o.mesh);
|
|
221
|
+
}
|
|
222
|
+
encodeColor(color) {
|
|
223
|
+
encode(this.desc, {
|
|
224
|
+
color
|
|
225
|
+
}, undefined);
|
|
226
|
+
return this.desc.buffer;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Updates a text entity's letters in place, growing or shrinking only when needed. */
|
|
230
|
+
buildText(root, text) {
|
|
231
|
+
let used = 0;
|
|
232
|
+
for (const {
|
|
233
|
+
mesh,
|
|
234
|
+
x,
|
|
235
|
+
y
|
|
236
|
+
} of layoutText(text.font, text.text, text.align, text.layout)) {
|
|
237
|
+
encode(this.desc, {
|
|
238
|
+
parent: root,
|
|
239
|
+
position: [x, y, 0],
|
|
240
|
+
color: text.color,
|
|
241
|
+
pickable: false
|
|
242
|
+
}, mesh);
|
|
243
|
+
const existing = text.glyphs[used];
|
|
244
|
+
if (existing !== undefined && this.native.setDesc(this.id, existing, this.desc.buffer)) {
|
|
245
|
+
used++;
|
|
246
|
+
} else {
|
|
247
|
+
const g = this.native.spawnDesc(this.id, this.desc.buffer);
|
|
248
|
+
if (g < 0) throw new Error(`World: no room for text "${text.text}" (capacity ${this.capacity})`);
|
|
249
|
+
text.glyphs[used++] = g;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (let i = used; i < text.glyphs.length; i++) this.native.despawn(this.id, text.glyphs[i]);
|
|
253
|
+
text.glyphs.length = used;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Native world backing this render source; bone palettes stay in Rust. */
|
|
257
|
+
get nativeWorld() {
|
|
258
|
+
return this.id;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Play a named GLB clip. Re-selecting it changes speed without restarting; `time` explicitly seeks.
|
|
262
|
+
* `fade` crossfades from the current pose (seconds). A speed of zero holds a pose for a ready screen.
|
|
263
|
+
*/
|
|
264
|
+
playClip(entity, clip, options = {}) {
|
|
265
|
+
if (this.disposed) return false;
|
|
266
|
+
return this.native.playClip(this.id, entity, clip, options.loop ?? true, options.speed ?? 1, options.fade ?? 0.15, options.time ?? -1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Hold the current GLB pose, or return to its bind pose with `reset`. */
|
|
270
|
+
stopClip(entity, reset = false) {
|
|
271
|
+
if (this.disposed) return false;
|
|
272
|
+
return this.native.stopClip(this.id, entity, reset);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Sample selected animated vertices in model space for foot contact and attachments.
|
|
276
|
+
* Reuse both arrays. `after` previews the interpolated pose following the next update.
|
|
277
|
+
*/
|
|
278
|
+
sampleSkinnedVertices(entity, vertices, output, after = 0) {
|
|
279
|
+
const indices = vertices.byteOffset === 0 && vertices.byteLength === vertices.buffer.byteLength ? vertices : vertices.slice();
|
|
280
|
+
const points = output.byteOffset === 0 && output.byteLength === output.buffer.byteLength ? output : new Float32Array(output.length);
|
|
281
|
+
const ok = this.native.skinPoints(this.id, entity, indices.buffer, points.buffer, after);
|
|
282
|
+
if (ok && points !== output) output.set(points);
|
|
283
|
+
return ok;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Animation and effects ────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Animates in Rust, with no per-frame JS: from where things are to `to`, or through keyframes.
|
|
290
|
+
* Each property (position, rotation, scale, color, shake) runs on its own, so a piece can move
|
|
291
|
+
* while it pulses; animating a property again replaces only that property. Pass several entities
|
|
292
|
+
* to animate them together (with `stagger` for waves). Resolves when every part has finished, been
|
|
293
|
+
* replaced or stopped, or its entity was despawned.
|
|
294
|
+
*
|
|
295
|
+
* ```ts
|
|
296
|
+
* await world.animate(piece, { position: [2, 0, 3] }, { duration: 0.25, easing: "back" });
|
|
297
|
+
* world.animate(coin, { scale: 1.2 }, { repeat: "forever", yoyo: true }); // pulse
|
|
298
|
+
* world.animate(piece, [{ moveBy: [1, 1.5, 0] }, { moveBy: [2, 0, 0] }], { path: "smooth" }); // hop
|
|
299
|
+
* world.animate(card, { turn: [0, Math.PI, 0] }); // flip
|
|
300
|
+
* world.animate(tiles, { scale: 1 }, { stagger: 0.03, easing: "back" }); // reveal a board
|
|
301
|
+
* world.animate(cursor, { position: [x, 0, z] }, { spring: true }); // follows the finger smoothly
|
|
302
|
+
* world.animate(wrongTile, { shake: 0.15 }, { duration: 0.4 });
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
animate(targets, to, options = {}) {
|
|
306
|
+
if (this.disposed) return Promise.resolve();
|
|
307
|
+
const single = typeof targets === 'number';
|
|
308
|
+
const entityCount = single ? 1 : targets.length;
|
|
309
|
+
const keyed = Array.isArray(to);
|
|
310
|
+
const keyCount = keyed ? to.length : 1;
|
|
311
|
+
if (entityCount === 0 || keyCount === 0) return Promise.resolve();
|
|
312
|
+
if (keyCount > 64) throw new Error('World.animate: at most 64 keyframes');
|
|
313
|
+
const length = ANIM_HEADER + entityCount + keyCount * ANIM_KEY_LEN;
|
|
314
|
+
if (this.anim.length < length) this.anim = new Float64Array(length * 2);
|
|
315
|
+
const a = this.anim;
|
|
316
|
+
a.fill(0, 0, length);
|
|
317
|
+
const spring = options.spring === true ? {} : options.spring === 'bouncy' ? {
|
|
318
|
+
stiffness: 220,
|
|
319
|
+
damping: 12
|
|
320
|
+
} : options.spring || null;
|
|
321
|
+
a[0] = entityCount;
|
|
322
|
+
a[1] = keyCount;
|
|
323
|
+
a[2] = options.duration ?? 0.3;
|
|
324
|
+
a[3] = options.delay ?? 0;
|
|
325
|
+
a[4] = EASING[options.easing ?? 'easeOut'];
|
|
326
|
+
a[5] = options.repeat === 'forever' ? -1 : options.repeat ?? 0;
|
|
327
|
+
a[6] = options.yoyo ? 1 : 0;
|
|
328
|
+
a[7] = options.stagger ?? 0;
|
|
329
|
+
a[8] = options.path === 'smooth' ? 1 : 0;
|
|
330
|
+
a[9] = spring ? spring.stiffness ?? 170 : 0;
|
|
331
|
+
a[10] = spring?.damping ?? 26;
|
|
332
|
+
a[11] = spring?.mass ?? 1;
|
|
333
|
+
if (single) a[ANIM_HEADER] = targets;else a.set(targets, ANIM_HEADER);
|
|
334
|
+
const first = ANIM_HEADER + entityCount;
|
|
335
|
+
if (keyed) for (let j = 0; j < keyCount; j++) this.encodeKeyframe(a, first + j * ANIM_KEY_LEN, to[j]);else this.encodeKeyframe(a, first, to);
|
|
336
|
+
const id = this.native.animate(this.id, a.buffer);
|
|
337
|
+
this.resolveFinished(); // animations this one replaced
|
|
338
|
+
if (id === 0) return Promise.resolve();
|
|
339
|
+
return new Promise(resolve => this.pending.set(id, resolve));
|
|
340
|
+
}
|
|
341
|
+
encodeKeyframe(a, o, k) {
|
|
342
|
+
let flags = 0;
|
|
343
|
+
if (k.at !== undefined) {
|
|
344
|
+
flags |= KEY_FLAG.at;
|
|
345
|
+
a[o + 1] = k.at;
|
|
346
|
+
}
|
|
347
|
+
if (k.position) {
|
|
348
|
+
flags |= KEY_FLAG.position;
|
|
349
|
+
a.set(k.position, o + 2);
|
|
350
|
+
}
|
|
351
|
+
if (k.rotation) {
|
|
352
|
+
flags |= KEY_FLAG.rotation;
|
|
353
|
+
a.set(k.rotation, o + 5);
|
|
354
|
+
}
|
|
355
|
+
if (k.scale !== undefined) {
|
|
356
|
+
flags |= KEY_FLAG.scale;
|
|
357
|
+
if (typeof k.scale === 'number') a.fill(k.scale, o + 9, o + 12);else a.set(k.scale, o + 9);
|
|
358
|
+
}
|
|
359
|
+
if (k.color) {
|
|
360
|
+
flags |= KEY_FLAG.color;
|
|
361
|
+
a[o + 12] = k.color[0];
|
|
362
|
+
a[o + 13] = k.color[1];
|
|
363
|
+
a[o + 14] = k.color[2];
|
|
364
|
+
a[o + 15] = k.color[3] ?? 1;
|
|
365
|
+
}
|
|
366
|
+
if (k.moveBy) {
|
|
367
|
+
flags |= KEY_FLAG.moveBy;
|
|
368
|
+
a.set(k.moveBy, o + 16);
|
|
369
|
+
}
|
|
370
|
+
if (k.turn) {
|
|
371
|
+
flags |= KEY_FLAG.turn;
|
|
372
|
+
a.set(k.turn, o + 19);
|
|
373
|
+
}
|
|
374
|
+
if (k.shake !== undefined) {
|
|
375
|
+
flags |= KEY_FLAG.shake;
|
|
376
|
+
a[o + 22] = k.shake;
|
|
377
|
+
}
|
|
378
|
+
a[o] = flags;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Stops every animation on the entity where it is (their promises resolve). */
|
|
382
|
+
stopAnimation(e) {
|
|
383
|
+
if (this.disposed) return;
|
|
384
|
+
this.native.stopAnimation(this.id, e);
|
|
385
|
+
this.resolveFinished();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Particles: many small, short-lived pieces flying out of a point (explosions, sparks, dust,
|
|
390
|
+
* confetti). One native call; they fall, spin, shrink away and clean themselves up. `endSize` and
|
|
391
|
+
* `endColor` grow / shrink and recolor them over their life (fire cooling, smoke thinning out).
|
|
392
|
+
* Returns how many were spawned (fewer if the world is nearly full).
|
|
393
|
+
*/
|
|
394
|
+
burst(options) {
|
|
395
|
+
const b = this.burstDesc;
|
|
396
|
+
const color = options.color ?? WHITE;
|
|
397
|
+
const single = typeof color[0] === 'number';
|
|
398
|
+
const colorCount = single ? 1 : Math.max(1, Math.min(4, color.length));
|
|
399
|
+
b.set(options.position, 0);
|
|
400
|
+
b.set(options.direction ?? UP, 3);
|
|
401
|
+
b[6] = options.spread ?? Math.PI;
|
|
402
|
+
b[7] = options.count ?? 16;
|
|
403
|
+
b[8] = meshId(options.mesh ?? 'cube');
|
|
404
|
+
b[9] = options.size ?? 0.15;
|
|
405
|
+
b[10] = options.speed ?? 5;
|
|
406
|
+
b[11] = options.lifetime ?? 0.8;
|
|
407
|
+
b[12] = options.gravity ?? -9.81;
|
|
408
|
+
b[13] = colorCount;
|
|
409
|
+
for (let k = 0; k < colorCount; k++) writeRgba(b, 14 + k * 4, single ? color : color[k]);
|
|
410
|
+
b[30] = options.endSize ?? 1;
|
|
411
|
+
const end = options.endColor;
|
|
412
|
+
b[31] = end ? 1 : 0;
|
|
413
|
+
if (end) writeRgba(b, 32, end);
|
|
414
|
+
return this.native.burst(this.id, b.buffer);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Runs `fn` after `seconds` of `update` time (game time: it pauses when updates stop). The clock is
|
|
419
|
+
* kept in Rust, so there's no JS timer list to scan every frame. Due callbacks run inside `update`,
|
|
420
|
+
* earliest first. Returns the timer's id for `cancelTimer`; `cancelTimers()` drops every pending one.
|
|
421
|
+
* After `dispose()` it does nothing and returns 0.
|
|
422
|
+
*
|
|
423
|
+
* ```ts
|
|
424
|
+
* world.after(0.3, () => checkMatches());
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
after(seconds, fn) {
|
|
428
|
+
if (this.disposed) return 0;
|
|
429
|
+
const native = this.native;
|
|
430
|
+
if (typeof native.after !== 'function') {
|
|
431
|
+
// Negative ids keep native-core builds without `after` cancellable too.
|
|
432
|
+
const id = -++this.fallbackTimerId;
|
|
433
|
+
this.fallbackTimers.push({
|
|
434
|
+
id,
|
|
435
|
+
at: this.clock + Math.max(0, seconds || 0),
|
|
436
|
+
run: fn
|
|
437
|
+
});
|
|
438
|
+
return id;
|
|
439
|
+
}
|
|
440
|
+
const id = native.after(this.id, seconds);
|
|
441
|
+
if (id > 0) this.timers.set(id, fn);
|
|
442
|
+
return id;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Drops one pending `after` timer so its callback never runs. Returns false if it already ran or was dropped. */
|
|
446
|
+
cancelTimer(id) {
|
|
447
|
+
if (id < 0) {
|
|
448
|
+
const index = this.fallbackTimers.findIndex(t => t.id === id);
|
|
449
|
+
if (index < 0) return false;
|
|
450
|
+
this.fallbackTimers.splice(index, 1);
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
// The core still reports the id when it falls due; without a callback it is ignored.
|
|
454
|
+
return this.timers.delete(id);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Drops every pending `after` timer: their callbacks never run (a new round, a reset). */
|
|
458
|
+
cancelTimers() {
|
|
459
|
+
this.fallbackTimers = [];
|
|
460
|
+
if (this.timers.size === 0 || this.disposed) return;
|
|
461
|
+
this.timers.clear();
|
|
462
|
+
const native = this.native;
|
|
463
|
+
if (typeof native.cancelTimers === 'function') native.cancelTimers(this.id);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Score, lives and the game clock, kept in Rust (created on first use). */
|
|
467
|
+
get scoreboard() {
|
|
468
|
+
return this.scoreboardValue ??= new Scoreboard(this.id);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Timers waiting to fire. */
|
|
472
|
+
get timerCount() {
|
|
473
|
+
return this.timers.size + this.fallbackTimers.length;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Resolves the promises of animations the core reports as ended, then runs due timers. */
|
|
477
|
+
resolveFinished() {
|
|
478
|
+
if (this.pending.size === 0 && this.timers.size === 0) return;
|
|
479
|
+
const n = this.native.doneLength(this.id);
|
|
480
|
+
let due = null;
|
|
481
|
+
for (let i = 0; i < n; i++) {
|
|
482
|
+
const id = this.finished[i];
|
|
483
|
+
const resolve = this.pending.get(id);
|
|
484
|
+
if (resolve) {
|
|
485
|
+
this.pending.delete(id);
|
|
486
|
+
resolve();
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const run = this.timers.get(id);
|
|
490
|
+
if (run) {
|
|
491
|
+
this.timers.delete(id);
|
|
492
|
+
(due ??= []).push(run);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Collected first: a callback may start timers or animations (which read the done list again).
|
|
496
|
+
if (due) for (const run of due) if (!this.disposed) run();
|
|
497
|
+
}
|
|
498
|
+
runFallbackTimers(dt) {
|
|
499
|
+
if (dt > 0 && Number.isFinite(dt)) this.clock += dt;
|
|
500
|
+
if (this.fallbackTimers.length === 0) return;
|
|
501
|
+
const now = this.clock;
|
|
502
|
+
const due = this.fallbackTimers.filter(t => t.at <= now).sort((a, b) => a.at - b.at);
|
|
503
|
+
if (due.length === 0) return;
|
|
504
|
+
this.fallbackTimers = this.fallbackTimers.filter(t => t.at > now);
|
|
505
|
+
for (const t of due) if (!this.disposed) t.run();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Queries ──────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Current simulated position (relative to the parent when attached), or null if the entity is gone.
|
|
512
|
+
* Pass `out` to avoid allocating every frame.
|
|
513
|
+
*/
|
|
514
|
+
position(e, out) {
|
|
515
|
+
if (this.disposed) return null;
|
|
516
|
+
return this.native.readPosition(this.id, e, false) ? this.copyScratch(out) : null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Where the entity was last drawn, in world space (parents and smoothing included), or null if it's gone. */
|
|
520
|
+
worldPosition(e, out) {
|
|
521
|
+
if (this.disposed) return null;
|
|
522
|
+
return this.native.readPosition(this.id, e, true) ? this.copyScratch(out) : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Current velocity, or null if the entity is gone. Pass `out` to avoid allocating every frame. */
|
|
526
|
+
velocity(e, out) {
|
|
527
|
+
if (this.disposed) return null;
|
|
528
|
+
return this.native.readVelocity(this.id, e) ? this.copyScratch(out) : null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** True while the entity exists (false after `dispose()`). */
|
|
532
|
+
has(e) {
|
|
533
|
+
if (this.disposed) return false;
|
|
534
|
+
return this.native.readPosition(this.id, e, false);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* First solid (non-sensor) collider along the ray whose layer is in `mask`, as of the last update.
|
|
539
|
+
* Pass `out` to reuse a hit object every frame.
|
|
540
|
+
*/
|
|
541
|
+
raycast(origin, direction, maxDistance, mask = -1, out) {
|
|
542
|
+
if (this.disposed) return null;
|
|
543
|
+
const hit = this.native.raycast(this.id, origin[0], origin[1], origin[2], direction[0], direction[1], direction[2], maxDistance, mask);
|
|
544
|
+
if (hit < 0) return null;
|
|
545
|
+
const s = this.scratch;
|
|
546
|
+
if (!out) return {
|
|
547
|
+
entity: hit,
|
|
548
|
+
distance: s[0],
|
|
549
|
+
normal: [s[1], s[2], s[3]],
|
|
550
|
+
point: [s[4], s[5], s[6]]
|
|
551
|
+
};
|
|
552
|
+
out.entity = hit;
|
|
553
|
+
out.distance = s[0];
|
|
554
|
+
writeVec3(out.normal, s[1], s[2], s[3]);
|
|
555
|
+
writeVec3(out.point, s[4], s[5], s[6]);
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The entity under a point of the GameView (e.g. a tap's `x, y`), tested against what was last
|
|
561
|
+
* drawn. Needs no physics. Returns null if nothing pickable is there. Pass `out` to reuse a hit object.
|
|
562
|
+
*/
|
|
563
|
+
pick(x, y, out) {
|
|
564
|
+
if (this.disposed) return null;
|
|
565
|
+
const hit = this.native.pickScreen(this.id, x, y);
|
|
566
|
+
if (hit < 0) return null;
|
|
567
|
+
const s = this.scratch;
|
|
568
|
+
if (!out) return {
|
|
569
|
+
entity: hit,
|
|
570
|
+
distance: s[0],
|
|
571
|
+
point: [s[1], s[2], s[3]]
|
|
572
|
+
};
|
|
573
|
+
out.entity = hit;
|
|
574
|
+
out.distance = s[0];
|
|
575
|
+
writeVec3(out.point, s[1], s[2], s[3]);
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Where a world position appears in the GameView, in points from its top-left, or null if it's behind
|
|
581
|
+
* the camera (or nothing has been drawn yet). Place React Native views over 3D objects with it.
|
|
582
|
+
* Pass `out` to avoid allocating every frame.
|
|
583
|
+
*/
|
|
584
|
+
toScreen(position, out) {
|
|
585
|
+
if (this.disposed || !this.native.toScreen(this.id, position[0], position[1], position[2])) return null;
|
|
586
|
+
if (!out) return [this.scratch[0], this.scratch[1]];
|
|
587
|
+
out[0] = this.scratch[0];
|
|
588
|
+
out[1] = this.scratch[1];
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Where a GameView point (a finger's `x, y`) lands on a plane, or null if the plane is behind the
|
|
594
|
+
* camera or edge-on. Defaults to the ground (y = 0). Drag a piece across a board with it:
|
|
595
|
+
* `world.screenToWorld(x, y, { point: [0, pieceHeight, 0] })`. 2D games looking down -z use
|
|
596
|
+
* `{ normal: [0, 0, 1] }`. Like `toScreen`, it needs a drawn frame (null before the first one).
|
|
597
|
+
* Pass `out` to avoid allocating every frame.
|
|
598
|
+
*/
|
|
599
|
+
screenToWorld(x, y, plane, out) {
|
|
600
|
+
if (this.disposed) return null;
|
|
601
|
+
const p = plane?.point ?? ORIGIN;
|
|
602
|
+
const n = plane?.normal ?? UP;
|
|
603
|
+
if (!this.native.screenToWorld(this.id, x, y, p[0], p[1], p[2], n[0], n[1], n[2])) return null;
|
|
604
|
+
return this.copyScratch(out);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Called by GameView after drawing a frame, with the camera matrix and the view size in points. */
|
|
608
|
+
setView(viewProj, width, height) {
|
|
609
|
+
this.native.setView(this.id, viewProj.buffer, width, height);
|
|
610
|
+
}
|
|
611
|
+
copyScratch(out = [0, 0, 0]) {
|
|
612
|
+
out[0] = this.scratch[0];
|
|
613
|
+
out[1] = this.scratch[1];
|
|
614
|
+
out[2] = this.scratch[2];
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ── World settings ───────────────────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
/** Default [0, -9.81, 0]. */
|
|
621
|
+
get gravity() {
|
|
622
|
+
return this.gravityValue;
|
|
623
|
+
}
|
|
624
|
+
set gravity(g) {
|
|
625
|
+
this.gravityValue = g;
|
|
626
|
+
this.native.setGravity(this.id, g[0], g[1], g[2]);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Keeps moving non-physics entities inside [minX, minZ, maxX, maxZ]. Physics bodies need walls. */
|
|
630
|
+
get bounds() {
|
|
631
|
+
return this.boundsValue;
|
|
632
|
+
}
|
|
633
|
+
set bounds(b) {
|
|
634
|
+
this.boundsValue = b;
|
|
635
|
+
const [minX, minZ, maxX, maxZ] = b ?? [-1e9, -1e9, 1e9, 1e9];
|
|
636
|
+
this.native.setBounds(this.id, minX, minZ, maxX, maxZ);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Impact sounds pan and fade relative to this entity (usually the player). */
|
|
640
|
+
get listener() {
|
|
641
|
+
return this.listenerValue;
|
|
642
|
+
}
|
|
643
|
+
set listener(e) {
|
|
644
|
+
this.listenerValue = e;
|
|
645
|
+
this.native.setListener(this.id, e ?? -1);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ── Simulation ───────────────────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
/** Advances the simulation (fixed 60 Hz steps), resolves finished animations, refreshes what GameView draws. */
|
|
651
|
+
update(dt) {
|
|
652
|
+
this.native.update(this.id, dt);
|
|
653
|
+
this.resolveFinished();
|
|
654
|
+
this.runFallbackTimers(dt);
|
|
655
|
+
if (!this.disposed) this.scoreboardValue?.dispatch();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Calls `fn` for each touch that started or ended during the last `update`. `info` is reused:
|
|
660
|
+
* copy what you need. Either entity may already be despawned by an earlier callback.
|
|
661
|
+
*/
|
|
662
|
+
forEachCollision(fn) {
|
|
663
|
+
if (this.disposed) return;
|
|
664
|
+
const length = this.native.eventLength(this.id);
|
|
665
|
+
const info = this.info;
|
|
666
|
+
for (let i = 0; i + EVENT_STRIDE <= length; i += EVENT_STRIDE) {
|
|
667
|
+
const flags = this.events[i + 2];
|
|
668
|
+
info.started = (flags & EVENT_STARTED) !== 0;
|
|
669
|
+
info.sensor = (flags & EVENT_SENSOR) !== 0;
|
|
670
|
+
info.speed = this.eventFloats[i + 3];
|
|
671
|
+
fn(this.events[i], this.events[i + 1], info);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Frees the native world. Pending animation promises resolve and timers are dropped. Later cleanup
|
|
677
|
+
* calls and queries are no-ops (see `isDisposed`); `spawn` and `update` throw.
|
|
678
|
+
*/
|
|
679
|
+
dispose() {
|
|
680
|
+
if (this.disposed) return;
|
|
681
|
+
this.disposed = true;
|
|
682
|
+
this.transformsValue?.clear();
|
|
683
|
+
for (const resolve of this.pending.values()) resolve();
|
|
684
|
+
this.pending.clear();
|
|
685
|
+
this.timers.clear();
|
|
686
|
+
this.fallbackTimers = [];
|
|
687
|
+
NativeEngine.destroyWorld(this.id);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
//# sourceMappingURL=World.js.map
|