@series-inc/rundot-3d-engine 0.6.9 → 0.6.11

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/SKILL.md ADDED
@@ -0,0 +1,366 @@
1
+ # Rundot 3D Engine
2
+
3
+ The Rundot 3D Engine (`@series-inc/rundot-3d-engine`) is a Three.js-based game engine with ECS architecture, Rapier physics, and StowKit asset integration.
4
+
5
+ ## Quick Start
6
+
7
+ ```typescript
8
+ import { VenusGame, GameObject, Component } from "@series-inc/rundot-3d-engine"
9
+ import { MeshRenderer, RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-inc/rundot-3d-engine/systems"
10
+
11
+ class MyGame extends VenusGame {
12
+ protected async onStart(): Promise<void> {
13
+ // Load assets
14
+ const stowkit = StowKitSystem.getInstance()
15
+ const buildJson = (await import("../prefabs/build.json")).default
16
+ await stowkit.loadFromBuildJson(buildJson, {
17
+ fetchBlob: (path) => fetch(path).then(r => r.blob()),
18
+ })
19
+
20
+ // Create a player
21
+ const player = new GameObject("Player")
22
+ player.position.set(0, 1, 0)
23
+
24
+ // Add mesh
25
+ const meshObj = new GameObject("PlayerMesh")
26
+ meshObj.addComponent(new MeshRenderer("player_mesh"))
27
+ player.add(meshObj)
28
+
29
+ // Add physics
30
+ player.addComponent(new RigidBodyComponentThree({
31
+ type: RigidBodyType.DYNAMIC,
32
+ shape: ColliderShape.CAPSULE,
33
+ radius: 0.5,
34
+ height: 2,
35
+ lockRotationX: true,
36
+ lockRotationY: true,
37
+ lockRotationZ: true,
38
+ }))
39
+
40
+ // Add lighting
41
+ const light = new THREE.DirectionalLight(0xffffff, 1)
42
+ light.position.set(5, 10, 5)
43
+ this.scene.add(light)
44
+ }
45
+
46
+ protected preRender(deltaTime: number): void {
47
+ // Per-frame logic
48
+ }
49
+
50
+ protected async onDispose(): Promise<void> {
51
+ // Cleanup
52
+ }
53
+ }
54
+
55
+ MyGame.create()
56
+ ```
57
+
58
+ ## Core Architecture
59
+
60
+ ### VenusGame — Game Base Class
61
+
62
+ Manages renderer, scene, camera, and all engine systems.
63
+
64
+ ```typescript
65
+ class MyGame extends VenusGame {
66
+ protected getConfig(): VenusGameConfig {
67
+ return {
68
+ backgroundColor: 0x87CEEB,
69
+ antialias: true,
70
+ shadowMapEnabled: true,
71
+ shadowMapType: "vsm", // or "pcf_soft"
72
+ toneMapping: "aces", // "aces" | "linear" | "none"
73
+ toneMappingExposure: 1.0,
74
+ audioEnabled: true,
75
+ cameraType: "perspective", // or "orthographic"
76
+ orthoSize: 10, // half-height for ortho camera
77
+ }
78
+ }
79
+
80
+ protected async onStart(): Promise<void> { /* init game */ }
81
+ protected preRender(deltaTime: number): void { /* per-frame logic */ }
82
+ protected async onDispose(): Promise<void> { /* cleanup */ }
83
+ }
84
+ ```
85
+
86
+ **Static access** (after initialization):
87
+ - `VenusGame.scene` — Three.js scene
88
+ - `VenusGame.camera` — active camera
89
+ - `VenusGame.renderer` — WebGL renderer
90
+ - `VenusGame.instance` — game instance
91
+
92
+ **Lifecycle order:** Physics → Tweens → Components → `preRender()` → `render()`
93
+
94
+ **IMPORTANT:** Use the `deltaTime` parameter in `preRender()`. Never call `this.clock.getDelta()`.
95
+
96
+ ### GameObject — Entity Class
97
+
98
+ Extends `THREE.Object3D`. Auto-added to scene on creation.
99
+
100
+ ```typescript
101
+ const obj = new GameObject("MyObject")
102
+ obj.position.set(0, 1, 0)
103
+ obj.addComponent(new MyComponent())
104
+
105
+ // Hierarchy
106
+ const child = new GameObject("Child")
107
+ obj.add(child)
108
+
109
+ // Component access
110
+ const comp = obj.getComponent(MyComponent)
111
+ obj.hasComponent(MyComponent)
112
+ obj.removeComponent(MyComponent)
113
+
114
+ // Enable/disable
115
+ obj.setEnabled(false) // triggers onDisabled() on all components
116
+ obj.setEnabled(true) // triggers onEnabled()
117
+
118
+ // Cleanup
119
+ obj.dispose() // disposes all components and children
120
+ ```
121
+
122
+ ### Component — Behavior Base Class
123
+
124
+ ```typescript
125
+ class MyComponent extends Component {
126
+ protected onCreate(): void {
127
+ // Called when added to GameObject — init here
128
+ }
129
+
130
+ public update(deltaTime: number): void {
131
+ // Called every frame
132
+ this.gameObject.position.x += deltaTime
133
+ }
134
+
135
+ public lateUpdate(deltaTime: number): void {
136
+ // Called after all update() calls — camera follow, UI updates
137
+ }
138
+
139
+ public onEnabled(): void { /* GameObject enabled */ }
140
+ public onDisabled(): void { /* GameObject disabled */ }
141
+
142
+ protected onCleanup(): void {
143
+ // Called on removal or dispose — cleanup listeners, resources
144
+ }
145
+ }
146
+ ```
147
+
148
+ **Access from Component:**
149
+ - `this.gameObject` — the attached GameObject
150
+ - `this.scene` — the Three.js scene
151
+ - `this.getComponent(Type)` — get sibling component
152
+
153
+ ## Systems
154
+
155
+ ### MeshRenderer — Static Mesh Display
156
+
157
+ ```typescript
158
+ import { MeshRenderer } from "@series-inc/rundot-3d-engine/systems"
159
+
160
+ // ALWAYS use child GameObject pattern
161
+ const renderer = new MeshRenderer("asset_name", castShadow?, receiveShadow?, isStatic?, materialOverride?)
162
+ const meshObj = new GameObject("Mesh")
163
+ meshObj.addComponent(renderer)
164
+ parentGameObject.add(meshObj)
165
+
166
+ // Check if loaded
167
+ renderer.isLoaded()
168
+ renderer.getBounds()
169
+ renderer.setVisible(false)
170
+ renderer.setMaterial(customMaterial)
171
+ ```
172
+
173
+ **Static meshes** (`isStatic: true`) skip per-frame matrix updates — use for non-moving objects.
174
+
175
+ ### SkeletalRenderer — Animated Characters
176
+
177
+ ```typescript
178
+ import { SkeletalRenderer } from "@series-inc/rundot-3d-engine/systems"
179
+
180
+ const skelRenderer = new SkeletalRenderer("character_mesh")
181
+ const meshObj = new GameObject("CharacterMesh")
182
+ meshObj.addComponent(skelRenderer)
183
+ character.add(meshObj)
184
+ ```
185
+
186
+ ### RigidBodyComponentThree — Physics
187
+
188
+ ```typescript
189
+ import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-inc/rundot-3d-engine/systems"
190
+
191
+ // Dynamic (affected by physics)
192
+ new RigidBodyComponentThree({
193
+ type: RigidBodyType.DYNAMIC,
194
+ shape: ColliderShape.BOX, // BOX, SPHERE, CAPSULE
195
+ size: new THREE.Vector3(1, 1, 1), // box dimensions
196
+ radius: 0.5, // sphere/capsule radius
197
+ height: 2, // capsule height
198
+ mass: 1.0,
199
+ friction: 0.5,
200
+ restitution: 0.8, // bounciness
201
+ linearDamping: 0.5,
202
+ angularDamping: 0.5,
203
+ lockRotationX/Y/Z: true, // lock rotation axes
204
+ fitToMesh: true, // auto-size from mesh bounds
205
+ })
206
+
207
+ // Static (immovable)
208
+ new RigidBodyComponentThree({ type: RigidBodyType.STATIC, shape: ColliderShape.BOX, size: ... })
209
+
210
+ // Kinematic (script-controlled)
211
+ new RigidBodyComponentThree({ type: RigidBodyType.KINEMATIC, ... })
212
+
213
+ // Trigger (sensor, no physics response)
214
+ new RigidBodyComponentThree({ type: RigidBodyType.STATIC, shape: ColliderShape.BOX, isSensor: true })
215
+ rb.registerOnTriggerEnter((other) => console.log("entered", other.name))
216
+ rb.registerOnTriggerExit((other) => console.log("exited", other.name))
217
+
218
+ // Velocity & forces (dynamic only)
219
+ rb.setVelocity(new THREE.Vector3(0, 5, 0))
220
+ rb.applyImpulse(new THREE.Vector3(10, 0, 0))
221
+ rb.applyForce(new THREE.Vector3(0, -9.8, 0))
222
+ ```
223
+
224
+ ### AnimationGraphComponent — State Machine Animations
225
+
226
+ ```typescript
227
+ import { AnimationGraphComponent } from "@series-inc/rundot-3d-engine/systems"
228
+
229
+ const config = {
230
+ parameters: {
231
+ speed: { type: "float", default: 0 },
232
+ isGrounded: { type: "bool", default: true },
233
+ },
234
+ states: {
235
+ idle: { animation: "idle" },
236
+ walk: { animation: "walk" },
237
+ run: { animation: "run" },
238
+ locomotion: {
239
+ tree: {
240
+ parameter: "speed",
241
+ children: [
242
+ { animation: "idle", threshold: 0 },
243
+ { animation: "walk", threshold: 1 },
244
+ { animation: "run", threshold: 2 },
245
+ ],
246
+ },
247
+ },
248
+ },
249
+ transitions: [
250
+ { from: "idle", to: "walk", when: { speed: 1 } },
251
+ { from: "walk", to: "run", when: { speed: 2 } },
252
+ { from: "attack", to: "idle", exitTime: 1.0 }, // after anim finishes
253
+ ],
254
+ initialState: "idle",
255
+ }
256
+
257
+ const animGraph = new AnimationGraphComponent(model, config)
258
+ character.addComponent(animGraph)
259
+
260
+ // Drive transitions via parameters
261
+ animGraph.setParameter("speed", 2)
262
+ animGraph.setState("attack") // direct state change
263
+ animGraph.getCurrentState()
264
+ ```
265
+
266
+ ### StowKitSystem — Asset Loading
267
+
268
+ ```typescript
269
+ import { StowKitSystem } from "@series-inc/rundot-3d-engine/systems"
270
+
271
+ const stowkit = StowKitSystem.getInstance()
272
+
273
+ // Load from build.json
274
+ await stowkit.loadFromBuildJson(buildJson, {
275
+ fetchBlob: (path) => fetch(path).then(r => r.blob()),
276
+ })
277
+
278
+ // Access assets
279
+ const mesh = await stowkit.getMesh("name") // async
280
+ const mesh = stowkit.getMeshSync("name") // sync (null if not loaded)
281
+ const tex = await stowkit.getTexture("name")
282
+ const clip = await stowkit.getAnimation("walk", "character_mesh")
283
+ const audio = await stowkit.getAudio("sfx_click")
284
+ const skinned = await stowkit.getSkinnedMesh("character", 1.0)
285
+
286
+ // Clone with shadow settings
287
+ const clone = await stowkit.cloneMesh("name", castShadow, receiveShadow)
288
+
289
+ // GPU instancing
290
+ await stowkit.registerMeshForInstancing("coin_batch", "coin_mesh", true, true, 500)
291
+ ```
292
+
293
+ ### Other Systems
294
+
295
+ - **AudioSystem** — 2D/3D positional audio with AudioListener management
296
+ - **InputManager** — keyboard, mouse, touch, and gamepad input
297
+ - **LightingSystem** — directional, point, and spot lights with shadow management
298
+ - **NavigationSystem** — A* pathfinding on navigation meshes
299
+ - **ParticleSystem** — GPU particle effects
300
+ - **TweenSystem** — property animation and easing
301
+ - **UISystem** — HTML-based UI overlay system
302
+ - **PrefabSystem** — load and instantiate prefab hierarchies from JSON
303
+ - **SplineSystem** — Catmull-Rom spline paths for cameras, movement, etc.
304
+
305
+ ## Patterns
306
+
307
+ ### Separation of Concerns
308
+
309
+ ```typescript
310
+ // Logic/physics on parent
311
+ const character = new GameObject("Character")
312
+ character.addComponent(new CharacterController())
313
+ character.addComponent(new RigidBodyComponentThree({ type: RigidBodyType.DYNAMIC, shape: ColliderShape.CAPSULE }))
314
+
315
+ // Visual as child (can offset independently)
316
+ const visual = new GameObject("Visual")
317
+ visual.addComponent(new SkeletalRenderer("character_mesh"))
318
+ character.add(visual)
319
+ visual.position.y = -1
320
+ ```
321
+
322
+ ### Cache Component References
323
+
324
+ ```typescript
325
+ class MyComponent extends Component {
326
+ private meshRenderer?: MeshRenderer
327
+
328
+ protected onCreate(): void {
329
+ this.meshRenderer = this.getComponent(MeshRenderer) // cache in onCreate
330
+ }
331
+
332
+ public update(deltaTime: number): void {
333
+ // use cached ref — don't search every frame
334
+ }
335
+ }
336
+ ```
337
+
338
+ ### Factory Pattern
339
+
340
+ ```typescript
341
+ class EnemyFactory {
342
+ static create(type: string): GameObject {
343
+ const enemy = new GameObject(`Enemy_${type}`)
344
+ enemy.addComponent(new EnemyAI())
345
+ const meshObj = new GameObject("Mesh")
346
+ meshObj.addComponent(new MeshRenderer(`enemy_${type}`))
347
+ enemy.add(meshObj)
348
+ return enemy
349
+ }
350
+ }
351
+ ```
352
+
353
+ ## Common Mistakes
354
+
355
+ - **Don't create GameObjects in `update()`** — causes memory leaks. Create once, reuse or pool.
356
+ - **Don't access `this.gameObject` in constructor** — use `onCreate()` instead.
357
+ - **Don't forget `dispose()`** — always dispose GameObjects when done.
358
+ - **Don't add multiple components of same type** to one GameObject — use child GameObjects.
359
+ - **Don't call `this.clock.getDelta()`** — use the `deltaTime` parameter.
360
+ - **Don't bypass MeshRenderer** by loading meshes directly from StowKitSystem — use the component pattern.
361
+
362
+ ## Dependencies
363
+
364
+ - `three` (peer, >=0.180.0)
365
+ - `@dimforge/rapier3d` — Rapier physics engine
366
+ - `@series-inc/stowkit-reader` + `@series-inc/stowkit-three-loader` — asset loading
@@ -1081,6 +1081,124 @@ import { Preferences } from "@capacitor/preferences";
1081
1081
  import { LocalNotifications } from "@capacitor/local-notifications";
