@nice2dev/game-engine 0.1.0 → 1.0.2

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 (112) hide show
  1. package/CHANGELOG.md +193 -1
  2. package/dist/cjs/audio/AudioBridge.js +454 -0
  3. package/dist/cjs/audio/AudioBridge.js.map +1 -0
  4. package/dist/cjs/devtools/GameplayAnalytics.js +651 -0
  5. package/dist/cjs/devtools/GameplayAnalytics.js.map +1 -0
  6. package/dist/cjs/dialogue/DialogueSystem.js +1023 -0
  7. package/dist/cjs/dialogue/DialogueSystem.js.map +1 -0
  8. package/dist/cjs/editor/NiceGameEditor.js +569 -71
  9. package/dist/cjs/editor/NiceGameEditor.js.map +1 -1
  10. package/dist/cjs/engine/SaveSystemV2.js +494 -0
  11. package/dist/cjs/engine/SaveSystemV2.js.map +1 -0
  12. package/dist/cjs/i18n/useTranslation.js +11 -11
  13. package/dist/cjs/index.js +90 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/input/GamepadNavigation.js +21 -21
  16. package/dist/cjs/input/useGamepads.js +6 -6
  17. package/dist/cjs/integration/IconSprite.js +281 -0
  18. package/dist/cjs/integration/IconSprite.js.map +1 -0
  19. package/dist/cjs/inventory/InventorySystem.js +930 -0
  20. package/dist/cjs/inventory/InventorySystem.js.map +1 -0
  21. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AbortController.js.map +1 -1
  22. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js.map +1 -1
  23. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js.map +1 -1
  24. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js.map +1 -1
  25. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Errors.js.map +1 -1
  26. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js.map +1 -1
  27. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js.map +1 -1
  28. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js.map +1 -1
  29. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpClient.js.map +1 -1
  30. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js.map +1 -1
  31. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnection.js.map +1 -1
  32. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js.map +1 -1
  33. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js.map +1 -1
  34. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ILogger.js.map +1 -1
  35. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ITransport.js.map +1 -1
  36. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js.map +1 -1
  37. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Loggers.js.map +1 -1
  38. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js.map +1 -1
  39. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js.map +1 -1
  40. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js.map +1 -1
  41. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Subject.js.map +1 -1
  42. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js.map +1 -1
  43. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Utils.js.map +1 -1
  44. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js.map +1 -1
  45. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js.map +1 -1
  46. package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/pkg-version.js.map +1 -1
  47. package/dist/cjs/quest/QuestSystem.js +924 -0
  48. package/dist/cjs/quest/QuestSystem.js.map +1 -0
  49. package/dist/cjs/rendering/WebGPURenderPipeline.js +658 -0
  50. package/dist/cjs/rendering/WebGPURenderPipeline.js.map +1 -0
  51. package/dist/cjs/xr/ARVR.js.map +1 -1
  52. package/dist/esm/audio/AudioBridge.js +446 -0
  53. package/dist/esm/audio/AudioBridge.js.map +1 -0
  54. package/dist/esm/devtools/GameplayAnalytics.js +639 -0
  55. package/dist/esm/devtools/GameplayAnalytics.js.map +1 -0
  56. package/dist/esm/dialogue/DialogueSystem.js +1008 -0
  57. package/dist/esm/dialogue/DialogueSystem.js.map +1 -0
  58. package/dist/esm/editor/NiceGameEditor.js +556 -58
  59. package/dist/esm/editor/NiceGameEditor.js.map +1 -1
  60. package/dist/esm/engine/SaveSystemV2.js +487 -0
  61. package/dist/esm/engine/SaveSystemV2.js.map +1 -0
  62. package/dist/esm/index.js +11 -3
  63. package/dist/esm/index.js.map +1 -1
  64. package/dist/esm/integration/IconSprite.js +266 -0
  65. package/dist/esm/integration/IconSprite.js.map +1 -0
  66. package/dist/esm/inventory/InventorySystem.js +924 -0
  67. package/dist/esm/inventory/InventorySystem.js.map +1 -0
  68. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AbortController.js.map +1 -1
  69. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js.map +1 -1
  70. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js.map +1 -1
  71. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js.map +1 -1
  72. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Errors.js.map +1 -1
  73. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js.map +1 -1
  74. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js.map +1 -1
  75. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js.map +1 -1
  76. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpClient.js.map +1 -1
  77. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js.map +1 -1
  78. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnection.js.map +1 -1
  79. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js.map +1 -1
  80. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js.map +1 -1
  81. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ILogger.js.map +1 -1
  82. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ITransport.js.map +1 -1
  83. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js.map +1 -1
  84. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Loggers.js.map +1 -1
  85. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js.map +1 -1
  86. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js.map +1 -1
  87. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js.map +1 -1
  88. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Subject.js.map +1 -1
  89. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js.map +1 -1
  90. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Utils.js.map +1 -1
  91. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js.map +1 -1
  92. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js.map +1 -1
  93. package/dist/esm/node_modules/@microsoft/signalr/dist/esm/pkg-version.js.map +1 -1
  94. package/dist/esm/quest/QuestSystem.js +916 -0
  95. package/dist/esm/quest/QuestSystem.js.map +1 -0
  96. package/dist/esm/rendering/WebGPURenderPipeline.js +642 -0
  97. package/dist/esm/rendering/WebGPURenderPipeline.js.map +1 -0
  98. package/dist/esm/xr/ARVR.js.map +1 -1
  99. package/dist/types/__tests__/setup.d.ts +1 -1
  100. package/dist/types/audio/AudioBridge.d.ts +199 -0
  101. package/dist/types/devtools/GameplayAnalytics.d.ts +279 -0
  102. package/dist/types/dialogue/DialogueSystem.d.ts +326 -0
  103. package/dist/types/dialogue/index.d.ts +2 -0
  104. package/dist/types/editor/NiceGameEditor.d.ts +12 -1
  105. package/dist/types/engine/SaveSystemV2.d.ts +155 -0
  106. package/dist/types/index.d.ts +19 -3
  107. package/dist/types/integration/IconSprite.d.ts +196 -0
  108. package/dist/types/inventory/InventorySystem.d.ts +336 -0
  109. package/dist/types/performance/WebGPUCompute.d.ts +0 -10
  110. package/dist/types/quest/QuestSystem.d.ts +287 -0
  111. package/dist/types/rendering/WebGPURenderPipeline.d.ts +255 -0
  112. package/package.json +7 -1
