littlejsengine 1.19.3 → 1.21.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 (54) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2857 -955
  4. package/dist/littlejs.esm.js +33428 -20995
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +33386 -20996
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +27841 -20422
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +786 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +148 -27
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +25 -0
  21. package/plugins/postProcess.js +96 -24
  22. package/plugins/render3d.js +2067 -1632
  23. package/plugins/render3dDebug.js +110 -0
  24. package/plugins/render3dEditor.js +1812 -0
  25. package/plugins/render3dExtras.js +1948 -0
  26. package/plugins/render3dLevel.js +189 -0
  27. package/plugins/render3dVoxels.js +514 -0
  28. package/plugins/sceneSystem.js +58 -0
  29. package/plugins/textureSheet.js +496 -458
  30. package/plugins/threejs.js +183 -163
  31. package/plugins/tweakables.js +411 -0
  32. package/plugins/tweenSystem.js +265 -145
  33. package/plugins/uiSystem.js +2024 -1687
  34. package/plugins/zzfxm.js +159 -159
  35. package/src/engine.js +378 -97
  36. package/src/engineAudio.js +231 -127
  37. package/src/engineBuild.mjs +292 -256
  38. package/src/engineDebug.js +967 -743
  39. package/src/engineDraw.js +391 -198
  40. package/src/engineEditor.js +2452 -0
  41. package/src/engineExport.js +18 -0
  42. package/src/engineInput.js +1466 -1293
  43. package/src/engineLogo.js +19 -0
  44. package/src/engineMath.js +1274 -1252
  45. package/src/engineObject.js +197 -82
  46. package/src/engineParticles.js +583 -532
  47. package/src/engineRelease.js +79 -45
  48. package/src/engineSettings.js +115 -29
  49. package/src/engineTileLayer.js +995 -741
  50. package/src/engineUtilities.js +19 -13
  51. package/src/engineWebGL.js +239 -82
  52. package/src/jsconfig.json +17 -13
  53. package/plugins/box2d.wasm.js +0 -630
  54. package/plugins/box2d.wasm.wasm +0 -0