1082
1082
  import { App } from "@capacitor/app";
1083
1083
  import { SplashScreen } from "@capacitor/splash-screen";
1084
+
1085
+ // src/platform/datadog-analytics/index.ts
1086
+ var OTEL_ENDPOINT = "https://otel.run.game";
1087
+ var LOGS_PATH = "/v1/logs";
1088
+ function toOtlpValue(v) {
1089
+ if (typeof v === "string") return { stringValue: v };
1090
+ if (typeof v === "number") return Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
1091
+ if (typeof v === "boolean") return { boolValue: v };
1092
+ return { stringValue: String(v) };
1093
+ }
1094
+ function toOtlpAttributes(data) {
1095
+ return Object.entries(data).map(([key, value]) => ({ key, value: toOtlpValue(value) }));
1096
+ }
1097
+ function generateSessionId() {
1098
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1099
+ return crypto.randomUUID();
1100
+ }
1101
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
1102
+ }
1103
+ var config = {
1104
+ endpoint: OTEL_ENDPOINT.replace(/\/$/, ""),
1105
+ serviceName: "unknown",
1106
+ serviceVersion: "0.0.0",
1107
+ platform: "web",
1108
+ sessionId: ""
1109
+ };
1110
+ function init(options) {
1111
+ config.sessionId = generateSessionId();
1112
+ if (options) {
1113
+ if (options.serviceName !== void 0) config.serviceName = options.serviceName;
1114
+ if (options.serviceVersion !== void 0) config.serviceVersion = options.serviceVersion;
1115
+ if (options.platform !== void 0) config.platform = options.platform;
1116
+ }
1117
+ }
1118
+ function getEndpoint() {
1119
+ if (config.platform === "android" || config.platform === "ios") {
1120
+ return config.endpoint;
1121
+ }
1122
+ if (typeof window !== "undefined") {
1123
+ const host = window.location?.hostname ?? "";
1124
+ if (host === "localhost" || host === "127.0.0.1") {
1125
+ return `${window.location.origin}/api/otel`;
1126
+ }
1127
+ }
1128
+ return config.endpoint;
1129
+ }
1130
+ function send(attributes, body) {
1131
+ const payload = {
1132
+ resourceLogs: [
1133
+ {
1134
+ resource: {
1135
+ attributes: [
1136
+ { key: "service.name", value: { stringValue: config.serviceName } },
1137
+ { key: "service.version", value: { stringValue: config.serviceVersion } },
1138
+ { key: "log.type", value: { stringValue: "hermes" } }
1139
+ ]
1140
+ },
1141
+ scopeLogs: [
1142
+ {
1143
+ scope: { name: "datadog-analytics", version: "1.0.0" },
1144
+ logRecords: [
1145
+ {
1146
+ timeUnixNano: String(Date.now() * 1e6),
1147
+ severityText: "INFO",
1148
+ severityNumber: 9,
1149
+ body: { stringValue: body },
1150
+ attributes: toOtlpAttributes({
1151
+ timestamp: Date.now(),
1152
+ platform: config.platform,
1153
+ service_name: config.serviceName,
1154
+ session_id: config.sessionId,
1155
+ ...attributes
1156
+ })
1157
+ }
1158
+ ]
1159
+ }
1160
+ ]
1161
+ }
1162
+ ]
1163
+ };
1164
+ const url = `${getEndpoint()}${LOGS_PATH}`;
1165
+ return fetch(url, {
1166
+ method: "POST",
1167
+ headers: { "Content-Type": "application/json" },
1168
+ body: JSON.stringify(payload)
1169
+ }).then((res) => {
1170
+ if (!res.ok && typeof console !== "undefined" && console.warn) {
1171
+ console.warn("[DatadogAnalytics] send failed:", res.status, url);
1172
+ }
1173
+ return res.ok;
1174
+ }).catch((err) => {
1175
+ if (typeof console !== "undefined" && console.warn) {
1176
+ console.warn("[DatadogAnalytics] send error:", err);
1177
+ }
1178
+ return false;
1179
+ });
1180
+ }
1181
+ function trackCustom(params) {
1182
+ const { name, screen, desc, ...rest } = params;
1183
+ return send(
1184
+ { event_type: name, screen_name: screen ?? "", ...desc !== void 0 && { description: desc }, ...rest },
1185
+ name
1186
+ );
1187
+ }
1188
+ function trackFunnel(params) {
1189
+ const { step, screenName, stepNumber = 0, funnelName = "studio_funnel", ...context } = params;
1190
+ return send(
1191
+ { event_type: "step_funnel", screen_name: screenName, funnel_name: funnelName, step_name: step, step_number: stepNumber, ...context },
1192
+ `step_funnel:${step}`
1193
+ );
1194
+ }
1195
+ var DatadogAnalytics = {
1196
+ init,
1197
+ trackCustom,
1198
+ trackFunnel
1199
+ };
1200
+
1201
+ // src/platform/CapacitorPlatform.ts
1084
1202
  var LOG_PREFIX = "[Capacitor]";
