littlejsengine 1.18.29 → 1.20.0

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 (51) hide show
  1. package/FAQ.md +841 -815
  2. package/README.md +211 -199
  3. package/dist/littlejs.d.ts +4383 -812
  4. package/dist/littlejs.esm.js +31848 -16680
  5. package/dist/littlejs.esm.min.js +14 -1
  6. package/dist/littlejs.js +31230 -16166
  7. package/dist/littlejs.min.js +13 -1
  8. package/dist/littlejs.release.js +27419 -15457
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +375 -0
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -330
  16. package/plugins/math3d.js +911 -0
  17. package/plugins/medalSystem.js +118 -72
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +196 -115
  21. package/plugins/postProcess.js +284 -166
  22. package/plugins/render3d.js +4072 -0
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +86 -48
  27. package/plugins/threejs.js +183 -158
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +646 -526
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +8 -15
  32. package/src/engine.js +345 -194
  33. package/src/engineAudio.js +1022 -777
  34. package/src/engineBuild.mjs +289 -253
  35. package/src/engineDebug.js +912 -735
  36. package/src/engineDraw.js +1774 -1510
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +419 -396
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineLogo.js +4 -3
  41. package/src/engineMath.js +1274 -1183
  42. package/src/engineObject.js +674 -551
  43. package/src/engineParticles.js +583 -532
  44. package/src/engineRelease.js +67 -45
  45. package/src/engineSettings.js +808 -726
  46. package/src/engineTileLayer.js +995 -741
  47. package/src/engineUtilities.js +32 -13
  48. package/src/engineWebGL.js +1162 -941
  49. package/src/jsconfig.json +17 -13
  50. package/plugins/box2d.wasm.js +0 -630
  51. package/plugins/box2d.wasm.wasm +0 -0