@@ -0,0 +1,651 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+
5
+ /* ────────────────────────────────────────────────────────────────
6
+ Gameplay Analytics — Telemetry, heatmaps & session recording
7
+
8
+ Comprehensive gameplay analytics with:
9
+ - Event-based telemetry pipeline with batching
10
+ - Spatial heatmaps (death, traversal, interaction, combat)
11
+ - Session recording and playback
12
+ - Funnel analysis and retention tracking
13
+ - Performance metrics collection
14
+ ──────────────────────────────────────────────────────────────── */
15
+ const DEFAULT_TELEMETRY_CONFIG = {
16
+ enabled: true,
17
+ batchSize: 50,
18
+ flushIntervalMs: 30000,
19
+ maxBufferSize: 10000,
20
+ categories: [],
21
+ sampleRate: 1,
22
+ };
23
+ const DEFAULT_HEATMAP_CONFIG = {
24
+ type: 'death',
25
+ cellSize: 1,
26
+ bounds: { minX: -50, minY: -50, maxX: 50, maxY: 50 },
27
+ colorStops: [
28
+ { value: 0, color: 'rgba(0, 0, 255, 0)' },
29
+ { value: 0.25, color: 'rgba(0, 0, 255, 0.5)' },
30
+ { value: 0.5, color: 'rgba(0, 255, 0, 0.7)' },
31
+ { value: 0.75, color: 'rgba(255, 255, 0, 0.8)' },
32
+ { value: 1, color: 'rgba(255, 0, 0, 1)' },
33
+ ],
34
+ maxValue: 0,
35
+ blurRadius: 1,
36
+ };
37
+ const DEFAULT_RECORDING_CONFIG = {
38
+ enabled: false,
39
+ fps: 10,
40
+ maxDurationMs: 600000, // 10 minutes
41
+ recordTypes: ['position', 'action', 'state-change'],
42
+ recordPosition: true,
43
+ compress: true,
44
+ };
45
+ /* ══════════════════════════════════════════════════════════════
46
+ TELEMETRY PIPELINE
47
+ ══════════════════════════════════════════════════════════════ */
48
+ class TelemetryPipeline {
49
+ constructor(eventBus, config = {}) {
50
+ this.buffer = [];
51
+ this.flushed = [];
52
+ this.flushTimer = null;
53
+ this.eventCounter = 0;
54
+ this.config = { ...DEFAULT_TELEMETRY_CONFIG, ...config };
55
+ this.eventBus = eventBus;
56
+ this.sessionId = this.generateId();
57
+ if (this.config.enabled)
58
+ this.startFlushTimer();
59
+ }
60
+ generateId() {
61
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
62
+ }
63
+ /** Track a telemetry event */
64
+ track(category, name, data = {}, position) {
65
+ if (!this.config.enabled)
66
+ return;
67
+ if (this.config.categories.length > 0 && !this.config.categories.includes(category))
68
+ return;
69
+ if (this.config.sampleRate < 1 && Math.random() > this.config.sampleRate)
70
+ return;
71
+ const event = {
72
+ id: `evt-${++this.eventCounter}`,
73
+ category,
74
+ name,
75
+ timestamp: Date.now(),
76
+ sessionId: this.sessionId,
77
+ data,
78
+ position,
79
+ };
80
+ this.buffer.push(event);
81
+ if (this.buffer.length >= this.config.batchSize) {
82
+ this.flush();
83
+ }
84
+ // Trim buffer if too large
85
+ if (this.buffer.length > this.config.maxBufferSize) {
86
+ this.buffer = this.buffer.slice(-this.config.maxBufferSize);
87
+ }
88
+ }
89
+ /** Flush buffered events */
90
+ flush() {
91
+ if (this.buffer.length === 0)
92
+ return [];
93
+ const batch = [...this.buffer];
94
+ this.buffer = [];
95
+ this.flushed.push(batch);
96
+ this.eventBus.emit('analytics:flush', { count: batch.length });
97
+ return batch;
98
+ }
99
+ startFlushTimer() {
100
+ this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
101
+ }
102
+ /** Get all events (current buffer + flushed) */
103
+ getAllEvents() {
104
+ return [...this.flushed.flat(), ...this.buffer];
105
+ }
106
+ /** Get events by category */
107
+ getByCategory(category) {
108
+ return this.getAllEvents().filter(e => e.category === category);
109
+ }
110
+ /** Get event count by name */
111
+ countByName(name) {
112
+ return this.getAllEvents().filter(e => e.name === name).length;
113
+ }
114
+ getSessionId() {
115
+ return this.sessionId;
116
+ }
117
+ getBufferSize() {
118
+ return this.buffer.length;
119
+ }
120
+ getTotalEvents() {
121
+ return this.flushed.reduce((s, b) => s + b.length, 0) + this.buffer.length;
122
+ }
123
+ clear() {
124
+ this.buffer = [];
125
+ this.flushed = [];
126
+ this.eventCounter = 0;
127
+ }
128
+ destroy() {
129
+ if (this.flushTimer)
130
+ clearInterval(this.flushTimer);
131
+ this.flush();
132
+ }
133
+ }
134
+ /* ══════════════════════════════════════════════════════════════
135
+ HEATMAP GENERATOR
136
+ ══════════════════════════════════════════════════════════════ */
137
+ class HeatmapGenerator {
138
+ constructor(config = {}) {
139
+ this.grid = new Map();
140
+ this.totalEvents = 0;
141
+ this.config = { ...DEFAULT_HEATMAP_CONFIG, ...config };
142
+ }
143
+ cellKey(cx, cy) {
144
+ return `${cx},${cy}`;
145
+ }
146
+ worldToCell(wx, wy) {
147
+ return [
148
+ Math.floor((wx - this.config.bounds.minX) / this.config.cellSize),
149
+ Math.floor((wy - this.config.bounds.minY) / this.config.cellSize),
150
+ ];
151
+ }
152
+ /** Add an event at a world position */
153
+ addEvent(x, y, weight = 1) {
154
+ var _a;
155
+ if (x < this.config.bounds.minX || x > this.config.bounds.maxX ||
156
+ y < this.config.bounds.minY || y > this.config.bounds.maxY)
157
+ return;
158
+ const [cx, cy] = this.worldToCell(x, y);
159
+ const key = this.cellKey(cx, cy);
160
+ this.grid.set(key, ((_a = this.grid.get(key)) !== null && _a !== void 0 ? _a : 0) + weight);
161
+ this.totalEvents++;
162
+ }
163
+ /** Add events from telemetry data */
164
+ addFromTelemetry(events) {
165
+ for (const event of events) {
166
+ if (event.position) {
167
+ this.addEvent(event.position.x, event.position.y);
168
+ }
169
+ }
170
+ }
171
+ /** Get the grid as an array of cells with normalized values */
172
+ getCells() {
173
+ const cells = [];
174
+ let maxVal = this.config.maxValue;
175
+ if (maxVal <= 0) {
176
+ // Auto-detect max value
177
+ maxVal = 0;
178
+ for (const v of this.grid.values()) {
179
+ if (v > maxVal)
180
+ maxVal = v;
181
+ }
182
+ }
183
+ if (maxVal === 0)
184
+ maxVal = 1;
185
+ for (const [key, value] of this.grid) {
186
+ const [cx, cy] = key.split(',').map(Number);
187
+ cells.push({
188
+ x: cx,
189
+ y: cy,
190
+ value: Math.min(value / maxVal, 1),
191
+ events: value,
192
+ });
193
+ }
194
+ return cells;
195
+ }
196
+ /** Apply Gaussian blur to the grid */
197
+ getBlurredCells() {
198
+ var _a;
199
+ const cells = this.getCells();
200
+ if (this.config.blurRadius <= 0)
201
+ return cells;
202
+ const radius = this.config.blurRadius;
203
+ const cellMap = new Map();
204
+ // Build raw map
205
+ for (const cell of cells) {
206
+ cellMap.set(this.cellKey(cell.x, cell.y), cell.events);
207
+ }
208
+ // Apply blur
209
+ const blurred = [];
210
+ const sigma = radius / 2;
211
+ const kernel = [];
212
+ for (let dy = -radius; dy <= radius; dy++) {
213
+ const row = [];
214
+ for (let dx = -radius; dx <= radius; dx++) {
215
+ row.push(Math.exp(-(dx * dx + dy * dy) / (2 * sigma * sigma)));
216
+ }
217
+ kernel.push(row);
218
+ }
219
+ // Normalize kernel
220
+ let kernelSum = 0;
221
+ for (const row of kernel)
222
+ for (const v of row)
223
+ kernelSum += v;
224
+ const visited = new Set();
225
+ for (const cell of cells) {
226
+ for (let dy = -radius; dy <= radius; dy++) {
227
+ for (let dx = -radius; dx <= radius; dx++) {
228
+ const nx = cell.x + dx;
229
+ const ny = cell.y + dy;
230
+ const key = this.cellKey(nx, ny);
231
+ if (visited.has(key))
232
+ continue;
233
+ visited.add(key);
234
+ let sum = 0;
235
+ for (let ky = -radius; ky <= radius; ky++) {
236
+ for (let kx = -radius; kx <= radius; kx++) {
237
+ const srcKey = this.cellKey(nx + kx, ny + ky);
238
+ const srcVal = (_a = cellMap.get(srcKey)) !== null && _a !== void 0 ? _a : 0;
239
+ sum += srcVal * kernel[ky + radius][kx + radius];
240
+ }
241
+ }
242
+ const normalized = sum / kernelSum;
243
+ if (normalized > 0.001) {
244
+ blurred.push({ x: nx, y: ny, value: 0, events: normalized });
245
+ }
246
+ }
247
+ }
248
+ }
249
+ // Normalize values
250
+ let maxVal = this.config.maxValue;
251
+ if (maxVal <= 0) {
252
+ maxVal = Math.max(...blurred.map(c => c.events), 1);
253
+ }
254
+ for (const cell of blurred) {
255
+ cell.value = Math.min(cell.events / maxVal, 1);
256
+ }
257
+ return blurred;
258
+ }
259
+ /** Render heatmap to a canvas */
260
+ renderToCanvas(ctx, width, height) {
261
+ const cells = this.getBlurredCells();
262
+ const { bounds, cellSize, colorStops } = this.config;
263
+ const worldW = bounds.maxX - bounds.minX;
264
+ const worldH = bounds.maxY - bounds.minY;
265
+ const scaleX = width / worldW;
266
+ const scaleY = height / worldH;
267
+ const cellW = cellSize * scaleX;
268
+ const cellH = cellSize * scaleY;
269
+ ctx.clearRect(0, 0, width, height);
270
+ for (const cell of cells) {
271
+ const screenX = cell.x * cellSize * scaleX;
272
+ const screenY = cell.y * cellSize * scaleY;
273
+ // Interpolate color from stops
274
+ const color = this.interpolateColor(cell.value, colorStops);
275
+ ctx.fillStyle = color;
276
+ ctx.fillRect(screenX, screenY, cellW + 1, cellH + 1);
277
+ }
278
+ }
279
+ interpolateColor(value, stops) {
280
+ if (stops.length === 0)
281
+ return 'transparent';
282
+ if (stops.length === 1)
283
+ return stops[0].color;
284
+ for (let i = 0; i < stops.length - 1; i++) {
285
+ if (value >= stops[i].value && value <= stops[i + 1].value) {
286
+ const t = (value - stops[i].value) / (stops[i + 1].value - stops[i].value);
287
+ return this.lerpColor(stops[i].color, stops[i + 1].color, t);
288
+ }
289
+ }
290
+ return stops[stops.length - 1].color;
291
+ }
292
+ lerpColor(a, b, t) {
293
+ const parse = (c) => {
294
+ const m = c.match(/[\d.]+/g);
295
+ return m ? m.map(Number) : [0, 0, 0, 1];
296
+ };
297
+ const ca = parse(a);
298
+ const cb = parse(b);
299
+ const r = Math.round(ca[0] + (cb[0] - ca[0]) * t);
300
+ const g = Math.round(ca[1] + (cb[1] - ca[1]) * t);
301
+ const bl = Math.round(ca[2] + (cb[2] - ca[2]) * t);
302
+ const al = ca[3] + (cb[3] - ca[3]) * t;
303
+ return `rgba(${r}, ${g}, ${bl}, ${al.toFixed(2)})`;
304
+ }
305
+ getTotalEvents() {
306
+ return this.totalEvents;
307
+ }
308
+ getConfig() {
309
+ return this.config;
310
+ }
311
+ clear() {
312
+ this.grid.clear();
313
+ this.totalEvents = 0;
314
+ }
315
+ }
316
+ /* ══════════════════════════════════════════════════════════════
317
+ SESSION RECORDER
318
+ ══════════════════════════════════════════════════════════════ */
319
+ class SessionRecorder {
320
+ constructor(config = {}) {
321
+ this.recording = false;
322
+ this.frames = [];
323
+ this.startedAt = 0;
324
+ this.lastFrameTime = 0;
325
+ this.pendingEvents = [];
326
+ this.recordTimer = null;
327
+ this.config = { ...DEFAULT_RECORDING_CONFIG, ...config };
328
+ this.sessionId = `sess-${Date.now().toString(36)}`;
329
+ }
330
+ /** Start recording */
331
+ start() {
332
+ if (this.recording)
333
+ return;
334
+ this.recording = true;
335
+ this.startedAt = Date.now();
336
+ this.lastFrameTime = this.startedAt;
337
+ this.frames = [];
338
+ const intervalMs = 1000 / this.config.fps;
339
+ this.recordTimer = setInterval(() => this.captureFrame(), intervalMs);
340
+ }
341
+ /** Stop recording and return the session */
342
+ stop(sceneId = 'unknown', buildVersion = '0.0.0') {
343
+ this.recording = false;
344
+ if (this.recordTimer) {
345
+ clearInterval(this.recordTimer);
346
+ this.recordTimer = null;
347
+ }
348
+ // Capture any remaining events
349
+ if (this.pendingEvents.length > 0)
350
+ this.captureFrame();
351
+ const endedAt = Date.now();
352
+ const recording = {
353
+ id: `rec-${Date.now().toString(36)}`,
354
+ sessionId: this.sessionId,
355
+ startedAt: this.startedAt,
356
+ endedAt,
357
+ durationMs: endedAt - this.startedAt,
358
+ frameCount: this.frames.length,
359
+ frames: this.frames,
360
+ metadata: { sceneId, buildVersion, tags: [] },
361
+ sizeBytes: 0,
362
+ };
363
+ // Estimate size
364
+ recording.sizeBytes = JSON.stringify(recording.frames).length;
365
+ return recording;
366
+ }
367
+ /** Add an event to the current frame batch */
368
+ recordEvent(type, data) {
369
+ if (!this.recording)
370
+ return;
371
+ if (this.config.recordTypes.length > 0 && !this.config.recordTypes.includes(type))
372
+ return;
373
+ this.pendingEvents.push({ type, data });
374
+ }
375
+ /** Record player position (convenience) */
376
+ recordPosition(x, y, z) {
377
+ if (!this.config.recordPosition)
378
+ return;
379
+ this.recordEvent('position', { x, y, z });
380
+ }
381
+ captureFrame() {
382
+ const now = Date.now();
383
+ // Check max duration
384
+ if (this.config.maxDurationMs > 0 && now - this.startedAt > this.config.maxDurationMs) {
385
+ this.stop();
386
+ return;
387
+ }
388
+ const frame = {
389
+ timestamp: now,
390
+ delta: now - this.lastFrameTime,
391
+ events: [...this.pendingEvents],
392
+ };
393
+ this.frames.push(frame);
394
+ this.pendingEvents = [];
395
+ this.lastFrameTime = now;
396
+ }
397
+ isRecording() {
398
+ return this.recording;
399
+ }
400
+ getFrameCount() {
401
+ return this.frames.length;
402
+ }
403
+ getDurationMs() {
404
+ return this.recording ? Date.now() - this.startedAt : 0;
405
+ }
406
+ }
407
+ /* ══════════════════════════════════════════════════════════════
408
+ SESSION PLAYER (Playback)
409
+ ══════════════════════════════════════════════════════════════ */
410
+ class SessionPlayer {
411
+ constructor() {
412
+ this.recording = null;
413
+ this.currentFrame = 0;
414
+ this.playing = false;
415
+ this.speed = 1;
416
+ this.playTimer = null;
417
+ this.onFrame = null;
418
+ }
419
+ load(recording) {
420
+ this.recording = recording;
421
+ this.currentFrame = 0;
422
+ this.stop();
423
+ }
424
+ /** Start playback */
425
+ play(onFrame) {
426
+ if (!this.recording || this.playing)
427
+ return;
428
+ this.playing = true;
429
+ this.onFrame = onFrame;
430
+ this.advanceFrame();
431
+ }
432
+ advanceFrame() {
433
+ if (!this.recording || !this.playing || this.currentFrame >= this.recording.frames.length) {
434
+ this.stop();
435
+ return;
436
+ }
437
+ const frame = this.recording.frames[this.currentFrame];
438
+ if (this.onFrame)
439
+ this.onFrame(frame, this.currentFrame);
440
+ this.currentFrame++;
441
+ if (this.currentFrame < this.recording.frames.length) {
442
+ const nextFrame = this.recording.frames[this.currentFrame];
443
+ const delay = (nextFrame.delta || 100) / this.speed;
444
+ this.playTimer = setTimeout(() => this.advanceFrame(), delay);
445
+ }
446
+ else {
447
+ this.stop();
448
+ }
449
+ }
450
+ stop() {
451
+ this.playing = false;
452
+ if (this.playTimer) {
453
+ clearTimeout(this.playTimer);
454
+ this.playTimer = null;
455
+ }
456
+ }
457
+ pause() {
458
+ this.playing = false;
459
+ if (this.playTimer) {
460
+ clearTimeout(this.playTimer);
461
+ this.playTimer = null;
462
+ }
463
+ }
464
+ seek(frameIndex) {
465
+ if (!this.recording)
466
+ return;
467
+ this.currentFrame = Math.max(0, Math.min(frameIndex, this.recording.frameCount - 1));
468
+ }
469
+ setSpeed(speed) {
470
+ this.speed = Math.max(0.1, Math.min(10, speed));
471
+ }
472
+ getProgress() {
473
+ if (!this.recording || this.recording.frameCount === 0)
474
+ return 0;
475
+ return this.currentFrame / this.recording.frameCount;
476
+ }
477
+ isPlaying() {
478
+ return this.playing;
479
+ }
480
+ }
481
+ /* ══════════════════════════════════════════════════════════════
482
+ FUNNEL TRACKER
483
+ ══════════════════════════════════════════════════════════════ */
484
+ class FunnelTracker {
485
+ constructor() {
486
+ this.funnels = new Map();
487
+ this.progress = new Map(); // funnelId → stepId → sessionIds
488
+ }
489
+ define(funnel) {
490
+ this.funnels.set(funnel.id, funnel);
491
+ const stepMap = new Map();
492
+ for (const step of funnel.steps) {
493
+ stepMap.set(step.id, new Set());
494
+ }
495
+ this.progress.set(funnel.id, stepMap);
496
+ }
497
+ /** Record that a session reached a funnel step */
498
+ recordStep(funnelId, stepEventName, sessionId) {
499
+ var _a;
500
+ const funnel = this.funnels.get(funnelId);
501
+ if (!funnel)
502
+ return;
503
+ const step = funnel.steps.find(s => s.eventName === stepEventName);
504
+ if (!step)
505
+ return;
506
+ const stepMap = this.progress.get(funnelId);
507
+ (_a = stepMap === null || stepMap === void 0 ? void 0 : stepMap.get(step.id)) === null || _a === void 0 ? void 0 : _a.add(sessionId);
508
+ }
509
+ /** Get funnel analysis */
510
+ analyze(funnelId) {
511
+ var _a, _b;
512
+ const funnel = this.funnels.get(funnelId);
513
+ const stepMap = this.progress.get(funnelId);
514
+ if (!funnel || !stepMap)
515
+ return [];
516
+ const result = [];
517
+ let prevCount = 0;
518
+ for (const step of funnel.steps) {
519
+ const count = (_b = (_a = stepMap.get(step.id)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0;
520
+ const dropOff = prevCount > 0 ? 1 - count / prevCount : 0;
521
+ result.push({
522
+ id: step.id,
523
+ name: step.name,
524
+ count,
525
+ dropOffRate: result.length === 0 ? 0 : dropOff,
526
+ });
527
+ prevCount = count;
528
+ }
529
+ return result;
530
+ }
531
+ getFunnels() {
532
+ return Array.from(this.funnels.values());
533
+ }
534
+ }
535
+ /* ══════════════════════════════════════════════════════════════
536
+ GAMEPLAY ANALYTICS (Main class)
537
+ ══════════════════════════════════════════════════════════════ */
538
+ class GameplayAnalytics {
539
+ constructor(eventBus, telemetryConfig, recordingConfig) {
540
+ this.heatmaps = new Map();
541
+ this.eventBus = eventBus;
542
+ this.telemetry = new TelemetryPipeline(eventBus, telemetryConfig);
543
+ this.recorder = new SessionRecorder(recordingConfig);
544
+ this.funnels = new FunnelTracker();
545
+ }
546
+ /** Create or get a heatmap */
547
+ heatmap(id, config) {
548
+ let hm = this.heatmaps.get(id);
549
+ if (!hm) {
550
+ hm = new HeatmapGenerator(config);
551
+ this.heatmaps.set(id, hm);
552
+ }
553
+ return hm;
554
+ }
555
+ /** Track and automatically distribute to relevant heatmaps */
556
+ trackEvent(category, name, data = {}, position) {
557
+ this.telemetry.track(category, name, data, position);
558
+ // Auto-feed heatmaps
559
+ if (position) {
560
+ for (const [_, hm] of this.heatmaps) {
561
+ hm.addEvent(position.x, position.y);
562
+ }
563
+ }
564
+ // Auto-feed session recorder
565
+ this.recorder.recordEvent('action', { category, name, ...data });
566
+ if (position) {
567
+ this.recorder.recordPosition(position.x, position.y, position.z);
568
+ }
569
+ // Auto-feed funnel tracker
570
+ for (const funnel of this.funnels.getFunnels()) {
571
+ this.funnels.recordStep(funnel.id, name, this.telemetry.getSessionId());
572
+ }
573
+ }
574
+ /** Get summary stats */
575
+ getSummary() {
576
+ return {
577
+ totalEvents: this.telemetry.getTotalEvents(),
578
+ sessionId: this.telemetry.getSessionId(),
579
+ heatmapCount: this.heatmaps.size,
580
+ isRecording: this.recorder.isRecording(),
581
+ recordedFrames: this.recorder.getFrameCount(),
582
+ };
583
+ }
584
+ destroy() {
585
+ this.telemetry.destroy();
586
+ if (this.recorder.isRecording())
587
+ this.recorder.stop();
588
+ this.heatmaps.clear();
589
+ }
590
+ }
591
+ /* ══════════════════════════════════════════════════════════════
592
+ REACT HOOK
593
+ ══════════════════════════════════════════════════════════════ */
594
+ function useGameplayAnalytics(eventBus, telemetryConfig, recordingConfig) {
595
+ const analyticsRef = React.useRef(null);
596
+ const analytics = React.useMemo(() => {
597
+ if (!analyticsRef.current) {
598
+ analyticsRef.current = new GameplayAnalytics(eventBus, telemetryConfig, recordingConfig);
599
+ }
600
+ return analyticsRef.current;
601
+ }, [eventBus]);
602
+ React.useEffect(() => {
603
+ return () => {
604
+ analytics.destroy();
605
+ analyticsRef.current = null;
606
+ };
607
+ }, [analytics]);
608
+ const trackEvent = React.useCallback((category, name, data, position) => analytics.trackEvent(category, name, data, position), [analytics]);
609
+ const [summary, setSummary] = React.useState(analytics.getSummary());
610
+ const refreshSummary = React.useCallback(() => {
611
+ setSummary(analytics.getSummary());
612
+ }, [analytics]);
613
+ return {
614
+ analytics,
615
+ trackEvent,
616
+ summary,
617
+ refreshSummary,
618
+ startRecording: () => analytics.recorder.start(),
619
+ stopRecording: (sceneId) => analytics.recorder.stop(sceneId),
620
+ heatmap: (id, config) => analytics.heatmap(id, config),
621
+ };
622
+ }
623
+ const AnalyticsDashboard = ({ analytics, className, style, }) => {
624
+ const [summary, setSummary] = React.useState(analytics.getSummary());
625
+ React.useEffect(() => {
626
+ const timer = setInterval(() => setSummary(analytics.getSummary()), 1000);
627
+ return () => clearInterval(timer);
628
+ }, [analytics]);
629
+ const statStyle = {
630
+ display: 'flex', justifyContent: 'space-between', padding: '4px 0',
631
+ borderBottom: '1px solid rgba(128,128,128,0.2)',
632
+ };
633
+ return React.createElement('div', {
634
+ className: `nice-analytics-dashboard ${className !== null && className !== void 0 ? className : ''}`.trim(),
635
+ style: { padding: 16, fontFamily: 'monospace', fontSize: 13, ...style },
636
+ }, React.createElement('h3', { style: { margin: '0 0 12px 0' } }, 'Gameplay Analytics'), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Session ID'), React.createElement('code', null, summary.sessionId)), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Total Events'), React.createElement('span', null, String(summary.totalEvents))), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Heatmaps'), React.createElement('span', null, String(summary.heatmapCount))), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Recording'), React.createElement('span', null, summary.isRecording ? `ON (${summary.recordedFrames} frames)` : 'OFF')));
637
+ };
638
+ AnalyticsDashboard.displayName = 'AnalyticsDashboard';
639
+
640
+ exports.AnalyticsDashboard = AnalyticsDashboard;
641
+ exports.DEFAULT_HEATMAP_CONFIG = DEFAULT_HEATMAP_CONFIG;
642
+ exports.DEFAULT_RECORDING_CONFIG = DEFAULT_RECORDING_CONFIG;
643
+ exports.DEFAULT_TELEMETRY_CONFIG = DEFAULT_TELEMETRY_CONFIG;
644
+ exports.FunnelTracker = FunnelTracker;
645
+ exports.GameplayAnalytics = GameplayAnalytics;
646
+ exports.HeatmapGenerator = HeatmapGenerator;
647
+ exports.SessionPlayer = SessionPlayer;
648
+ exports.SessionRecorder = SessionRecorder;
649
+ exports.TelemetryPipeline = TelemetryPipeline;
650
+ exports.useGameplayAnalytics = useGameplayAnalytics;
651
+ //# sourceMappingURL=GameplayAnalytics.js.map