1085
1203
  function stringToNotificationId(str) {
1086
1204
  let hash = 0;
@@ -1140,14 +1258,16 @@ var CapacitorPlatform = class {
1140
1258
  };
1141
1259
  // AppsFlyer module (lazy-loaded, only on native)
1142
1260
  appsFlyerModule = null;
1143
- // Analytics implementation - AppsFlyer on native, console fallback otherwise
1261
+ // Analytics implementation - AppsFlyer + Datadog on native (Capacitor builds), console fallback otherwise
1144
1262
  analytics = {
1145
1263
  trackFunnelStep: (step, name) => {
1146
1264
  console.log(`${LOG_PREFIX} analytics.trackFunnelStep(${step}, "${name}")`);
1265
+ this.sendDatadogFunnel(step, name);
1147
1266
  this.sendAppsFlyerEvent(`funnel_step_${step}`, { af_content: name });
1148
1267
  },
1149
1268
  recordCustomEvent: (eventName, params) => {
1150
1269
  console.log(`${LOG_PREFIX} analytics.recordCustomEvent("${eventName}")`, params);
1270
+ this.sendDatadogCustomEvent(eventName, params ?? {});
1151
1271
  this.sendAppsFlyerEvent(eventName, params ?? {});
1152
1272
  }
1153
1273
  };
@@ -1156,6 +1276,17 @@ var CapacitorPlatform = class {
1156
1276
  this.appsFlyerModule.AppsFlyer.logEvent({ eventName, eventValue }).catch(() => {
1157
1277
  });
1158
1278
  }
1279
+ datadogInitialized = false;
1280
+ sendDatadogFunnel(step, name) {
1281
+ if (!this.datadogInitialized) return;
1282
+ DatadogAnalytics.trackFunnel({ step: name, screenName: name, stepNumber: step }).catch(() => {
1283
+ });
1284
+ }
1285
+ sendDatadogCustomEvent(eventName, params) {
1286
+ if (!this.datadogInitialized) return;
1287
+ DatadogAnalytics.trackCustom({ name: eventName, ...params }).catch(() => {
1288
+ });
1289
+ }
1159
1290
  // Ads implementation
1160
1291
  // TODO: Add @capacitor-community/admob when ready for monetization
1161
1292
  ads = {
@@ -1250,13 +1381,23 @@ var CapacitorPlatform = class {
1250
1381
  // Notifications implementation using @capacitor/local-notifications
1251
1382
  notifications = {
1252
1383
  scheduleAsync: async (title, body, delaySeconds, notificationId) => {
1384
+ if (!Capacitor.isNativePlatform()) {
1385
+ return;
1386
+ }
1253
1387
  const id = notificationId ? stringToNotificationId(notificationId) : Date.now();
1254
1388
  console.log(`${LOG_PREFIX} notifications.scheduleAsync("${title}", "${body}", ${delaySeconds}s, id=${id})`);
1255
1389
  try {
1256
- const permission = await LocalNotifications.requestPermissions();
1257
- console.log(`${LOG_PREFIX} notifications: permission status =`, permission.display);
1390
+ const existing = await LocalNotifications.checkPermissions();
1391
+ let permission = existing;
1392
+ if (existing.display !== "granted") {
1393
+ if (existing.display === "denied") {
1394
+ console.log(`${LOG_PREFIX} notifications: permission denied, skipping`);
1395
+ return;
1396
+ }
1397
+ permission = await LocalNotifications.requestPermissions();
1398
+ }
1258
1399
  if (permission.display !== "granted") {
1259
- console.warn(`${LOG_PREFIX} notifications: permission not granted (${permission.display})`);
1400
+ console.log(`${LOG_PREFIX} notifications: permission not granted (${permission.display})`);
1260
1401
  return;
1261
1402
  }
1262
1403
  await this.ensureNotificationChannel();
@@ -1310,6 +1451,12 @@ var CapacitorPlatform = class {
1310
1451
  this.pauseCallbacks.push(callback);
1311
1452
  }
1312
1453
  };
1454
+ initDatadogAsync() {
1455
+ const platform = Capacitor.getPlatform();
1456
+ DatadogAnalytics.init({ serviceName: "burgertime-capacitor", serviceVersion: "0.0.0", platform });
1457
+ this.datadogInitialized = true;
1458
+ console.log(`${LOG_PREFIX} Datadog analytics: initialized (${platform})`);
1459
+ }
1313
1460
  async initAppsFlyerAsync() {
1314
1461
  console.log(`${LOG_PREFIX} AppsFlyer: initAppsFlyerAsync() called (native platform)`);
1315
1462
  const devKey = import.meta.env?.VITE_APPSFLYER_DEV_KEY;
@@ -1359,6 +1506,7 @@ var CapacitorPlatform = class {
1359
1506
  // Initialize
1360
1507
  async initializeAsync(options) {
1361
1508
  console.log(`${LOG_PREFIX} initializeAsync()`, options);
1509
+ this.initDatadogAsync();
1362
1510
  if (Capacitor.isNativePlatform()) {
1363
1511
  await this.initAppsFlyerAsync();
1364
1512
  }
@@ -5533,6 +5681,8 @@ var StowKitSystem = class _StowKitSystem {
5533
5681
  materialConverter;
5534
5682
  decoderPaths = { ...DEFAULT_DECODER_PATHS };
5535
5683
  fetchBlob;
5684
+ onTimingStart;
5685
+ onTimingEnd;
5536
5686
  // Loaded packs by alias
5537
5687
  packs = /* @__PURE__ */ new Map();
5538
5688
  // Asset caches
@@ -5572,11 +5722,13 @@ var StowKitSystem = class _StowKitSystem {
5572
5722
  * @param config Configuration including material converter and CDN fetch function
5573
5723
  * @returns The loaded PrefabCollection
5574
5724
  */
5575
- async loadFromBuildJson(buildJson, config) {
5576
- this.materialConverter = config.materialConverter;
5577
- this.fetchBlob = config.fetchBlob;
5578
- if (config.decoderPaths) {
5579
- this.decoderPaths = { ...DEFAULT_DECODER_PATHS, ...config.decoderPaths };
5725
+ async loadFromBuildJson(buildJson, config2) {
5726
+ this.materialConverter = config2.materialConverter;
5727
+ this.fetchBlob = config2.fetchBlob;
5728
+ this.onTimingStart = config2.onTimingStart;
5729
+ this.onTimingEnd = config2.onTimingEnd;
5730
+ if (config2.decoderPaths) {
5731
+ this.decoderPaths = { ...DEFAULT_DECODER_PATHS, ...config2.decoderPaths };
5580
5732
  }
5581
5733
  const prefabCollection = PrefabCollection.createFromJSON(
5582
5734
  buildJson
@@ -5589,13 +5741,22 @@ var StowKitSystem = class _StowKitSystem {
5589
5741
  await Promise.all(
5590
5742
  mounts.filter((mount) => !this.packs.has(mount.alias)).map(async (mount) => {
5591
5743
  console.log(`[StowKitSystem] Loading pack "${mount.alias}" from ${mount.path}`);
5592
- const blob = await config.fetchBlob(mount.path);
5744
+ const fetchLabel = `StowKit: fetch - ${mount.alias}`;
5745
+ this.onTimingStart?.(fetchLabel);
5746
+ const blob = await config2.fetchBlob(mount.path);
5747
+ this.onTimingEnd?.(fetchLabel);
5748
+ const arrayBufferLabel = `StowKit: arrayBuffer - ${mount.alias}`;
5749
+ this.onTimingStart?.(arrayBufferLabel);
5593
5750
  const arrayBuffer = await blob.arrayBuffer();
5751
+ this.onTimingEnd?.(arrayBufferLabel);
5752
+ const unpackLabel = `StowKit: unpack - ${mount.alias}`;
5753
+ this.onTimingStart?.(unpackLabel);
5594
5754
  const pack = await StowKitLoader2.loadFromMemory(arrayBuffer, {
5595
5755
  basisPath: this.decoderPaths.basis,
5596
5756
  dracoPath: this.decoderPaths.draco,
5597
5757
  wasmPath: this.decoderPaths.wasm
5598
5758
  });
5759
+ this.onTimingEnd?.(unpackLabel);
5599
5760
  this.packs.set(mount.alias, pack);
5600
5761
  })
5601
5762
  );
@@ -5620,13 +5781,22 @@ var StowKitSystem = class _StowKitSystem {
5620
5781
  throw new Error("StowKitSystem: fetchBlob not configured. Call loadFromBuildJson first.");
5621
5782
  }
5622
5783
  console.log(`[StowKitSystem] Loading pack "${alias}" from ${path}`);
5784
+ const fetchLabel = `StowKit: fetch - ${alias}`;
5785
+ this.onTimingStart?.(fetchLabel);
5623
5786
  const blob = await this.fetchBlob(path);
5787
+ this.onTimingEnd?.(fetchLabel);
5788
+ const arrayBufferLabel = `StowKit: arrayBuffer - ${alias}`;
5789
+ this.onTimingStart?.(arrayBufferLabel);
5624
5790
  const arrayBuffer = await blob.arrayBuffer();
5791
+ this.onTimingEnd?.(arrayBufferLabel);
5792
+ const unpackLabel = `StowKit: unpack - ${alias}`;
5793
+ this.onTimingStart?.(unpackLabel);
5625
5794
  const pack = await StowKitLoader2.loadFromMemory(arrayBuffer, {
5626
5795
  basisPath: this.decoderPaths.basis,
5627
5796
  dracoPath: this.decoderPaths.draco,
5628
5797
  wasmPath: this.decoderPaths.wasm
5629
5798
  });
5799
+ this.onTimingEnd?.(unpackLabel);
5630
5800
  this.packs.set(alias, pack);
5631
5801
  console.log(`[StowKitSystem] Pack "${alias}" loaded`);
5632
5802
  }
@@ -6242,4 +6412,4 @@ export {
6242
6412
  PrefabLoader,
6243
6413
  StowKitSystem
6244
6414
  };
6245
- //# sourceMappingURL=chunk-UDJVZHS6.js.map
6415
+ //# sourceMappingURL=chunk-WLXQBO3A.js.map