@@ -1,743 +1,967 @@
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, mainCanvasSize.x-3, 3);
578
- mainContext.fillStyle = '#fff';
579
- mainContext.fillText(text, mainCanvasSize.x-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, audioTapNode;
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
- // tap the end of the master chain so a master effect is in the recording
650
- // (a master effect swapped mid-capture drops the tap, the rest records silent)
651
- audioStreamDestination = audioContext.createMediaStreamDestination();
652
- audioTapNode = audioMasterEffectOutput || audioMasterGain;
653
- audioTapNode.connect(audioStreamDestination);
654
- for (const track of audioStreamDestination.stream.getAudioTracks())
655
- stream.addTrack(track); // add audio tracks to capture stream
656
- }
657
-
658
- // start recording
659
- try { mediaRecorder.start(); }
660
- catch(e)
661
- {
662
- LOG('Video capture not supported in this browser!');
663
- silentAudioSource?.stop();
664
- audioStreamDestination && audioTapNode.disconnect(audioStreamDestination);
665
- return;
666
- }
667
-
668
- LOG('Video capture started.');
669
-
670
- // save debug video info
671
- debugVideoCapture =
672
- {
673
- mediaRecorder,
674
- captureTimer,
675
- videoTrack,
676
- silentAudioSource,
677
- audioStreamDestination,
678
- audioTapNode
679
- };
680
- }
681
-
682
- /** Stop capturing video and save to disk
683
- * @memberof Debug */
684
- function debugVideoCaptureStop()
685
- {
686
- ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
687
-
688
- // stop recording
689
- LOG(`Video capture ended. ${debugVideoCapture.captureTimer.get().toFixed(2)} seconds recorded.`);
690
- debugVideoCaptureIcon.style.display = 'none';
691
- debugVideoCapture.silentAudioSource?.stop();
692
- debugVideoCapture.mediaRecorder?.stop();
693
- debugVideoCapture.videoTrack?.stop();
694
- if (debugVideoCapture.audioStreamDestination)
695
- {
696
- // the tap is already gone if the master effect changed during the capture
697
- try { debugVideoCapture.audioTapNode.disconnect(debugVideoCapture.audioStreamDestination); }
698
- catch { }
699
- }
700
- debugVideoCapture = undefined;
701
- }
702
-
703
- // update video capture, called automatically by engine
704
- function debugVideoCaptureUpdate()
705
- {
706
- ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
707
-
708
- // save the video frame
709
- combineCanvases();
710
- debugVideoCapture.videoTrack.requestFrame();
711
- debugVideoCaptureIcon.textContent = '● REC '
712
- + formatTime(debugVideoCapture.captureTimer);
713
- }
714
-
715
- ///////////////////////////////////////////////////////////////////////////////
716
- // debug utility functions
717
-
718
- // make color constants immutable with debug assertions
719
- function debugProtectConstant(obj)
720
- {
721
- if (debug)
722
- {
723
- // get properties and store original values
724
- const props = Object.keys(obj), values = {};
725
- props.forEach(prop => values[prop] = obj[prop]);
726
-
727
- // replace with getters/setters that assert
728
- props.forEach(prop =>
729
- {
730
- Object.defineProperty(obj, prop, {
731
- get: ()=> values[prop],
732
- set: (value)=>
733
- {
734
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
735
- },
736
- enumerable: true
737
- });
738
- });
739
- }
740
-
741
- // freeze the object to prevent adding new properties
742
- return Object.freeze(obj);
743
- }
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
+ for (const line of debugOverlayKeys)
688
+ {
689
+ // a key a debug plugin added, when it has something to say
690
+ const key = line();
691
+ if (!key) continue;
692
+ debugContext.fillStyle = key.on ? '#f00' : '#fff';
693
+ debugContext.fillText(key.text, x, y += h);
694
+ }
695
+ debugContext.fillStyle = '#fff';
696
+
697
+ let keysPressed = '';
698
+ let mousePressed = '';
699
+ for (const i in inputData[0])
700
+ {
701
+ // read the input state itself, keyIsDown asserts on the numbered keys for..in gives as strings
702
+ if (!(inputData[0][i] & 1))
703
+ continue;
704
+ if (!isNaN(+i)) // mouse buttons are numbered, keys are named
705
+ mousePressed += i + ' ' ;
706
+ else
707
+ keysPressed += i + ' ' ;
708
+ }
709
+ mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
710
+ keysPressed && debugContext.fillText('Keys: ' + keysPressed, x, y += h);
711
+
712
+ // show gamepad buttons
713
+ for (let i = 1; i < inputData.length; i++)
714
+ {
715
+ let buttonsPressed = '';
716
+ if (inputData[i])
717
+ for (const j in inputData[i])
718
+ {
719
+ if (inputData[i][j] & 1)
720
+ buttonsPressed += j + ' ' ;
721
+ }
722
+ buttonsPressed && debugContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
723
+ }
724
+ }
725
+ else
726
+ {
727
+ debugContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
728
+ debugContext.fillText(debugTiles ? 'Debug Tiles' + debugTilesLabel(tileLayers) : '', x, y += h);
729
+ debugContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
730
+ debugContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
731
+ debugContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
732
+ debugContext.fillText(debugSound ? 'Debug Sound' : '', x, y += h);
733
+ }
734
+
735
+ debugContext.restore();
736
+ }
737
+
738
+ if (debugWatermark || debugOverlay)
739
+ {
740
+ // show fps stats display, the text state put back after for the game's next frame
741
+ mainContext.save();
742
+ mainContext.textAlign = 'right';
743
+ mainContext.textBaseline = 'top';
744
+ mainContext.font = '1em monospace';
745
+ mainContext.fillStyle = '#000';
746
+ const text = engineName + ' v' + engineVersion + ' / '
747
+ + savedDrawCount + ' / ' + savedPrimitiveCount + ' / '
748
+ + engineObjects.length + ' / ' + averageFPS.toFixed(1)
749
+ + (glEnable ? ' GL' : ' 2D') ;
750
+ mainContext.fillText(text, mainCanvasSize.x-3, 3);
751
+ mainContext.fillStyle = '#fff';
752
+ mainContext.fillText(text, mainCanvasSize.x-2, 2);
753
+ mainContext.restore();
754
+ }
755
+ }
756
+
757
+ function debugRenderPost() { debugVideoCaptureIsActive() && debugVideoCaptureUpdate(); }
758
+
759
+ ///////////////////////////////////////////////////////////////////////////////
760
+ // video capture - records video and audio at 60 fps using MediaRecorder API
761
+
762
+ // internal variables used to capture video
763
+ let debugVideoCapture, debugVideoCaptureIcon;
764
+
765
+ /** Check if video capture is active
766
+ * @memberof Debug */
767
+ function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
768
+
769
+ /** Start capturing video
770
+ * @memberof Debug */
771
+ function debugVideoCaptureStart()
772
+ {
773
+ ASSERT(!debugVideoCaptureIsActive(), 'Already capturing video!');
774
+
775
+ // everything that can fail where recording is unsupported is in the try, a missing captureStream
776
+ // or a MediaRecorder that refuses webm throws here and is cleaned up rather than stopping the game
777
+ const captureTimer = new Timer(0, true);
778
+ const chunks = [];
779
+ let videoTrack, mediaRecorder, audioStreamDestination, silentAudioSource, audioTapNode;
780
+ try
781
+ {
782
+ // setup captureStream to capture manually by passing 0
783
+ const stream = mainCanvas.captureStream(0);
784
+ videoTrack = stream.getVideoTracks()[0];
785
+ videoTrack.applyConstraints({frameRate:frameRate}).catch(()=>{});
786
+
787
+ // set up the media recorder
788
+ mediaRecorder = new MediaRecorder(stream,
789
+ {mimeType:'video/webm;codecs=vp8'});
790
+ mediaRecorder.ondataavailable = (e)=> chunks.push(e.data);
791
+ mediaRecorder.onstop = ()=>
792
+ {
793
+ const blob = new Blob(chunks, {type: 'video/webm'});
794
+ const url = URL.createObjectURL(blob);
795
+ saveDataURL(url, 'capture.webm', 1e3);
796
+ };
797
+
798
+ if (soundEnable)
799
+ {
800
+ // create silent audio source
801
+ // fixes issue where video can not start recording without audio
802
+ silentAudioSource = new ConstantSourceNode(audioContext, { offset: 0 });
803
+ silentAudioSource.connect(audioMasterGain);
804
+ silentAudioSource.start();
805
+
806
+ // tap the end of the master chain so a master effect is in the recording
807
+ // (a master effect swapped mid-capture drops the tap, the rest records silent)
808
+ audioTapNode = audioMasterEffectOutput || audioMasterGain;
809
+ audioStreamDestination = audioContext.createMediaStreamDestination();
810
+ audioTapNode.connect(audioStreamDestination);
811
+ for (const track of audioStreamDestination.stream.getAudioTracks())
812
+ stream.addTrack(track); // add audio tracks to capture stream
813
+ }
814
+
815
+ // start recording
816
+ mediaRecorder.start();
817
+ }
818
+ catch(e)
819
+ {
820
+ LOG('Video capture not supported in this browser!');
821
+ videoTrack?.stop();
822
+ silentAudioSource?.stop();
823
+ if (audioStreamDestination)
824
+ {
825
+ // the tap may not have been made before the failure
826
+ try { audioTapNode.disconnect(audioStreamDestination); }
827
+ catch { }
828
+ }
829
+ return;
830
+ }
831
+
832
+ if (!debugVideoCaptureIcon)
833
+ {
834
+ // create recording icon to show it is capturing video
835
+ debugVideoCaptureIcon = document.createElement('div');
836
+ debugVideoCaptureIcon.style.position = 'absolute';
837
+ debugVideoCaptureIcon.style.padding = '9px';
838
+ debugVideoCaptureIcon.style.color = '#f00';
839
+ debugVideoCaptureIcon.style.font = '50px monospace';
840
+ document.body.appendChild(debugVideoCaptureIcon);
841
+ }
842
+ // show recording icon, only once recording has started
843
+ debugVideoCaptureIcon.textContent = '';
844
+ debugVideoCaptureIcon.style.display = '';
845
+
846
+ LOG('Video capture started.');
847
+
848
+ // save debug video info
849
+ debugVideoCapture =
850
+ {
851
+ mediaRecorder,
852
+ captureTimer,
853
+ videoTrack,
854
+ silentAudioSource,
855
+ audioStreamDestination,
856
+ audioTapNode
857
+ };
858
+ }
859
+
860
+ /** Stop capturing video and save to disk
861
+ * @memberof Debug */
862
+ function debugVideoCaptureStop()
863
+ {
864
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
865
+
866
+ // stop recording
867
+ LOG(`Video capture ended. ${debugVideoCapture.captureTimer.get().toFixed(2)} seconds recorded.`);
868
+ debugVideoCaptureIcon.style.display = 'none';
869
+ debugVideoCapture.silentAudioSource?.stop();
870
+ debugVideoCapture.mediaRecorder?.stop();
871
+ debugVideoCapture.videoTrack?.stop();
872
+ if (debugVideoCapture.audioStreamDestination)
873
+ {
874
+ // the tap is already gone if the master effect changed during the capture
875
+ try { debugVideoCapture.audioTapNode.disconnect(debugVideoCapture.audioStreamDestination); }
876
+ catch { }
877
+ }
878
+ debugVideoCapture = undefined;
879
+ }
880
+
881
+ // update video capture, called automatically by engine
882
+ function debugVideoCaptureUpdate()
883
+ {
884
+ ASSERT(debugVideoCaptureIsActive(), 'Not capturing video!');
885
+
886
+ // save the video frame
887
+ combineCanvases();
888
+ debugVideoCapture.videoTrack.requestFrame();
889
+ debugVideoCaptureIcon.textContent = '● REC '
890
+ + formatTime(debugVideoCapture.captureTimer.get());
891
+ }
892
+
893
+ ///////////////////////////////////////////////////////////////////////////////
894
+ // debug utility functions
895
+
896
+ // make color constants immutable with debug assertions
897
+ function debugProtectConstant(obj)
898
+ {
899
+ if (debug)
900
+ {
901
+ // get properties and store original values
902
+ const props = Object.keys(obj), values = {};
903
+ props.forEach(prop => values[prop] = obj[prop]);
904
+
905
+ // replace with getters/setters that assert
906
+ props.forEach(prop =>
907
+ {
908
+ Object.defineProperty(obj, prop, {
909
+ get: ()=> values[prop],
910
+ set: (value)=>
911
+ {
912
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
913
+ },
914
+ enumerable: true
915
+ });
916
+ });
917
+ }
918
+
919
+ // freeze the object to prevent adding new properties
920
+ return Object.freeze(obj);
921
+ }
922
+
923
+ ///////////////////////////////////////////////////////////////////////////////
924
+ // Input capture: the free camera and the 3D level editor take the keyboard and mouse, so the game under them reads
925
+ // every key and mouse button as up, and no mouse movement or wheel; gamepads are left to the game
926
+
927
+ let inputCaptureOn = false; // something has taken the keyboard and mouse
928
+ let inputCaptureReading = false; // it is reading them now
929
+ let inputCaptureDeltaScreen, inputCaptureWheel = 0; // this update's mouse movement and wheel, for it alone
930
+
931
+ // the game let go of a captured mouse itself, with pointerLockExit, since the debug tools last looked: a capture
932
+ // that is lost without it is the browser's doing, which is how Chrome takes Escape
933
+ let inputLockLetGo = false;
934
+ function inputLockExit() { inputLockLetGo = true; }
935
+
936
+ // the keys debug plugins add to the overlay's list, each a function that gives {text, on} or nothing to show
937
+ const debugOverlayKeys = [];
938
+
939
+ // take the keyboard and mouse from the game, or hand them back
940
+ function inputCapture(on=true)
941
+ {
942
+ inputCaptureOn = !!on;
943
+ inputCaptureDeltaScreen = vec2();
944
+ inputCaptureWheel = 0;
945
+ }
946
+
947
+ // read the input as the one that took it, keyIsDown and the rest work as always inside read
948
+ function inputCaptureRead(read)
949
+ {
950
+ const was = inputCaptureReading;
951
+ inputCaptureReading = true;
952
+ try { return read(); }
953
+ finally { inputCaptureReading = was; }
954
+ }
955
+
956
+ // if a read of the keyboard and mouse, device 0, is the game's while they are taken
957
+ function inputCaptureHides(device) { return inputCaptureOn && !inputCaptureReading && !device; }
958
+
959
+ // called first in inputUpdate: the mouse movement and wheel go to the one that took the input
960
+ function inputCaptureMouse()
961
+ {
962
+ if (!inputCaptureOn) return;
963
+ inputCaptureDeltaScreen = mouseDeltaScreen;
964
+ inputCaptureWheel = mouseWheel;
965
+ mouseDeltaScreen = vec2();
966
+ mouseWheel = 0;
967
+ }