@@ -1,736 +1,913 @@
1
- /**
2
- * LittleJS Debug System
3
- * - Press Esc to toggle debug overlay with object picking
4
- * - Number keys toggle debug visualizations (physics, particles, etc.)
5
- * - +/- keys control time scale for slow motion/fast forward
6
- * - ASSERT and LOG macros for development (removed in release builds)
7
- * - Debug primitive rendering (rectangles, circles, lines, points, text)
8
- * - Screenshot and video capture support
9
- * - FPS counter and performance watermark
10
- * - Debug overlay shows mouse position and picked objects
11
- * @namespace Debug
12
- */
13
-
14
- 'use strict';
15
-
16
- /** True if debug is enabled
17
- * @type {boolean}
18
- * @default
19
- * @memberof Debug */
20
- const debug = true;
21
-
22
- /** Size to render debug points by default
23
- * @type {number}
24
- * @default
25
- * @memberof Debug */
26
- const debugPointSize = .5;
27
-
28
- /** True if watermark with FPS should be shown, false in release builds
29
- * @type {boolean}
30
- * @default
31
- * @memberof Debug */
32
- let debugWatermark = true;
33
-
34
- /** Key code used to toggle debug mode, Esc by default
35
- * @type {string}
36
- * @default
37
- * @memberof Debug */
38
- let debugKey = 'Escape';
39
-
40
- /** True if the debug overlay is active, always false in release builds
41
- * @type {boolean}
42
- * @default
43
- * @memberof Debug */
44
- let debugOverlay = false;
45
-
46
- // Engine internal variables not exposed to documentation
47
- let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugSound = false, debugTakeScreenshot;
48
-
49
- ///////////////////////////////////////////////////////////////////////////////
50
- // Debug helper functions
51
-
52
- /** Asserts if the expression is false, does nothing in release builds
53
- * Halts execution if the assert fails and throws an error
54
- * @param {boolean} assert
55
- * @param {...Object} output - error message output
56
- * @memberof Debug */
57
- function ASSERT(assert, ...output)
58
- {
59
- if (assert) return;
60
- console.assert(assert, ...output)
61
- throw new Error('Assert failed!'); // halt execution
62
- }
63
-
64
- /** Log to console if debug is enabled, does nothing in release builds
65
- * @param {...Object} output - message output
66
- * @memberof Debug */
67
- function LOG(...output) { console.log(...output); }
68
-
69
- /** Draw a debug rectangle in world space
70
- * @param {Vector2} pos
71
- * @param {Vector2} [size=vec2(0)]
72
- * @param {Color|string} [color]
73
- * @param {number} [time]
74
- * @param {number} [angle]
75
- * @param {boolean} [fill]
76
- * @param {boolean} [screenSpace]
77
- * @memberof Debug */
78
- function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
79
- {
80
- ASSERT(isVector2(pos), 'pos must be a vec2');
81
- ASSERT(isVector2(size), 'size must be a vec2');
82
- ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
83
- ASSERT(isNumber(time), 'time must be a number');
84
- ASSERT(isNumber(angle), 'angle must be a number');
85
-
86
- if (isColor(color))
87
- color = color.toString();
88
- const timer = new Timer(time);
89
- debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
90
- }
91
-
92
- /** Draw a debug poly in world space
93
- * @param {Vector2} pos
94
- * @param {Array<Vector2>} points
95
- * @param {Color|string} [color]
96
- * @param {number} [time]
97
- * @param {number} [angle]
98
- * @param {boolean} [fill]
99
- * @param {boolean} [screenSpace]
100
- * @memberof Debug */
101
- function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
102
- {
103
- ASSERT(isVector2(pos), 'pos must be a vec2');
104
- ASSERT(isArray(points), 'points must be an array');
105
- ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
106
- ASSERT(isNumber(time), 'time must be a number');
107
- ASSERT(isNumber(angle), 'angle must be a number');
108
-
109
- if (isColor(color))
110
- color = color.toString();
111
- pos = pos.copy();
112
- points = points.map(p=>p.copy());
113
- const timer = new Timer(time);
114
- debugPrimitives.push({pos, points, color, timer, angle, fill, screenSpace});
115
- }
116
-
117
- /** Draw a debug circle in world space
118
- * @param {Vector2} pos
119
- * @param {number} [size] - diameter
120
- * @param {Color|string} [color]
121
- * @param {number} [time]
122
- * @param {boolean} [fill]
123
- * @param {boolean} [screenSpace]
124
- * @memberof Debug */
125
- function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=false)
126
- {
127
- ASSERT(isVector2(pos), 'pos must be a vec2');
128
- ASSERT(isNumber(size), 'size must be a number');
129
- ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
130
- ASSERT(isNumber(time), 'time must be a number');
131
-
132
- if (isColor(color))
133
- color = color.toString();
134
- pos = pos.copy();
135
- const timer = new Timer(time);
136
- debugPrimitives.push({pos, size, color, timer, angle:0, fill, screenSpace});
137
- }
138
-
139
- /** Draw a debug point in world space
140
- * @param {Vector2} pos
141
- * @param {Color|string} [color]
142
- * @param {number} [time]
143
- * @param {number} [angle]
144
- * @param {boolean} [screenSpace]
145
- * @memberof Debug */
146
- function debugPoint(pos, color, time, angle, screenSpace=false)
147
- { debugRect(pos, undefined, color, time, angle, false, screenSpace); }
148
-
149
- /** Draw a debug line in world space
150
- * @param {Vector2} posA
151
- * @param {Vector2} posB
152
- * @param {Color|string} [color]
153
- * @param {number} [width]
154
- * @param {number} [time]
155
- * @param {boolean} [screenSpace]
156
- * @memberof Debug */
157
- function debugLine(posA, posB, color, width=.1, time=0, screenSpace=false)
158
- {
159
- ASSERT(isVector2(posA), 'posA must be a vec2');
160
- ASSERT(isVector2(posB), 'posB must be a vec2');
161
- ASSERT(isNumber(width), 'width must be a number');
162
-
163
- const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
164
- const size = vec2(width, halfDelta.length()*2);
165
- debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true, screenSpace);
166
- }
167
-
168
- /** Draw a debug combined axis aligned bounding box in world space
169
- * @param {Vector2} posA
170
- * @param {Vector2} sizeA
171
- * @param {Vector2} posB
172
- * @param {Vector2} sizeB
173
- * @param {Color|string} [color]
174
- * @param {number} [time]
175
- * @param {boolean} [screenSpace]
176
- * @memberof Debug */
177
- function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
178
- {
179
- ASSERT(isVector2(posA), 'posA must be a vec2');
180
- ASSERT(isVector2(posB), 'posB must be a vec2');
181
- ASSERT(isVector2(sizeA), 'sizeA must be a vec2');
182
- ASSERT(isVector2(sizeB), 'sizeB must be a vec2');
183
-
184
- const minPos = vec2(
185
- min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
186
- min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
187
- );
188
- const maxPos = vec2(
189
- max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
190
- max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
191
- );
192
- debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
193
- }
194
-
195
- /** Draw debug text in world space
196
- * @param {string|number} text
197
- * @param {Vector2} pos
198
- * @param {number} [size]
199
- * @param {Color|string} [color]
200
- * @param {number} [time]
201
- * @param {number} [angle]
202
- * @param {string} [font]
203
- * @param {boolean} [screenSpace]
204
- * @memberof Debug */
205
- function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
206
- {
207
- ASSERT(isStringLike(text), 'text must be a string');
208
- ASSERT(isVector2(pos), 'pos must be a vec2');
209
- ASSERT(isNumber(size), 'size must be a number');
210
- ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
211
- ASSERT(isNumber(time), 'time must be a number');
212
- ASSERT(isNumber(angle), 'angle must be a number');
213
- ASSERT(isStringLike(font), 'font must be a string');
214
-
215
- if (isColor(color))
216
- color = color.toString();
217
- pos = pos.copy();
218
- const timer = new Timer(time);
219
- debugPrimitives.push({text, pos, size, color, timer, angle, font, screenSpace});
220
- }
221
-
222
- /** Clear all debug primitives in the list
223
- * @memberof Debug */
224
- function debugClear() { debugPrimitives = []; }
225
-
226
- /** Trigger debug system to take a screenshot
227
- * @memberof Debug */
228
- function debugScreenshot() { debugTakeScreenshot = 1; }
229
-
230
- /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
231
- * This is a good function to call at the start of your game to catch all errors
232
- * In release builds this function has no effect
233
- * @memberof Debug */
234
- function debugShowErrors()
235
- {
236
- const showError = (message)=>
237
- {
238
- // replace entire page with error message
239
- document.body.style = 'background-color:#111;margin:8px';
240
- document.body.innerHTML = `<pre style=color:#f00;font-size:28px;white-space:pre-wrap>` + message;
241
- }
242
-
243
- const originalAssert = console.assert;
244
- console.assert = (assertion, ...output)=>
245
- {
246
- originalAssert(assertion, ...output);
247
- if (!assertion)
248
- {
249
- const message = output.join(' ');
250
- const stack = new Error().stack;
251
- throw 'Assertion failed!\n' + message + '\n' + stack;
252
- }
253
- };
254
- onunhandledrejection = (event)=>
255
- showError(event.reason.stack || event.reason);
256
- onerror = (message, source, lineno, colno)=>
257
- showError(`${message}\n${source}\nLn ${lineno}, Col ${colno}`);
258
- }
259
-
260
- ///////////////////////////////////////////////////////////////////////////////
261
- // Engine debug functions (called automatically)
262
-
263
- function debugInit()
264
- {
265
- if (showEngineVersion)
266
- console.warn("LittleJS DEBUG build loaded. Use the release build for production.");
267
- }
268
-
269
- function debugUpdate()
270
- {
271
- if (!debug) return;
272
-
273
- if (keyWasPressed(debugKey)) // Esc
274
- debugOverlay = !debugOverlay;
275
- if (debugOverlay)
276
- {
277
- if (keyWasPressed('Digit1'))
278
- debugPhysics = !debugPhysics, debugParticles = false;
279
- if (keyWasPressed('Digit2'))
280
- debugParticles = !debugParticles, debugPhysics = false;
281
- if (keyWasPressed('Digit3'))
282
- debugGamepads = !debugGamepads;
283
- if (keyWasPressed('Digit4'))
284
- debugRaycast = !debugRaycast;
285
- if (keyWasPressed('Digit5'))
286
- debugScreenshot();
287
- if (keyWasPressed('Digit7'))
288
- debugSound = !debugSound;
289
- }
290
- if (debugVideoCaptureIsActive())
291
- {
292
- // control to stop video capture
293
- if (!debugOverlay || keyWasPressed('Digit6'))
294
- debugVideoCaptureStop();
295
- }
296
- else if (debugOverlay && keyWasPressed('Digit6'))
297
- debugVideoCaptureStart();
298
- }
299
-
300
- function debugRender()
301
- {
302
- if (debugVideoCaptureIsActive())
303
- return; // don't show debug info when capturing video
304
-
305
- // flush any gl sprites before drawing debug info
306
- glFlush();
307
-
308
- const savedDrawCount = drawCount;
309
- const savedPrimitiveCount = primitiveCount;
310
-
311
- if (debugTakeScreenshot)
312
- {
313
- // combine canvases, remove alpha and save
314
- combineCanvases();
315
- saveCanvas(mainCanvas);
316
- debugTakeScreenshot = 0;
317
- }
318
-
319
- const debugContext = mainContext;
320
- if (debugGamepads && gamepadsEnable)
321
- {
322
- // draw gamepads
323
- const maxGamepads = 8;
324
- let gamepadConnectedCount = 0;
325
- for (let i = 0; i < maxGamepads; i++)
326
- gamepadConnected(i) && gamepadConnectedCount++;
327
-
328
- for (let i = 0; i < maxGamepads; i++)
329
- {
330
- if (!gamepadConnected(i))
331
- continue;
332
-
333
- const stickScale = 1;
334
- const buttonScale = .2;
335
- const cornerPos = cameraPos.add(vec2(-stickScale*2, ((gamepadConnectedCount-1)/2-i)*stickScale*3));
336
- debugText(i, cornerPos.add(vec2(-stickScale, stickScale)), 1);
337
- if (i === gamepadPrimary)
338
- debugText('Main', cornerPos.add(vec2(-stickScale*2, 0)),1, '#0f0');
339
-
340
- // read analog sticks
341
- const stickCount = gamepadStickData[i].length;
342
- for (let j = 0; j < stickCount; j++)
343
- {
344
- if (!(j in gamepadStickData[i]))
345
- continue; // skip sticks that are not present (eg a disabled touch left stick)
346
- const stick = gamepadStick(j, i);
347
- const drawPos = cornerPos.add(vec2(j*stickScale*2, 0));
348
- const stickPos = drawPos.add(stick.scale(stickScale));
349
- debugCircle(drawPos, stickScale*2, '#fff7',0,true);
350
- debugLine(drawPos, stickPos, '#f00');
351
- debugText(j, drawPos, .3);
352
- debugPoint(stickPos, '#f00');
353
- }
354
-
355
- const buttonCount = inputData[i+1].length;
356
- for (let j = 0; j < buttonCount; j++)
357
- {
358
- const drawPos = cornerPos.add(vec2(j*buttonScale*2, -stickScale-buttonScale*2));
359
- const pressed = gamepadIsDown(j, i);
360
- debugCircle(drawPos, buttonScale*2, pressed ? '#f00' : '#fff7', 0, true);
361
- debugText(j, drawPos, .3);
362
- }
363
- }
364
- }
365
-
366
- let debugObject;
367
- if (debugOverlay)
368
- {
369
- // draw red rectangle around screen
370
- const cameraSize = getCameraSize();
371
- debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
372
-
373
- // mouse pick
374
- let bestDistance = Infinity;
375
- for (const o of engineObjects)
376
- {
377
- if (o.destroyed)
378
- continue;
379
-
380
- if (o instanceof TileLayer)
381
- continue; // prevent tile layers from being picked
382
-
383
- o.renderDebugInfo();
384
- if (!o.size.x || !o.size.y)
385
- continue;
386
-
387
- const distance = mousePos.distanceSquared(o.pos);
388
- if (distance < bestDistance)
389
- {
390
- bestDistance = distance;
391
- debugObject = o;
392
- }
393
- }
394
-
395
- if (tileCollisionTest(mousePos))
396
- {
397
- // show floored tile pick for tile collision
398
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(1,1,0,.5), 0, false);
399
- }
400
- }
401
-
402
- {
403
- // draw debug primitives
404
- debugContext.lineWidth = 2;
405
- debugPrimitives.forEach(p=>
406
- {
407
- debugContext.save();
408
-
409
- // create canvas transform from world space to screen space
410
- // without scaling because we want consistent pixel sizes
411
- let pos = p.pos, scale = 1, angle = p.angle;
412
- if (!p.screenSpace)
413
- {
414
- pos = worldToScreen(p.pos);
415
- scale = cameraScale;
416
- angle -= cameraAngle;
417
- }
418
- debugContext.translate(pos.x|0, pos.y|0);
419
- debugContext.rotate(angle);
420
- debugContext.scale(1, p.text ? 1 : -1);
421
- debugContext.fillStyle = p.color;
422
- debugContext.strokeStyle = p.color;
423
- if (p.text !== undefined)
424
- {
425
- debugContext.font = p.size*scale + 'px '+ p.font;
426
- debugContext.textAlign = 'center';
427
- debugContext.textBaseline = 'middle';
428
- debugContext.fillText(p.text, 0, 0);
429
- }
430
- else if (p.points !== undefined)
431
- {
432
- // poly
433
- debugContext.beginPath();
434
- for (const point of p.points)
435
- {
436
- const p2 = point.scale(scale).floor();
437
- debugContext.lineTo(p2.x, p2.y);
438
- }
439
- debugContext.closePath();
440
- p.fill && debugContext.fill();
441
- debugContext.stroke();
442
- }
443
- else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
444
- {
445
- // point
446
- const pointSize = debugPointSize * scale;
447
- debugContext.fillRect(-pointSize/2, -1, pointSize, 3);
448
- debugContext.fillRect(-1, -pointSize/2, 3, pointSize);
449
- }
450
- else if (p.size.x !== undefined)
451
- {
452
- // rect
453
- const s = p.size.scale(scale).floor();
454
- const w = s.x, h = s.y;
455
- p.fill && debugContext.fillRect(-w/2|0, -h/2|0, w, h);
456
- debugContext.strokeRect(-w/2|0, -h/2|0, w, h);
457
- }
458
- else
459
- {
460
- // circle
461
- debugContext.beginPath();
462
- debugContext.arc(0, 0, p.size*scale/2, 0, 9);
463
- p.fill && debugContext.fill();
464
- debugContext.stroke();
465
- }
466
-
467
- debugContext.restore();
468
- });
469
-
470
- // remove expired primitives
471
- debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
472
- }
473
-
474
- if (debugObject)
475
- {
476
- const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
477
- raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3), 0, false);
478
- drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5), undefined, undefined, false);
479
-
480
- let debugText = 'mouse pos = ' + mousePos;
481
- if (tileCollisionLayers.length)
482
- debugText += '\nmouse collision = ' + tileCollisionGetData(mousePos);
483
- debugText += '\n\n--- object info ---\n';
484
- debugText += debugObject.toString();
485
- drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
486
- }
487
-
488
- {
489
- // draw debug overlay
490
- const fontSize = 20;
491
- const lineHeight = fontSize * 1.2 | 0;
492
- debugContext.save();
493
- debugContext.fillStyle = '#fff';
494
- debugContext.textAlign = 'left';
495
- debugContext.textBaseline = 'top';
496
- debugContext.font = fontSize + 'px monospace';
497
- debugContext.shadowColor = '#000';
498
- debugContext.shadowBlur = 9;
499
-
500
- let x = 9, y = 0, h = lineHeight;
501
- if (debugOverlay)
502
- {
503
- debugContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
504
- debugContext.fillText('Time: ' + formatTime(time), x, y += h);
505
- debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
506
- x, y += h);
507
- debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
508
- debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
509
- debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
510
- debugContext.fillText('---------', x, y += h);
511
- debugContext.fillStyle = '#f00';
512
- debugContext.fillText('ESC: Debug Overlay', x, y += h);
513
- debugContext.fillStyle = debugPhysics ? '#f00' : '#fff';
514
- debugContext.fillText('1: Debug Physics', x, y += h);
515
- debugContext.fillStyle = debugParticles ? '#f00' : '#fff';
516
- debugContext.fillText('2: Debug Particles', x, y += h);
517
- debugContext.fillStyle = debugGamepads ? '#f00' : '#fff';
518
- debugContext.fillText('3: Debug Gamepads', x, y += h);
519
- debugContext.fillStyle = debugRaycast ? '#f00' : '#fff';
520
- debugContext.fillText('4: Debug Raycasts', x, y += h);
521
- debugContext.fillStyle = '#fff';
522
- debugContext.fillText('5: Save Screenshot', x, y += h);
523
- debugContext.fillText('6: Toggle Video Capture', x, y += h);
524
- debugContext.fillStyle = debugSound ? '#f00' : '#fff';
525
- debugContext.fillText('7: Debug Sound', x, y += h);
526
-
527
- let keysPressed = '';
528
- let mousePressed = '';
529
- for (const i in inputData[0])
530
- {
531
- if (!keyIsDown(i, 0))
532
- continue;
533
- if (parseInt(i) < 3)
534
- mousePressed += i + ' ' ;
535
- else
536
- keysPressed += i + ' ' ;
537
- }
538
- mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
539
- keysPressed && debugContext.fillText('Keys: ' + keysPressed, x, y += h);
540
-
541
- // show gamepad buttons
542
- for (let i = 1; i < inputData.length; i++)
543
- {
544
- let buttonsPressed = '';
545
- if (inputData[i])
546
- for (const j in inputData[i])
547
- {
548
- if (keyIsDown(j, i))
549
- buttonsPressed += j + ' ' ;
550
- }
551
- buttonsPressed && debugContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
552
- }
553
- }
554
- else
555
- {
556
- debugContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
557
- debugContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
558
- debugContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
559
- debugContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
560
- debugContext.fillText(debugSound ? 'Debug Sound' : '', x, y += h);
561
- }
562
-
563
- debugContext.restore();
564
- }
565
-
566
- if (debugWatermark || debugOverlay)
567
- {
568
- // show fps stats display
569
- mainContext.textAlign = 'right';
570
- mainContext.textBaseline = 'top';
571
- mainContext.font = '1em monospace';
572
- mainContext.fillStyle = '#000';
573
- const text = engineName + ' v' + engineVersion + ' / '
574
- + savedDrawCount + ' / ' + savedPrimitiveCount + ' / '
575
- + engineObjects.length + ' / ' + averageFPS.toFixed(1)
576
- + (glEnable ? ' GL' : ' 2D') ;
577
- mainContext.fillText(text, mainCanvas.width-3, 3);
578
- mainContext.fillStyle = '#fff';
579
- mainContext.fillText(text, mainCanvas.width-2, 2);
580
- }
581
- }
582
-
583
- function debugRenderPost()
584
- {
585
- if (debugVideoCaptureIsActive())
586
- {
587
- debugVideoCaptureUpdate();
588
- return;
589
- }
590
- }
591
-
592
- ///////////////////////////////////////////////////////////////////////////////
593
- // video capture - records video and audio at 60 fps using MediaRecorder API
594
-
595
- // internal variables used to capture video
596
- let debugVideoCapture, debugVideoCaptureIcon;
597
-
598
- /** Check if video capture is active
599
- * @memberof Debug */
600
- function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
601
-
602
- /** Start capturing video
603
- * @memberof Debug */
604
- function debugVideoCaptureStart()
605
- {
606
- ASSERT(!debugVideoCaptureIsActive(), 'Already capturing video!');
607
-
608
- if (!debugVideoCaptureIcon)
609
- {
610
- // create recording icon to show it is capturing video
611
- debugVideoCaptureIcon = document.createElement('div');
612
- debugVideoCaptureIcon.style.position = 'absolute';
613
- debugVideoCaptureIcon.style.padding = '9px';
614
- debugVideoCaptureIcon.style.color = '#f00';
615
- debugVideoCaptureIcon.style.font = '50px monospace';
616
- document.body.appendChild(debugVideoCaptureIcon);
617
- }
618
- // show recording icon
619
- debugVideoCaptureIcon.textContent = '';
620
- debugVideoCaptureIcon.style.display = '';
621
-
622
- // setup captureStream to capture manually by passing 0
623
- const stream = mainCanvas.captureStream(0);
624
- const videoTrack = stream.getVideoTracks()[0];
625
- const captureTimer = new Timer(0, true);
626
- const chunks = [];
627
- videoTrack.applyConstraints({frameRate:frameRate});
628
-
629
- // set up the media recorder
630
- const mediaRecorder = new MediaRecorder(stream,
631
- {mimeType:'video/webm;codecs=vp8'});
632
- mediaRecorder.ondataavailable = (e)=> chunks.push(e.data);
633
- mediaRecorder.onstop = ()=>
634
- {
635
- const blob = new Blob(chunks, {type: 'video/webm'});
636
- const url = URL.createObjectURL(blob);
637
- saveDataURL(url, 'capture.webm', 1e3);
638
- };
639
-
640
- let audioStreamDestination, silentAudioSource;
641
- if (soundEnable)
642
- {
643
- // create silent audio source
644
- // fixes issue where video can not start recording without audio
645
- silentAudioSource = new ConstantSourceNode(audioContext, { offset: 0 });
646
- silentAudioSource.connect(audioMasterGain);
647
- silentAudioSource.start();
648
-
649
- // connect to audio master gain node
650
- audioStreamDestination = audioContext.createMediaStreamDestination();
651
- audioMasterGain.connect(audioStreamDestination);
652
- for (const track of audioStreamDestination.stream.getAudioTracks())
653
- stream.addTrack(track); // add audio tracks to capture stream
654
- }
655
-
656
- // start recording
657
- try { mediaRecorder.start(); }
658
- catch(e)
659
- {
660
- LOG('Video capture not supported in this browser!');
661
- silentAudioSource?.stop();
662
- audioStreamDestination && audioMasterGain.disconnect(audioStreamDestination);
663
- return;
664
- }
665
-
666
- LOG('Video capture started.');
667
-
668
- // save debug video info
669
- debugVideoCapture =
670
- {
671
- mediaRecorder,
672
- captureTimer,
673
- videoTrack,
674
- silentAudioSource,
675
- audioStreamDestination
676
- };
677
- }
678
-
679
- /** Stop capturing video and save to disk
680
- * @memberof Debug */
681
- function debugVideoCaptureStop()
682
- {
683
- ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
684
-
685
- // stop recording
686
- LOG(`Video capture ended. ${debugVideoCapture.captureTimer.get().toFixed(2)} seconds recorded.`);
687
- debugVideoCaptureIcon.style.display = 'none';
688
- debugVideoCapture.silentAudioSource?.stop();
689
- debugVideoCapture.mediaRecorder?.stop();
690
- debugVideoCapture.videoTrack?.stop();
691
- if (debugVideoCapture.audioStreamDestination)
692
- audioMasterGain.disconnect(debugVideoCapture.audioStreamDestination);
693
- debugVideoCapture = undefined;
694
- }
695
-
696
- // update video capture, called automatically by engine
697
- function debugVideoCaptureUpdate()
698
- {
699
- ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
700
-
701
- // save the video frame
702
- combineCanvases();
703
- debugVideoCapture.videoTrack.requestFrame();
704
- debugVideoCaptureIcon.textContent = '● REC '
705
- + formatTime(debugVideoCapture.captureTimer);
706
- }
707
-
708
- ///////////////////////////////////////////////////////////////////////////////
709
- // debug utility functions
710
-
711
- // make color constants immutable with debug assertions
712
- function debugProtectConstant(obj)
713
- {
714
- if (debug)
715
- {
716
- // get properties and store original values
717
- const props = Object.keys(obj), values = {};
718
- props.forEach(prop => values[prop] = obj[prop]);
719
-
720
- // replace with getters/setters that assert
721
- props.forEach(prop =>
722
- {
723
- Object.defineProperty(obj, prop, {
724
- get: ()=> values[prop],
725
- set: (value)=>
726
- {
727
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
728
- },
729
- enumerable: true
730
- });
731
- });
732
- }
733
-
734
- // freeze the object to prevent adding new properties
735
- return Object.freeze(obj);
1
+ /**
2
+ * LittleJS Debug System
3
+ * - Press Esc to toggle debug overlay with object picking
4
+ * - Number keys toggle debug visualizations (physics, particles, etc.) while the overlay is open
5
+ * - +/- keys control time scale for slow motion/fast forward while the overlay is open
6
+ * - setDebugKeysAlways lets those keys work with the overlay closed too
7
+ * - ASSERT and LOG macros for development (removed in release builds)
8
+ * - Debug primitive rendering (rectangles, circles, lines, points, text)
9
+ * - Screenshot and video capture support
10
+ * - FPS counter and performance watermark
11
+ * - Debug overlay shows mouse position and picked objects
12
+ * @namespace Debug
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ /** True if debug is enabled
18
+ * @type {boolean}
19
+ * @default
20
+ * @memberof Debug */
21
+ const debug = true;
22
+
23
+ /** Size to render debug points by default
24
+ * @type {number}
25
+ * @default
26
+ * @memberof Debug */
27
+ const debugPointSize = .5;
28
+
29
+ /** True if watermark with FPS should be shown, false in release builds
30
+ * @type {boolean}
31
+ * @default
32
+ * @memberof Debug */
33
+ let debugWatermark = true;
34
+
35
+ /** Key code used to toggle debug mode, Esc by default
36
+ * @type {string}
37
+ * @default
38
+ * @memberof Debug */
39
+ let debugKey = 'Escape';
40
+
41
+ /** Let the debug keys work while the overlay is closed, the number keys and the +/- time keys, for a game that does
42
+ * not use them; off by default, so they only work while the overlay is open
43
+ * @type {boolean}
44
+ * @default
45
+ * @memberof Debug */
46
+ let debugKeysAlways = false;
47
+
48
+ /** True if the debug overlay is active, always false in release builds
49
+ * @type {boolean}
50
+ * @default
51
+ * @memberof Debug */
52
+ let debugOverlay = false;
53
+
54
+ /** Open or close the debug overlay from code, as the debug key does; does nothing in release builds
55
+ * @param {boolean} [show]
56
+ * @memberof Debug */
57
+ function setDebugOverlay(show=true) { debug && (debugOverlay = !!show); }
58
+
59
+ /** True if the tweakables panel is shown, 9 toggles it while the overlay is open; set it to show the panel
60
+ * from the start, the panel is never shown in release builds
61
+ * @type {boolean}
62
+ * @default
63
+ * @memberof Debug */
64
+ let debugTweakables = false;
65
+
66
+ // the shadow behind debug text, so it reads over anything: the overlay menu and the mouse text both use it;
67
+ // a smaller blur is tighter and darker, a larger one softer and fainter
68
+ const debugTextShadowColor = '#000', debugTextShadowBlur = 9;
69
+ function debugTextShadow(context) { context.shadowColor = debugTextShadowColor; context.shadowBlur = debugTextShadowBlur; }
70
+
71
+ // Engine internal variables not exposed to documentation
72
+ let debugPrimitives = [], debugClearCount = 0, debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugSound = false, debugTiles = 0, debugTakeScreenshot;
73
+ // debugTiles is 0 for off, 1 for every layer, and 2 on for one layer at a time, see debugTileLayersSelected
74
+
75
+ ///////////////////////////////////////////////////////////////////////////////
76
+ // Debug helper functions
77
+
78
+ /** Asserts if the expression is false, does nothing in release builds
79
+ * Halts execution if the assert fails and throws an error
80
+ * @param {*} assert - any value, the assert fails when it is falsy
81
+ * @param {...Object} output - error message output
82
+ * @memberof Debug */
83
+ function ASSERT(assert, ...output)
84
+ {
85
+ if (assert) return;
86
+ console.assert(assert, ...output)
87
+ throw new Error('Assert failed!'); // halt execution
88
+ }
89
+
90
+ /** Log to console if debug is enabled, does nothing in release builds
91
+ * @param {...Object} output - message output
92
+ * @memberof Debug */
93
+ function LOG(...output) { console.log(...output); }
94
+
95
+ /** Draw a debug rectangle in world space, or on the screen with screenSpace
96
+ * @param {Vector2} pos
97
+ * @param {Vector2} [size=vec2(0)]
98
+ * @param {Color|string} [color]
99
+ * @param {number} [time]
100
+ * @param {number} [angle]
101
+ * @param {boolean} [fill]
102
+ * @param {boolean} [screenSpace=drawScreenSpace]
103
+ * @memberof Debug */
104
+ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, screenSpace=drawScreenSpace)
105
+ {
106
+ ASSERT(isVector2(pos), 'pos must be a vec2');
107
+ ASSERT(isVector2(size), 'size must be a vec2');
108
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
109
+ ASSERT(isNumber(time), 'time must be a number');
110
+ ASSERT(isNumber(angle), 'angle must be a number');
111
+
112
+ if (headlessMode) return; // nothing draws them, they would pile up
113
+ if (isColor(color))
114
+ color = color.toString();
115
+ const timer = new Timer(time, true); // real time, so they still expire while the game is paused
116
+ debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
117
+ }
118
+
119
+ /** Draw a debug poly in world space, or on the screen with screenSpace
120
+ * @param {Vector2} pos
121
+ * @param {Array<Vector2>} points
122
+ * @param {Color|string} [color]
123
+ * @param {number} [time]
124
+ * @param {number} [angle]
125
+ * @param {boolean} [fill]
126
+ * @param {boolean} [screenSpace=drawScreenSpace]
127
+ * @memberof Debug */
128
+ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screenSpace=drawScreenSpace)
129
+ {
130
+ ASSERT(isVector2(pos), 'pos must be a vec2');
131
+ ASSERT(isArray(points), 'points must be an array');
132
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
133
+ ASSERT(isNumber(time), 'time must be a number');
134
+ ASSERT(isNumber(angle), 'angle must be a number');
135
+
136
+ if (headlessMode) return;
137
+ if (isColor(color))
138
+ color = color.toString();
139
+ pos = pos.copy();
140
+ points = points.map(p=>p.copy());
141
+ const timer = new Timer(time, true); // real time, so they still expire while the game is paused
142
+ debugPrimitives.push({pos, points, color, timer, angle, fill, screenSpace});
143
+ }
144
+
145
+ /** Draw a debug circle in world space, or on the screen with screenSpace
146
+ * @param {Vector2} pos
147
+ * @param {number} [size] - diameter
148
+ * @param {Color|string} [color]
149
+ * @param {number} [time]
150
+ * @param {boolean} [fill]
151
+ * @param {boolean} [screenSpace=drawScreenSpace]
152
+ * @memberof Debug */
153
+ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=drawScreenSpace)
154
+ {
155
+ ASSERT(isVector2(pos), 'pos must be a vec2');
156
+ ASSERT(isNumber(size), 'size must be a number');
157
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
158
+ ASSERT(isNumber(time), 'time must be a number');
159
+
160
+ if (headlessMode) return;
161
+ if (isColor(color))
162
+ color = color.toString();
163
+ pos = pos.copy();
164
+ const timer = new Timer(time, true); // real time, so they still expire while the game is paused
165
+ debugPrimitives.push({pos, size, color, timer, angle:0, fill, screenSpace});
166
+ }
167
+
168
+ /** Draw a debug point in world space, or on the screen with screenSpace
169
+ * @param {Vector2} pos
170
+ * @param {Color|string} [color]
171
+ * @param {number} [time]
172
+ * @param {number} [angle]
173
+ * @param {boolean} [screenSpace=drawScreenSpace]
174
+ * @memberof Debug */
175
+ function debugPoint(pos, color, time, angle, screenSpace=drawScreenSpace)
176
+ { debugRect(pos, undefined, color, time, angle, false, screenSpace); }
177
+
178
+ /** Draw a debug line in world space, or on the screen with screenSpace
179
+ * @param {Vector2} posA
180
+ * @param {Vector2} posB
181
+ * @param {Color|string} [color]
182
+ * @param {number} [width]
183
+ * @param {number} [time]
184
+ * @param {boolean} [screenSpace=drawScreenSpace]
185
+ * @memberof Debug */
186
+ function debugLine(posA, posB, color, width=.1, time=0, screenSpace=drawScreenSpace)
187
+ {
188
+ ASSERT(isVector2(posA), 'posA must be a vec2');
189
+ ASSERT(isVector2(posB), 'posB must be a vec2');
190
+ ASSERT(isNumber(width), 'width must be a number');
191
+
192
+ const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
193
+ const size = vec2(width, halfDelta.length()*2);
194
+ // screen space y points down, so its angle is measured the other way, as drawLine does
195
+ const angle = vec2(halfDelta.x, screenSpace ? -halfDelta.y : halfDelta.y).angle();
196
+ debugRect(posA.add(halfDelta), size, color, time, angle, true, screenSpace);
197
+ }
198
+
199
+ /** Draw a debug combined axis aligned bounding box in world space, or on the screen with screenSpace
200
+ * @param {Vector2} posA
201
+ * @param {Vector2} sizeA
202
+ * @param {Vector2} posB
203
+ * @param {Vector2} sizeB
204
+ * @param {Color|string} [color]
205
+ * @param {number} [time]
206
+ * @param {boolean} [screenSpace=drawScreenSpace]
207
+ * @memberof Debug */
208
+ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=drawScreenSpace)
209
+ {
210
+ ASSERT(isVector2(posA), 'posA must be a vec2');
211
+ ASSERT(isVector2(posB), 'posB must be a vec2');
212
+ ASSERT(isVector2(sizeA), 'sizeA must be a vec2');
213
+ ASSERT(isVector2(sizeB), 'sizeB must be a vec2');
214
+
215
+ const minPos = vec2(
216
+ min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
217
+ min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
218
+ );
219
+ const maxPos = vec2(
220
+ max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
221
+ max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
222
+ );
223
+ debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
224
+ }
225
+
226
+ /** Draw debug text in world space, or on the screen with screenSpace
227
+ * @param {string|number} text
228
+ * @param {Vector2} pos
229
+ * @param {number} [size]
230
+ * @param {Color|string} [color]
231
+ * @param {number} [time]
232
+ * @param {number} [angle]
233
+ * @param {string} [font]
234
+ * @param {boolean} [screenSpace=drawScreenSpace]
235
+ * @memberof Debug */
236
+ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=drawScreenSpace)
237
+ {
238
+ ASSERT(isStringLike(text), 'text must be a string');
239
+ ASSERT(isVector2(pos), 'pos must be a vec2');
240
+ ASSERT(isNumber(size), 'size must be a number');
241
+ ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
242
+ ASSERT(isNumber(time), 'time must be a number');
243
+ ASSERT(isNumber(angle), 'angle must be a number');
244
+ ASSERT(isStringLike(font), 'font must be a string');
245
+
246
+ if (headlessMode) return;
247
+ if (isColor(color))
248
+ color = color.toString();
249
+ pos = pos.copy();
250
+ const timer = new Timer(time, true); // real time, so they still expire while the game is paused
251
+ debugPrimitives.push({text, pos, size, color, timer, angle, font, screenSpace});
252
+ }
253
+
254
+ /** Clear all debug primitives in the list
255
+ * @memberof Debug */
256
+ function debugClear() { debugPrimitives = []; ++debugClearCount; } // the count lets plugins clear their own
257
+
258
+ /** Trigger debug system to take a screenshot
259
+ * @memberof Debug */
260
+ function debugScreenshot() { debugTakeScreenshot = 1; }
261
+
262
+ /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
263
+ * This is a good function to call at the start of your game to catch all errors
264
+ * In release builds this function has no effect
265
+ * @memberof Debug */
266
+ function debugShowErrors()
267
+ {
268
+ const showError = (message)=>
269
+ {
270
+ // replace entire page with error message
271
+ document.body.style.cssText = 'background-color:#111;margin:8px';
272
+ document.body.innerHTML = `<pre style=color:#f00;font-size:28px;white-space:pre-wrap>` + message;
273
+ }
274
+
275
+ const originalAssert = console.assert;
276
+ console.assert = (assertion, ...output)=>
277
+ {
278
+ originalAssert(assertion, ...output);
279
+ if (!assertion)
280
+ {
281
+ const message = output.join(' ');
282
+ const stack = new Error().stack;
283
+ throw 'Assertion failed!\n' + message + '\n' + stack;
284
+ }
285
+ };
286
+ onunhandledrejection = (event)=>
287
+ showError(event.reason?.stack || event.reason);
288
+ onerror = (message, source, lineno, colno)=>
289
+ showError(`${message}\n${source}\nLn ${lineno}, Col ${colno}`);
290
+ }
291
+
292
+ ///////////////////////////////////////////////////////////////////////////////
293
+ // Engine debug functions (called automatically)
294
+
295
+ function debugInit()
296
+ {
297
+ if (showEngineVersion)
298
+ console.warn("LittleJS DEBUG build loaded. Use the release build for production.");
299
+ }
300
+
301
+ function debugUpdate()
302
+ {
303
+ if (!debug) return;
304
+
305
+ if (keyWasPressed(debugKey)) // Esc
306
+ debugOverlay = !debugOverlay;
307
+ const debugKeys = debugOverlay || debugKeysAlways; // the keys work while the overlay is open, or always if set
308
+ if (debugKeys)
309
+ {
310
+ if (keyWasPressed('Digit1'))
311
+ debugPhysics = !debugPhysics, debugParticles = false;
312
+ if (keyWasPressed('Digit2'))
313
+ debugTiles = (debugTiles + 1) % (debugTileLayersShown().length + 2); // off, all, then each layer
314
+ if (keyWasPressed('Digit3'))
315
+ debugParticles = !debugParticles, debugPhysics = false;
316
+ if (keyWasPressed('Digit4'))
317
+ debugRaycast = !debugRaycast;
318
+ if (keyWasPressed('Digit5'))
319
+ debugGamepads = !debugGamepads;
320
+ if (keyWasPressed('Digit6'))
321
+ debugSound = !debugSound;
322
+ if (keyWasPressed('Digit7'))
323
+ debugScreenshot();
324
+ if (keyWasPressed('Digit9'))
325
+ debugTweakables = !debugTweakables;
326
+ if (keyWasPressed('Digit0'))
327
+ levelEditor.isOpen ? levelEditor.close() : levelEditor.open();
328
+ }
329
+ if (debugVideoCaptureIsActive())
330
+ {
331
+ // control to stop video capture, a capture the overlay started also stops when the overlay closes,
332
+ // one the game started from code runs until it calls debugVideoCaptureStop
333
+ if (debugKeys ? keyWasPressed('Digit8') : debugVideoCapture.fromOverlay)
334
+ debugVideoCaptureStop();
335
+ }
336
+ else if (debugKeys && keyWasPressed('Digit8'))
337
+ {
338
+ debugVideoCaptureStart();
339
+ if (debugVideoCapture)
340
+ debugVideoCapture.fromOverlay = true;
341
+ }
342
+ }
343
+
344
+ // the text beside the mouse, with the same shadow as the overlay
345
+ function debugMouseText(text)
346
+ {
347
+ mainContext.save();
348
+ debugTextShadow(mainContext);
349
+ drawTextScreen(text, mousePosScreen, 24, WHITE, 0, BLACK, 'center', 'monospace', undefined, undefined, 0, mainContext);
350
+ mainContext.restore();
351
+ }
352
+
353
+ // the center of the tile under a world position, on the grid of the collision layer that has a tile there
354
+ function debugTileCellCenter(pos)
355
+ {
356
+ for (const layer of tileCollisionLayers)
357
+ {
358
+ const local = pos.subtract(layer.pos);
359
+ if (local.arrayCheck(layer.size) && layer.getCollisionData(local))
360
+ return local.floor().add(layer.pos).add(vec2(.5));
361
+ }
362
+ return pos.floor().add(vec2(.5));
363
+ }
364
+
365
+ // the tile layers Debug Tiles can show, the ones not switched off with debugShow, in render order
366
+ function debugTileLayersShown()
367
+ {
368
+ return engineObjects.filter(o=> o instanceof TileLayer && !o.destroyed && o.debugShow)
369
+ .sort((a, b)=> a.renderOrder - b.renderOrder); // a stable sort, so equal orders keep the order they were made in
370
+ }
371
+
372
+ // the layers Debug Tiles shows right now, from the ones shown: every one, or the one the 2 key has stepped to,
373
+ // every one again if layers went away since
374
+ function debugTileLayersSelected(layers)
375
+ {
376
+ const layer = layers[debugTiles - 2];
377
+ return layer ? [layer] : layers;
378
+ }
379
+
380
+ // what the overlay menu says Debug Tiles is showing
381
+ function debugTilesLabel(layers)
382
+ {
383
+ if (!debugTiles) return '';
384
+ const layer = layers[debugTiles - 2];
385
+ return layer ? ` (layer ${debugTiles - 1} of ${layers.length}, renderOrder ${layer.renderOrder}${layer instanceof TileCollisionLayer ? ', collision' : ''})` : ' (all)';
386
+ }
387
+
388
+ // Debug Tiles: each layer's bounds, then the collision value of every cell on screen, tinted by value, with the
389
+ // number when a tile is big enough on screen to read it
390
+ function debugTileLayers(layers)
391
+ {
392
+ // everything the camera can see, turned or not
393
+ const reach = getCameraSize().length() / 2;
394
+ const showValues = cameraScale >= 24;
395
+ for (const layer of debugTileLayersSelected(layers))
396
+ {
397
+ const isCollision = layer instanceof TileCollisionLayer, size = layer.size, pos = layer.pos;
398
+ const color = isCollision ? '#f80' : '#0cf';
399
+ debugRect(pos.add(size.scale(.5)), size, color, 0, 0, false, false);
400
+ const label = 'layer ' + (layers.indexOf(layer) + 1) + ', ' + size.x + 'x' + size.y + ', renderOrder ' + layer.renderOrder + (isCollision ? ', collision' : '');
401
+ debugText(label, pos.add(vec2(size.x / 2, size.y + .4)), .6, color, 0, 0, 'monospace', false);
402
+ if (!isCollision) continue;
403
+
404
+ const x0 = max(0, floor(cameraPos.x - reach - pos.x)), x1 = min(size.x, ceil(cameraPos.x + reach - pos.x));
405
+ const y0 = max(0, floor(cameraPos.y - reach - pos.y)), y1 = min(size.y, ceil(cameraPos.y + reach - pos.y));
406
+ for (let y = y0; y < y1; ++y)
407
+ for (let x = x0; x < x1; ++x)
408
+ {
409
+ const data = layer.collisionData[y * size.x + x];
410
+ if (!data) continue;
411
+ const center = vec2(pos.x + x + .5, pos.y + y + .5), tint = hsl(data * .17 % 1, 1, .6, .8);
412
+ debugRect(center, vec2(.9), tint, 0, 0, false, false);
413
+ showValues && debugText(data, center, .5, tint, 0, 0, 'monospace', false);
414
+ }
415
+ }
416
+ }
417
+
418
+ // the tile and collision value under the mouse of each layer shown by Debug Tiles, as lines of the mouse text
419
+ function debugTileText(layers)
420
+ {
421
+ if (!debugTiles) return '';
422
+ // each layer by its number in the 2 key's cycle, the one the menu shows
423
+ let text = '';
424
+ for (const layer of debugTileLayersSelected(layers))
425
+ {
426
+ const local = mousePos.subtract(layer.pos);
427
+ if (!local.arrayCheck(layer.size)) continue;
428
+ // only a collision layer has collision data, the others are just drawn
429
+ const data = layer.getData(local);
430
+ const collision = layer instanceof TileCollisionLayer && layer.getCollisionData(local);
431
+ text += '\nlayer ' + (layers.indexOf(layer) + 1) + ': tile ' + (data?.tile ?? 'empty') + (collision ? ', collision ' + collision : '');
432
+ }
433
+ return text;
434
+ }
435
+
436
+ function debugRender()
437
+ {
438
+ if (debugTakeScreenshot)
439
+ {
440
+ // combine canvases, remove alpha and save, before the capture check so it is taken during video capture too
441
+ glFlush();
442
+ combineCanvases();
443
+ saveCanvas(mainCanvas);
444
+ debugTakeScreenshot = 0;
445
+ }
446
+
447
+ if (debugVideoCaptureIsActive())
448
+ {
449
+ // don't show debug info when capturing video, but still drop the expired primitives so they don't pile up
450
+ debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
451
+ return;
452
+ }
453
+
454
+ // flush any gl sprites before drawing debug info
455
+ glFlush();
456
+
457
+ // the tile layers Debug Tiles can show, found once for everything below
458
+ const tileLayers = debugTiles ? debugTileLayersShown() : [];
459
+
460
+ const savedDrawCount = drawCount;
461
+ const savedPrimitiveCount = primitiveCount;
462
+
463
+ const debugContext = mainContext;
464
+ if (debugGamepads && gamepadsEnable)
465
+ {
466
+ // draw gamepads
467
+ const maxGamepads = 8;
468
+ let gamepadConnectedCount = 0;
469
+ for (let i = 0; i < maxGamepads; i++)
470
+ gamepadConnected(i) && gamepadConnectedCount++;
471
+
472
+ for (let i = 0; i < maxGamepads; i++)
473
+ {
474
+ if (!gamepadConnected(i))
475
+ continue;
476
+
477
+ const stickScale = 1;
478
+ const buttonScale = .2;
479
+ const cornerPos = cameraPos.add(vec2(-stickScale*2, ((gamepadConnectedCount-1)/2-i)*stickScale*3));
480
+ debugText(i, cornerPos.add(vec2(-stickScale, stickScale)), 1, WHITE, 0, 0, 'monospace', false);
481
+ if (i === gamepadPrimary)
482
+ debugText('Main', cornerPos.add(vec2(-stickScale*2, 0)),1, '#0f0', 0, 0, 'monospace', false);
483
+
484
+ // read analog sticks
485
+ const stickCount = gamepadStickCount(i); // none after an inputClear this frame
486
+ for (let j = 0; j < stickCount; j++)
487
+ {
488
+ if (!(j in gamepadStickData[i]))
489
+ continue; // skip sticks that are not present (eg a disabled touch left stick)
490
+ const stick = gamepadStick(j, i);
491
+ const drawPos = cornerPos.add(vec2(j*stickScale*2, 0));
492
+ const stickPos = drawPos.add(stick.scale(stickScale));
493
+ debugCircle(drawPos, stickScale*2, '#fff7',0,true, false);
494
+ debugLine(drawPos, stickPos, '#f00', .1, 0, false);
495
+ debugText(j, drawPos, .3, WHITE, 0, 0, 'monospace', false);
496
+ debugPoint(stickPos, '#f00', undefined, undefined, false);
497
+ }
498
+
499
+ const buttonCount = inputData[i+1].length;
500
+ for (let j = 0; j < buttonCount; j++)
501
+ {
502
+ const drawPos = cornerPos.add(vec2(j*buttonScale*2, -stickScale-buttonScale*2));
503
+ const pressed = gamepadIsDown(j, i);
504
+ debugCircle(drawPos, buttonScale*2, pressed ? '#f00' : '#fff7', 0, true, false);
505
+ debugText(j, drawPos, .3, WHITE, 0, 0, 'monospace', false);
506
+ }
507
+ }
508
+ }
509
+
510
+ let debugObject;
511
+ if (debugOverlay)
512
+ {
513
+ // draw red rectangle around screen
514
+ const cameraSize = getCameraSize();
515
+ debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008', 0, 0, false, false);
516
+
517
+ // mouse pick
518
+ let bestDistance = Infinity;
519
+ for (const o of engineObjects)
520
+ {
521
+ if (o.destroyed)
522
+ continue;
523
+
524
+ if (o instanceof TileLayer)
525
+ continue; // prevent tile layers from being picked
526
+
527
+ o.renderDebugInfo();
528
+ if (!o.size.x || !o.size.y)
529
+ continue;
530
+
531
+ const distance = mousePos.distanceSquared(o.pos);
532
+ if (distance < bestDistance)
533
+ {
534
+ bestDistance = distance;
535
+ debugObject = o;
536
+ }
537
+ }
538
+
539
+ if (tileCollisionTest(mousePos))
540
+ {
541
+ // show the collision tile under the mouse, on its layer's own grid
542
+ drawRect(debugTileCellCenter(mousePos), vec2(1), rgb(1,1,0,.5), 0, false, false);
543
+ }
544
+ }
545
+
546
+ // Debug Tiles draws with the overlay closed too, like the other modes listed in the corner
547
+ debugTiles && debugTileLayers(tileLayers);
548
+
549
+ {
550
+ // draw debug primitives, the line width put back after so the game's next frame starts as it would in release
551
+ debugContext.save();
552
+ debugContext.lineWidth = 2;
553
+ debugPrimitives.forEach(p=>
554
+ {
555
+ debugContext.save();
556
+
557
+ // create canvas transform from world space to screen space
558
+ // without scaling because we want consistent pixel sizes
559
+ let pos = p.pos, scale = 1, angle = p.angle;
560
+ if (!p.screenSpace)
561
+ {
562
+ pos = worldToScreen(p.pos);
563
+ scale = cameraScale;
564
+ angle -= cameraAngle;
565
+ }
566
+ debugContext.translate(pos.x|0, pos.y|0);
567
+ debugContext.rotate(angle);
568
+ debugContext.scale(1, p.text !== undefined ? 1 : -1);
569
+ debugContext.fillStyle = p.color;
570
+ debugContext.strokeStyle = p.color;
571
+ if (p.text !== undefined)
572
+ {
573
+ debugContext.font = p.size*scale + 'px '+ p.font;
574
+ debugContext.textAlign = 'center';
575
+ debugContext.textBaseline = 'middle';
576
+ debugContext.fillText(p.text, 0, 0);
577
+ }
578
+ else if (p.points !== undefined)
579
+ {
580
+ // poly
581
+ debugContext.beginPath();
582
+ for (const point of p.points)
583
+ {
584
+ const p2 = point.scale(scale).floor();
585
+ debugContext.lineTo(p2.x, p2.y);
586
+ }
587
+ debugContext.closePath();
588
+ p.fill && debugContext.fill();
589
+ debugContext.stroke();
590
+ }
591
+ else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
592
+ {
593
+ // point
594
+ const pointSize = debugPointSize * scale;
595
+ debugContext.fillRect(-pointSize/2, -1, pointSize, 3);
596
+ debugContext.fillRect(-1, -pointSize/2, 3, pointSize);
597
+ }
598
+ else if (p.size.x !== undefined)
599
+ {
600
+ // rect
601
+ const s = p.size.scale(scale).floor();
602
+ const w = s.x, h = s.y;
603
+ p.fill && debugContext.fillRect(-w/2|0, -h/2|0, w, h);
604
+ debugContext.strokeRect(-w/2|0, -h/2|0, w, h);
605
+ }
606
+ else
607
+ {
608
+ // circle
609
+ debugContext.beginPath();
610
+ debugContext.arc(0, 0, abs(p.size)*scale/2, 0, 9); // a negative radius would throw
611
+ p.fill && debugContext.fill();
612
+ debugContext.stroke();
613
+ }
614
+
615
+ debugContext.restore();
616
+ });
617
+ debugContext.restore();
618
+
619
+ // remove expired primitives
620
+ debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
621
+ }
622
+
623
+ if (debugObject)
624
+ {
625
+ const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
626
+ raycastHitPos && drawRect(debugTileCellCenter(raycastHitPos), vec2(1), rgb(0,1,1,.3), 0, false, false);
627
+ drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5), undefined, undefined, false, false);
628
+
629
+ let debugText = 'mouse pos = ' + mousePos;
630
+ if (tileCollisionLayers.length)
631
+ debugText += '\nmouse collision = ' + tileCollisionGetData(mousePos);
632
+ debugText += debugTileText(tileLayers);
633
+ debugText += '\n\n--- object info ---\n';
634
+ debugText += debugObject.toString();
635
+ debugMouseText(debugText);
636
+ }
637
+ else if (debugOverlay && debugTiles)
638
+ {
639
+ // no object to pick, the tiles under the mouse on their own
640
+ const text = debugTileText(tileLayers);
641
+ text && debugMouseText('mouse pos = ' + mousePos + text);
642
+ }
643
+
644
+ {
645
+ // draw debug overlay
646
+ const fontSize = 20;
647
+ const lineHeight = fontSize * 1.2 | 0;
648
+ debugContext.save();
649
+ debugContext.fillStyle = '#fff';
650
+ debugContext.textAlign = 'left';
651
+ debugContext.textBaseline = 'top';
652
+ debugContext.font = fontSize + 'px monospace';
653
+ debugTextShadow(debugContext);
654
+
655
+ let x = 9, y = 0, h = lineHeight;
656
+ if (debugOverlay)
657
+ {
658
+ debugContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
659
+ debugContext.fillText('Time: ' + formatTime(time), x, y += h);
660
+ debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
661
+ x, y += h);
662
+ debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
663
+ debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
664
+ debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
665
+ debugContext.fillText('---------', x, y += h);
666
+ debugContext.fillStyle = '#f00';
667
+ debugContext.fillText('ESC: Debug Overlay', x, y += h);
668
+ debugContext.fillStyle = debugPhysics ? '#f00' : '#fff';
669
+ debugContext.fillText('1: Debug Physics', x, y += h);
670
+ debugContext.fillStyle = debugTiles ? '#f00' : '#fff';
671
+ debugContext.fillText('2: Debug Tiles' + debugTilesLabel(tileLayers), x, y += h);
672
+ debugContext.fillStyle = debugParticles ? '#f00' : '#fff';
673
+ debugContext.fillText('3: Debug Particles', x, y += h);
674
+ debugContext.fillStyle = debugRaycast ? '#f00' : '#fff';
675
+ debugContext.fillText('4: Debug Raycasts', x, y += h);
676
+ debugContext.fillStyle = debugGamepads ? '#f00' : '#fff';
677
+ debugContext.fillText('5: Debug Gamepads', x, y += h);
678
+ debugContext.fillStyle = debugSound ? '#f00' : '#fff';
679
+ debugContext.fillText('6: Debug Sound', x, y += h);
680
+ debugContext.fillStyle = '#fff';
681
+ debugContext.fillText('7: Save Screenshot', x, y += h);
682
+ debugContext.fillText('8: Toggle Video Capture', x, y += h);
683
+ debugContext.fillStyle = debugTweakables ? '#f00' : '#fff';
684
+ debugContext.fillText('9: Tweakables', x, y += h);
685
+ debugContext.fillStyle = levelEditor.isOpen ? '#f00' : '#fff';
686
+ debugContext.fillText('0: Edit Level', x, y += h);
687
+ debugContext.fillStyle = '#fff';
688
+
689
+ let keysPressed = '';
690
+ let mousePressed = '';
691
+ for (const i in inputData[0])
692
+ {
693
+ // read the input state itself, keyIsDown asserts on the numbered keys for..in gives as strings
694
+ if (!(inputData[0][i] & 1))
695
+ continue;
696
+ if (!isNaN(+i)) // mouse buttons are numbered, keys are named
697
+ mousePressed += i + ' ' ;
698
+ else
699
+ keysPressed += i + ' ' ;
700
+ }
701
+ mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
702
+ keysPressed && debugContext.fillText('Keys: ' + keysPressed, x, y += h);
703
+
704
+ // show gamepad buttons
705
+ for (let i = 1; i < inputData.length; i++)
706
+ {
707
+ let buttonsPressed = '';
708
+ if (inputData[i])
709
+ for (const j in inputData[i])
710
+ {
711
+ if (inputData[i][j] & 1)
712
+ buttonsPressed += j + ' ' ;
713
+ }
714
+ buttonsPressed && debugContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
715
+ }
716
+ }
717
+ else
718
+ {
719
+ debugContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
720
+ debugContext.fillText(debugTiles ? 'Debug Tiles' + debugTilesLabel(tileLayers) : '', x, y += h);
721
+ debugContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
722
+ debugContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
723
+ debugContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
724
+ debugContext.fillText(debugSound ? 'Debug Sound' : '', x, y += h);
725
+ }
726
+
727
+ debugContext.restore();
728
+ }
729
+
730
+ if (debugWatermark || debugOverlay)
731
+ {
732
+ // show fps stats display, the text state put back after for the game's next frame
733
+ mainContext.save();
734
+ mainContext.textAlign = 'right';
735
+ mainContext.textBaseline = 'top';
736
+ mainContext.font = '1em monospace';
737
+ mainContext.fillStyle = '#000';
738
+ const text = engineName + ' v' + engineVersion + ' / '
739
+ + savedDrawCount + ' / ' + savedPrimitiveCount + ' / '
740
+ + engineObjects.length + ' / ' + averageFPS.toFixed(1)
741
+ + (glEnable ? ' GL' : ' 2D') ;
742
+ mainContext.fillText(text, mainCanvasSize.x-3, 3);
743
+ mainContext.fillStyle = '#fff';
744
+ mainContext.fillText(text, mainCanvasSize.x-2, 2);
745
+ mainContext.restore();
746
+ }
747
+ }
748
+
749
+ function debugRenderPost() { debugVideoCaptureIsActive() && debugVideoCaptureUpdate(); }
750
+
751
+ ///////////////////////////////////////////////////////////////////////////////
752
+ // video capture - records video and audio at 60 fps using MediaRecorder API
753
+
754
+ // internal variables used to capture video
755
+ let debugVideoCapture, debugVideoCaptureIcon;
756
+
757
+ /** Check if video capture is active
758
+ * @memberof Debug */
759
+ function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
760
+
761
+ /** Start capturing video
762
+ * @memberof Debug */
763
+ function debugVideoCaptureStart()
764
+ {
765
+ ASSERT(!debugVideoCaptureIsActive(), 'Already capturing video!');
766
+
767
+ // everything that can fail where recording is unsupported is in the try, a missing captureStream
768
+ // or a MediaRecorder that refuses webm throws here and is cleaned up rather than stopping the game
769
+ const captureTimer = new Timer(0, true);
770
+ const chunks = [];
771
+ let videoTrack, mediaRecorder, audioStreamDestination, silentAudioSource, audioTapNode;
772
+ try
773
+ {
774
+ // setup captureStream to capture manually by passing 0
775
+ const stream = mainCanvas.captureStream(0);
776
+ videoTrack = stream.getVideoTracks()[0];
777
+ videoTrack.applyConstraints({frameRate:frameRate}).catch(()=>{});
778
+
779
+ // set up the media recorder
780
+ mediaRecorder = new MediaRecorder(stream,
781
+ {mimeType:'video/webm;codecs=vp8'});
782
+ mediaRecorder.ondataavailable = (e)=> chunks.push(e.data);
783
+ mediaRecorder.onstop = ()=>
784
+ {
785
+ const blob = new Blob(chunks, {type: 'video/webm'});
786
+ const url = URL.createObjectURL(blob);
787
+ saveDataURL(url, 'capture.webm', 1e3);
788
+ };
789
+
790
+ if (soundEnable)
791
+ {
792
+ // create silent audio source
793
+ // fixes issue where video can not start recording without audio
794
+ silentAudioSource = new ConstantSourceNode(audioContext, { offset: 0 });
795
+ silentAudioSource.connect(audioMasterGain);
796
+ silentAudioSource.start();
797
+
798
+ // tap the end of the master chain so a master effect is in the recording
799
+ // (a master effect swapped mid-capture drops the tap, the rest records silent)
800
+ audioTapNode = audioMasterEffectOutput || audioMasterGain;
801
+ audioStreamDestination = audioContext.createMediaStreamDestination();
802
+ audioTapNode.connect(audioStreamDestination);
803
+ for (const track of audioStreamDestination.stream.getAudioTracks())
804
+ stream.addTrack(track); // add audio tracks to capture stream
805
+ }
806
+
807
+ // start recording
808
+ mediaRecorder.start();
809
+ }
810
+ catch(e)
811
+ {
812
+ LOG('Video capture not supported in this browser!');
813
+ videoTrack?.stop();
814
+ silentAudioSource?.stop();
815
+ if (audioStreamDestination)
816
+ {
817
+ // the tap may not have been made before the failure
818
+ try { audioTapNode.disconnect(audioStreamDestination); }
819
+ catch { }
820
+ }
821
+ return;
822
+ }
823
+
824
+ if (!debugVideoCaptureIcon)
825
+ {
826
+ // create recording icon to show it is capturing video
827
+ debugVideoCaptureIcon = document.createElement('div');
828
+ debugVideoCaptureIcon.style.position = 'absolute';
829
+ debugVideoCaptureIcon.style.padding = '9px';
830
+ debugVideoCaptureIcon.style.color = '#f00';
831
+ debugVideoCaptureIcon.style.font = '50px monospace';
832
+ document.body.appendChild(debugVideoCaptureIcon);
833
+ }
834
+ // show recording icon, only once recording has started
835
+ debugVideoCaptureIcon.textContent = '';
836
+ debugVideoCaptureIcon.style.display = '';
837
+
838
+ LOG('Video capture started.');
839
+
840
+ // save debug video info
841
+ debugVideoCapture =
842
+ {
843
+ mediaRecorder,
844
+ captureTimer,
845
+ videoTrack,
846
+ silentAudioSource,
847
+ audioStreamDestination,
848
+ audioTapNode
849
+ };
850
+ }
851
+
852
+ /** Stop capturing video and save to disk
853
+ * @memberof Debug */
854
+ function debugVideoCaptureStop()
855
+ {
856
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
857
+
858
+ // stop recording
859
+ LOG(`Video capture ended. ${debugVideoCapture.captureTimer.get().toFixed(2)} seconds recorded.`);
860
+ debugVideoCaptureIcon.style.display = 'none';
861
+ debugVideoCapture.silentAudioSource?.stop();
862
+ debugVideoCapture.mediaRecorder?.stop();
863
+ debugVideoCapture.videoTrack?.stop();
864
+ if (debugVideoCapture.audioStreamDestination)
865
+ {
866
+ // the tap is already gone if the master effect changed during the capture
867
+ try { debugVideoCapture.audioTapNode.disconnect(debugVideoCapture.audioStreamDestination); }
868
+ catch { }
869
+ }
870
+ debugVideoCapture = undefined;
871
+ }
872
+
873
+ // update video capture, called automatically by engine
874
+ function debugVideoCaptureUpdate()
875
+ {
876
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
877
+
878
+ // save the video frame
879
+ combineCanvases();
880
+ debugVideoCapture.videoTrack.requestFrame();
881
+ debugVideoCaptureIcon.textContent = '● REC '
882
+ + formatTime(debugVideoCapture.captureTimer.get());
883
+ }
884
+
885
+ ///////////////////////////////////////////////////////////////////////////////
886
+ // debug utility functions
887
+
888
+ // make color constants immutable with debug assertions
889
+ function debugProtectConstant(obj)
890
+ {
891
+ if (debug)
892
+ {
893
+ // get properties and store original values
894
+ const props = Object.keys(obj), values = {};
895
+ props.forEach(prop => values[prop] = obj[prop]);
896
+
897
+ // replace with getters/setters that assert
898
+ props.forEach(prop =>
899
+ {
900
+ Object.defineProperty(obj, prop, {
901
+ get: ()=> values[prop],
902
+ set: (value)=>
903
+ {
904
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
905
+ },
906
+ enumerable: true
907
+ });
908
+ });
909
+ }
910
+
911
+ // freeze the object to prevent adding new properties
912
+ return Object.freeze(obj);
736
913
  }