littlejsengine 1.14.10 → 1.14.16

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 (76) hide show
  1. package/dist/littlejs.d.ts +264 -120
  2. package/dist/littlejs.esm.js +603 -288
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +600 -287
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +521 -232
  7. package/examples/box2d/gameObjects.js +2 -2
  8. package/examples/breakout/gameObjects.js +2 -2
  9. package/examples/breakoutTutorial/README.md +32 -32
  10. package/examples/breakoutTutorial/game.js +1 -1
  11. package/examples/electron/game.js +3 -3
  12. package/examples/electron/index.html +2 -2
  13. package/examples/electron/package.json +1 -8
  14. package/examples/index.html +59 -51
  15. package/examples/module/game.js +3 -3
  16. package/examples/platformer/gameCharacter.js +6 -2
  17. package/examples/platformer/gameEffects.js +4 -4
  18. package/examples/platformer/gameLevel.js +1 -1
  19. package/examples/platformer/gameObjects.js +11 -9
  20. package/examples/puzzle/game.js +1 -1
  21. package/examples/shorts/animation.js +1 -1
  22. package/examples/shorts/base.html +1 -1
  23. package/examples/shorts/blending.js +8 -14
  24. package/examples/shorts/box2d.js +7 -3
  25. package/examples/shorts/box2dCar.js +2 -1
  26. package/examples/shorts/empty.js +30 -0
  27. package/examples/shorts/helloWorld.js +1 -1
  28. package/examples/shorts/hillGlideGame.js +10 -4
  29. package/examples/shorts/landerGame.js +30 -3
  30. package/examples/shorts/medals.js +4 -4
  31. package/examples/shorts/music.js +29 -55
  32. package/examples/shorts/musicPlayer.js +134 -0
  33. package/examples/shorts/nineSlice.js +34 -15
  34. package/examples/shorts/parallax.js +4 -3
  35. package/examples/shorts/particles.js +16 -16
  36. package/examples/shorts/piano.js +15 -21
  37. package/examples/shorts/pongGame.js +6 -4
  38. package/examples/shorts/raycasting.js +13 -5
  39. package/examples/shorts/sequencer.js +122 -0
  40. package/examples/shorts/shapes.js +7 -4
  41. package/examples/shorts/slidingPuzzle.js +16 -6
  42. package/examples/shorts/sound.js +18 -9
  43. package/examples/shorts/spaceGame.js +12 -8
  44. package/examples/shorts/spriteAtlas.js +7 -7
  45. package/examples/shorts/starfield.js +1 -1
  46. package/examples/shorts/systemFont.js +2 -2
  47. package/examples/shorts/texture.js +2 -2
  48. package/examples/shorts/tileLayer.js +10 -10
  49. package/examples/shorts/tiltedView.js +14 -3
  50. package/examples/shorts/timers.js +10 -0
  51. package/examples/shorts/topDown.js +4 -1
  52. package/examples/shorts/uiSystem.js +8 -5
  53. package/examples/starter/game.js +3 -3
  54. package/examples/starter/index.html +2 -2
  55. package/examples/typescript/game.js +3 -3
  56. package/examples/typescript/game.ts +3 -3
  57. package/examples/uiSystem/game.js +6 -6
  58. package/package.json +4 -2
  59. package/plugins/box2d.js +18 -3
  60. package/plugins/newgrounds.js +11 -9
  61. package/plugins/postProcess.js +5 -2
  62. package/plugins/uiSystem.js +120 -34
  63. package/plugins/zzfxm.js +5 -1
  64. package/reference.md +1 -3
  65. package/src/engine.js +52 -21
  66. package/src/engineAudio.js +31 -14
  67. package/src/engineDebug.js +80 -55
  68. package/src/engineDraw.js +60 -31
  69. package/src/engineExport.js +3 -1
  70. package/src/engineMedals.js +10 -2
  71. package/src/engineObject.js +14 -10
  72. package/src/engineParticles.js +59 -46
  73. package/src/engineRelease.js +1 -0
  74. package/src/engineSettings.js +7 -7
  75. package/src/engineTileLayer.js +49 -18
  76. package/src/engineUtilities.js +79 -34
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.10';
36
+ const engineVersion = '1.14.16';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -96,9 +96,14 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
96
96
 
97
97
  const pluginUpdateList = [], pluginRenderList = [];
98
98
 
99
+ /**
100
+ * @callback PluginCallback - Update or render function for a plugin
101
+ * @memberof Engine
102
+ */
103
+
99
104
  /** Add a new update function for a plugin
100
- * @param {Function} [updateFunction]
101
- * @param {Function} [renderFunction]
105
+ * @param {PluginCallback} [updateFunction]
106
+ * @param {PluginCallback} [renderFunction]
102
107
  * @memberof Engine */
103
108
  function engineAddPlugin(updateFunction, renderFunction)
104
109
  {
@@ -111,23 +116,33 @@ function engineAddPlugin(updateFunction, renderFunction)
111
116
  ///////////////////////////////////////////////////////////////////////////////
112
117
  // Main Engine Functions
113
118
 
119
+ /**
120
+ * @callback GameInitCallback - Called after the engine starts, can be async
121
+ * @returns {void|Promise<void>}
122
+ * @memberof Engine
123
+ */
124
+ /**
125
+ * @callback GameCallback - Update or render function for the game
126
+ * @memberof Engine
127
+ */
128
+
114
129
  /** Startup LittleJS engine with your callback functions
115
- * @param {Function|function():Promise} gameInit - Called once after the engine starts up, can be async for loading
116
- * @param {Function} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
117
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
118
- * @param {Function} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
119
- * @param {Function} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
130
+ * @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
131
+ * @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
132
+ * @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
133
+ * @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
134
+ * @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
120
135
  * @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
121
136
  * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
122
137
  * @example
123
138
  * // Basic engine startup
124
139
  * engineInit(
125
- * () => { console.log('Game initialized!'); }, // gameInit
126
- * () => { updateGameLogic(); }, // gameUpdate
127
- * () => { updateUI(); }, // gameUpdatePost
128
- * () => { drawBackground(); }, // gameRender
129
- * () => { drawHUD(); }, // gameRenderPost
130
- * ['tiles.png', 'tilesLevel.png'] // images to load
140
+ * () => { LOG('Game initialized!'); }, // gameInit
141
+ * () => { updateGameLogic(); }, // gameUpdate
142
+ * () => { updateUI(); }, // gameUpdatePost
143
+ * () => { drawBackground(); }, // gameRender
144
+ * () => { drawHUD(); }, // gameRenderPost
145
+ * ['tiles.png', 'tilesLevel.png'] // images to load
131
146
  * );
132
147
  * @memberof Engine */
133
148
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
@@ -173,16 +188,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
173
188
  if (!debugSpeedUp)
174
189
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
175
190
 
191
+ let wasUpdated = false;
176
192
  if (paused)
177
193
  {
194
+ // update everything except the game and objects
195
+ wasUpdated = true;
178
196
  updateCanvas();
197
+ inputUpdate();
198
+ pluginUpdateList.forEach(f=>f());
179
199
 
180
200
  // update object transforms even when paused
181
201
  for (const o of engineObjects)
182
202
  o.parent || o.updateTransforms();
183
203
 
184
- inputUpdate();
185
- pluginUpdateList.forEach(f=>f());
204
+ // do post update
186
205
  debugUpdate();
187
206
  gameUpdatePost();
188
207
  inputUpdatePost();
@@ -199,12 +218,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
199
218
  }
200
219
 
201
220
  // update multiple frames if necessary in case of slow framerate
202
- for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
221
+ for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
203
222
  {
204
223
  // increment frame and update time
205
224
  time = frame++ / frameRate;
206
225
 
207
226
  // update game and objects
227
+ wasUpdated = true;
208
228
  updateCanvas();
209
229
  inputUpdate();
210
230
  gameUpdate();
@@ -215,7 +235,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
215
235
  debugUpdate();
216
236
  gameUpdatePost();
217
237
  inputUpdatePost();
218
-
219
238
  if (debugVideoCaptureIsActive())
220
239
  renderFrame();
221
240
  }
@@ -232,6 +251,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
232
251
  {
233
252
  if (headlessMode) return;
234
253
 
254
+ // canvas must be updated before rendering
255
+ if (!wasUpdated)
256
+ updateCanvas();
257
+
235
258
  // render sort then render while removing destroyed objects
236
259
  enginePreRender();
237
260
  gameRender();
@@ -360,6 +383,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
360
383
  const promises = imageSources.map((src, textureIndex)=>
361
384
  new Promise(resolve =>
362
385
  {
386
+ ASSERT(isString(src), 'imageSources must be an array of strings');
387
+
363
388
  const image = new Image;
364
389
  image.onerror = image.onload = ()=>
365
390
  {
@@ -475,10 +500,16 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
475
500
  return collectedObjects;
476
501
  }
477
502
 
503
+ /**
504
+ * @callback ObjectCallbackFunction - Function that processes an object
505
+ * @param {EngineObject} uiObjects
506
+ * @memberof Engine
507
+ */
508
+
478
509
  /** Triggers a callback for each object within a given area
479
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
480
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
481
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
510
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
511
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
512
+ * @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
482
513
  * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
483
514
  * @memberof Engine */
484
515
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
@@ -688,12 +719,6 @@ function drawEngineSplashScreen(t)
688
719
  * @memberof Debug */
689
720
  const debug = true;
690
721
 
691
- /** True if asserts are enabled
692
- * @type {boolean}
693
- * @default
694
- * @memberof Debug */
695
- const enableAsserts = true;
696
-
697
722
  /** Size to render debug points by default
698
723
  * @type {number}
699
724
  * @default
@@ -724,77 +749,92 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
724
749
  ///////////////////////////////////////////////////////////////////////////////
725
750
  // Debug helper functions
726
751
 
727
- /** Asserts if the expression is false, does not do anything in release builds
752
+ /** Asserts if the expression is false, does nothing in release builds
753
+ * Halts execution if the assert fails and throws an error
728
754
  * @param {boolean} assert
729
755
  * @param {...Object} [output] - error message output
730
756
  * @memberof Debug */
731
757
  function ASSERT(assert, ...output)
732
758
  {
733
- if (enableAsserts)
734
- console.assert(assert, ...output);
759
+ if (assert) return;
760
+ console.assert(assert, ...output)
761
+ throw new Error('Assert failed!'); // halt execution
735
762
  }
736
763
 
764
+ /** Log to console if debug is enabled, does nothing in release builds
765
+ * @param {...Object} [output] - message output
766
+ * @memberof Debug */
767
+ function LOG(...output) { console.log(...output); }
768
+
737
769
  /** Draw a debug rectangle in world space
738
770
  * @param {Vector2} pos
739
771
  * @param {Vector2} [size=Vector2()]
740
- * @param {string} [color]
741
- * @param {number} [time]
742
- * @param {number} [angle]
772
+ * @param {Color|string} [color]
773
+ * @param {number} [time]
774
+ * @param {number} [angle]
743
775
  * @param {boolean} [fill]
744
776
  * @memberof Debug */
745
- function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
777
+ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
746
778
  {
747
779
  if (typeof size === 'number')
748
780
  size = vec2(size); // allow passing in floats
749
- ASSERT(typeof color === 'string', 'pass in css color strings');
750
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
781
+ if (isColor(color))
782
+ color = color.toString();
783
+ pos = pos.copy();
784
+ size = size.copy();
785
+ const timer = new Timer(time);
786
+ debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill});
751
787
  }
752
788
 
753
789
  /** Draw a debug poly in world space
754
790
  * @param {Vector2} pos
755
791
  * @param {Array<Vector2>} points
756
- * @param {string} [color]
757
- * @param {number} [time]
758
- * @param {number} [angle]
792
+ * @param {Color|string} [color]
793
+ * @param {number} [time]
794
+ * @param {number} [angle]
759
795
  * @param {boolean} [fill]
760
796
  * @memberof Debug */
761
- function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
797
+ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
762
798
  {
763
- ASSERT(typeof color === 'string', 'pass in css color strings');
764
- debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
799
+ if (isColor(color))
800
+ color = color.toString();
801
+ pos = pos.copy();
802
+ points = points.map(p=>p.copy());
803
+ const timer = new Timer(time);
804
+ debugPrimitives.push({pos, points, color, timer, angle, fill});
765
805
  }
766
806
 
767
807
  /** Draw a debug circle in world space
768
808
  * @param {Vector2} pos
769
- * @param {number} [size] - diameter
770
- * @param {string} [color]
771
- * @param {number} [time]
809
+ * @param {number} [size] - diameter
810
+ * @param {Color|string} [color]
811
+ * @param {number} [time]
772
812
  * @param {boolean} [fill]
773
813
  * @memberof Debug */
774
- function debugCircle(pos, size=0, color='#fff', time=0, fill=false)
814
+ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
775
815
  {
776
- ASSERT(typeof color === 'string', 'pass in css color strings');
777
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle:0, fill});
816
+ if (isColor(color))
817
+ color = color.toString();
818
+ pos = pos.copy();
819
+ const timer = new Timer(time);
820
+ debugPrimitives.push({pos, size, color, timer, angle:0, fill});
778
821
  }
779
822
 
780
823
  /** Draw a debug point in world space
781
824
  * @param {Vector2} pos
782
- * @param {string} [color]
783
- * @param {number} [time]
784
- * @param {number} [angle]
825
+ * @param {Color|string} [color]
826
+ * @param {number} [time]
827
+ * @param {number} [angle]
785
828
  * @memberof Debug */
786
829
  function debugPoint(pos, color, time, angle)
787
- {
788
- ASSERT(typeof color === 'string', 'pass in css color strings');
789
- debugRect(pos, undefined, color, time, angle);
790
- }
830
+ { debugRect(pos, undefined, color, time, angle); }
791
831
 
792
832
  /** Draw a debug line in world space
793
833
  * @param {Vector2} posA
794
834
  * @param {Vector2} posB
795
- * @param {string} [color]
796
- * @param {number} [width]
797
- * @param {number} [time]
835
+ * @param {Color|string} [color]
836
+ * @param {number} [width]
837
+ * @param {number} [time]
798
838
  * @memberof Debug */
799
839
  function debugLine(posA, posB, color, width=.1, time)
800
840
  {
@@ -808,7 +848,7 @@ function debugLine(posA, posB, color, width=.1, time)
808
848
  * @param {Vector2} sizeA
809
849
  * @param {Vector2} posB
810
850
  * @param {Vector2} sizeB
811
- * @param {string} [color]
851
+ * @param {Color|string} [color]
812
852
  * @memberof Debug */
813
853
  function debugOverlap(posA, sizeA, posB, sizeB, color)
814
854
  {
@@ -824,18 +864,21 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
824
864
  }
825
865
 
826
866
  /** Draw a debug axis aligned bounding box in world space
827
- * @param {string} text
867
+ * @param {string} text
828
868
  * @param {Vector2} pos
829
- * @param {number} [size]
830
- * @param {string} [color]
831
- * @param {number} [time]
832
- * @param {number} [angle]
833
- * @param {string} [font]
869
+ * @param {number} [size]
870
+ * @param {Color|string} [color]
871
+ * @param {number} [time]
872
+ * @param {number} [angle]
873
+ * @param {string} [font]
834
874
  * @memberof Debug */
835
- function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
875
+ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
836
876
  {
837
- ASSERT(typeof color === 'string', 'pass in css color strings');
838
- debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
877
+ if (isColor(color))
878
+ color = color.toString();
879
+ pos = pos.copy();
880
+ const timer = new Timer(time);
881
+ debugPrimitives.push({text, pos, size, color, timer, angle, font});
839
882
  }
840
883
 
841
884
  /** Clear all debug primitives in the list
@@ -886,17 +929,30 @@ function debugSaveDataURL(dataURL, filename)
886
929
  downloadLink.click();
887
930
  }
888
931
 
889
- /** Show error as full page of red text
932
+ /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
933
+ * This is a good function to call at the start of your game to catch all errors
934
+ * In release builds this function has no effect
890
935
  * @memberof Debug */
891
936
  function debugShowErrors()
892
937
  {
893
938
  const showError = (message)=>
894
939
  {
895
940
  // replace entire page with error message
896
- document.body.style.display = '';
897
- document.body.style.backgroundColor = '#111';
898
- document.body.innerHTML = `<pre style=color:#f00;font-size:50px;white-space:pre-wrap>` + message;
941
+ document.body.style = 'background-color:#111;margin:8px';
942
+ document.body.innerHTML = `<pre style=color:#f00;font-size:28px;white-space:pre-wrap>` + message;
899
943
  }
944
+
945
+ const originalAssert = console.assert;
946
+ console.assert = (assertion, ...output)=>
947
+ {
948
+ originalAssert(assertion, ...output);
949
+ if (!assertion)
950
+ {
951
+ const message = output.join(' ');
952
+ const stack = new Error().stack;
953
+ throw 'Assertion failed!\n' + message + '\n' + stack;
954
+ }
955
+ };
900
956
  onunhandledrejection = (event)=>
901
957
  showError(event.reason.stack || event.reason);
902
958
  onerror = (message, source, lineno, colno)=>
@@ -1064,7 +1120,7 @@ function debugRender()
1064
1120
  p.fill && overlayContext.fill();
1065
1121
  overlayContext.stroke();
1066
1122
  }
1067
- else if (p.size === 0 || p.size.x === 0 && p.size.y === 0)
1123
+ else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
1068
1124
  {
1069
1125
  // point
1070
1126
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
@@ -1091,7 +1147,7 @@ function debugRender()
1091
1147
  });
1092
1148
 
1093
1149
  // remove expired primitives
1094
- debugPrimitives = debugPrimitives.filter(r=>r.time<0);
1150
+ debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
1095
1151
  }
1096
1152
 
1097
1153
  if (debugObject)
@@ -1226,7 +1282,7 @@ function debugVideoCaptureStart()
1226
1282
  }
1227
1283
 
1228
1284
  // start recording
1229
- console.log('Video capture started.');
1285
+ LOG('Video capture started.');
1230
1286
  debugVideoCapture.start();
1231
1287
  debugVideoCaptureTimer = new Timer(0);
1232
1288
 
@@ -1253,7 +1309,7 @@ function debugVideoCaptureStop()
1253
1309
  return; // not recording
1254
1310
 
1255
1311
  // stop recording
1256
- console.log(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
1312
+ LOG(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
1257
1313
  debugVideoCapture.stop();
1258
1314
  debugVideoCapture = 0;
1259
1315
  debugVideoCaptureIcon.style.display = 'none';
@@ -1487,7 +1543,12 @@ function wave(frequency=1, amplitude=1, t=time, offset=0)
1487
1543
  * @param {number} t - time in seconds
1488
1544
  * @return {string}
1489
1545
  * @memberof Utilities */
1490
- function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
1546
+ function formatTime(t)
1547
+ {
1548
+ const sign = t < 0 ? '-' : '';
1549
+ t = abs(t)|0;
1550
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1551
+ }
1491
1552
 
1492
1553
  /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1493
1554
  * @param {string} url - URL of JSON file
@@ -1496,6 +1557,8 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
1496
1557
  async function fetchJSON(url)
1497
1558
  {
1498
1559
  const response = await fetch(url);
1560
+ if (!response.ok)
1561
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1499
1562
  return response.json();
1500
1563
  }
1501
1564
 
@@ -1506,6 +1569,13 @@ async function fetchJSON(url)
1506
1569
  * @memberof Utilities */
1507
1570
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1508
1571
 
1572
+ /**
1573
+ * Check if object is a valid string or can be converted to one
1574
+ * @param {any} s
1575
+ * @return {boolean}
1576
+ * @memberof Utilities */
1577
+ function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1578
+
1509
1579
  ///////////////////////////////////////////////////////////////////////////////
1510
1580
 
1511
1581
  /** Random global functions
@@ -1568,6 +1638,7 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1568
1638
  /**
1569
1639
  * Seeded random number generator
1570
1640
  * - Can be used to create a deterministic random number sequence
1641
+ * @memberof Engine
1571
1642
  * @example
1572
1643
  * let r = new RandomGenerator(123); // random number generator with seed 123
1573
1644
  * let a = r.float(); // random value between 0 and 1
@@ -1664,6 +1735,7 @@ function ASSERT_VECTOR2_NORMAL(v)
1664
1735
  /**
1665
1736
  * 2D Vector object with vector math library
1666
1737
  * - Functions do not change this so they can be chained together
1738
+ * @memberof Engine
1667
1739
  * @example
1668
1740
  * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
1669
1741
  * let b = new Vector2; // vector with coordinates (0, 0)
@@ -1804,9 +1876,10 @@ class Vector2
1804
1876
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1805
1877
  }
1806
1878
 
1807
- /** Set the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
1879
+ /** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
1808
1880
  * @param {number} [direction]
1809
- * @param {number} [length] */
1881
+ * @param {number} [length]
1882
+ * @return {Vector2} */
1810
1883
  setDirection(direction, length=1)
1811
1884
  {
1812
1885
  ASSERT_NUMBER_VALID(direction);
@@ -1814,8 +1887,10 @@ class Vector2
1814
1887
  direction = mod(direction, 4);
1815
1888
  ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1816
1889
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1817
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1818
- direction%2 ? 0 : direction ? -length : length);
1890
+
1891
+ this.x = direction%2 ? direction-1 ? -length : length : 0;
1892
+ this.y = direction%2 ? 0 : direction ? -length : length;
1893
+ return this;
1819
1894
  }
1820
1895
 
1821
1896
  /** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
@@ -1823,11 +1898,7 @@ class Vector2
1823
1898
  direction()
1824
1899
  { return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }
1825
1900
 
1826
- /** Returns a copy of this vector that has been inverted
1827
- * @return {Vector2} */
1828
- invert() { return new Vector2(this.y, -this.x); }
1829
-
1830
- /** Returns a copy of this vector absolute values
1901
+ /** Returns a copy of this vector with absolute values
1831
1902
  * @return {Vector2} */
1832
1903
  abs() { return new Vector2(abs(this.x), abs(this.y)); }
1833
1904
 
@@ -1869,13 +1940,10 @@ class Vector2
1869
1940
  toString(digits=3)
1870
1941
  {
1871
1942
  ASSERT_NUMBER_VALID(digits);
1872
- if (debug)
1873
- {
1874
- if (this.isValid())
1875
- return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1876
- else
1877
- return `(${this.x}, ${this.y})`;
1878
- }
1943
+ if (this.isValid())
1944
+ return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1945
+ else
1946
+ return `(${this.x}, ${this.y})`;
1879
1947
  }
1880
1948
 
1881
1949
  /** Checks if this is a valid vector
@@ -1918,6 +1986,7 @@ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
1918
1986
 
1919
1987
  /**
1920
1988
  * Color object (red, green, blue, alpha) with some helpful functions
1989
+ * @memberof Engine
1921
1990
  * @example
1922
1991
  * let a = new Color; // white
1923
1992
  * let b = new Color(1, 0, 0); // red
@@ -2095,7 +2164,8 @@ class Color
2095
2164
  * @return {Color} */
2096
2165
  setHex(hex)
2097
2166
  {
2098
- ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
2167
+ ASSERT(isString(hex), 'Color hex code must be a string');
2168
+ ASSERT(hex[0] === '#', 'Color hex code must start with #');
2099
2169
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
2100
2170
 
2101
2171
  if (hex.length < 6)
@@ -2137,77 +2207,78 @@ class Color
2137
2207
  }
2138
2208
 
2139
2209
  ///////////////////////////////////////////////////////////////////////////////
2140
- // default colors
2210
+ // Default Colors
2141
2211
 
2142
2212
  /** Color - White #ffffff
2143
2213
  * @type {Color}
2144
2214
  * @memberof Utilities */
2145
- const WHITE = rgb();
2215
+ const WHITE = protectEngineConstant(rgb());
2146
2216
 
2147
- /** Color - Clear White #ffffff with 0 alpha
2217
+ /** Color - Clear White #757474ff with 0 alpha
2148
2218
  * @type {Color}
2149
2219
  * @memberof Utilities */
2150
- const CLEAR_WHITE = rgb(1,1,1,0);
2220
+ const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
2151
2221
 
2152
2222
  /** Color - Black #000000
2153
2223
  * @type {Color}
2154
2224
  * @memberof Utilities */
2155
- const BLACK = rgb(0,0,0);
2225
+ const BLACK = protectEngineConstant(rgb(0,0,0));
2156
2226
 
2157
2227
  /** Color - Clear Black #000000 with 0 alpha
2158
2228
  * @type {Color}
2159
2229
  * @memberof Utilities */
2160
- const CLEAR_BLACK = rgb(0,0,0,0);
2230
+ const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
2161
2231
 
2162
2232
  /** Color - Gray #808080
2163
2233
  * @type {Color}
2164
2234
  * @memberof Utilities */
2165
- const GRAY = rgb(.5,.5,.5);
2235
+ const GRAY = protectEngineConstant(rgb(.5,.5,.5));
2166
2236
 
2167
2237
  /** Color - Red #ff0000
2168
2238
  * @type {Color}
2169
2239
  * @memberof Utilities */
2170
- const RED = rgb(1,0,0);
2240
+ const RED = protectEngineConstant(rgb(1,0,0));
2171
2241
 
2172
2242
  /** Color - Orange #ff8000
2173
2243
  * @type {Color}
2174
2244
  * @memberof Utilities */
2175
- const ORANGE = rgb(1,.5,0);
2245
+ const ORANGE = protectEngineConstant(rgb(1,.5,0));
2176
2246
 
2177
2247
  /** Color - Yellow #ffff00
2178
2248
  * @type {Color}
2179
2249
  * @memberof Utilities */
2180
- const YELLOW = rgb(1,1,0);
2250
+ const YELLOW = protectEngineConstant(rgb(1,1,0));
2181
2251
 
2182
2252
  /** Color - Green #00ff00
2183
2253
  * @type {Color}
2184
2254
  * @memberof Utilities */
2185
- const GREEN = rgb(0,1,0);
2255
+ const GREEN = protectEngineConstant(rgb(0,1,0));
2186
2256
 
2187
2257
  /** Color - Cyan #00ffff
2188
2258
  * @type {Color}
2189
2259
  * @memberof Utilities */
2190
- const CYAN = rgb(0,1,1);
2260
+ const CYAN = protectEngineConstant(rgb(0,1,1));
2191
2261
 
2192
2262
  /** Color - Blue #0000ff
2193
2263
  * @type {Color}
2194
2264
  * @memberof Utilities */
2195
- const BLUE = rgb(0,0,1);
2265
+ const BLUE = protectEngineConstant(rgb(0,0,1));
2196
2266
 
2197
2267
  /** Color - Purple #8000ff
2198
2268
  * @type {Color}
2199
2269
  * @memberof Utilities */
2200
- const PURPLE = rgb(.5,0,1);
2270
+ const PURPLE = protectEngineConstant(rgb(.5,0,1));
2201
2271
 
2202
2272
  /** Color - Magenta #ff00ff
2203
2273
  * @type {Color}
2204
2274
  * @memberof Utilities */
2205
- const MAGENTA = rgb(1,0,1);
2275
+ const MAGENTA = protectEngineConstant(rgb(1,0,1));
2206
2276
 
2207
2277
  ///////////////////////////////////////////////////////////////////////////////
2208
2278
 
2209
2279
  /**
2210
2280
  * Timer object tracks how long has passed since it was set
2281
+ * @memberof Engine
2211
2282
  * @example
2212
2283
  * let a = new Timer; // creates a timer that is not set
2213
2284
  * a.set(3); // sets the timer to 3 seconds
@@ -2264,11 +2335,41 @@ class Timer
2264
2335
 
2265
2336
  /** Returns this timer expressed as a string
2266
2337
  * @return {string} */
2267
- toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
2338
+ toString() { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2268
2339
 
2269
2340
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2270
2341
  * @return {number} */
2271
2342
  valueOf() { return this.get(); }
2343
+ }
2344
+
2345
+ ///////////////////////////////////////////////////////////////////////////////
2346
+ // Helper functions used by the engine
2347
+
2348
+ // make color constants immutable with debug assertions
2349
+ function protectEngineConstant(obj)
2350
+ {
2351
+ if (debug)
2352
+ {
2353
+ // get properties and store original values
2354
+ const props = Object.keys(obj), values = {};
2355
+ props.forEach(prop => values[prop] = obj[prop]);
2356
+
2357
+ // replace with getters/setters that assert
2358
+ props.forEach(prop =>
2359
+ {
2360
+ Object.defineProperty(obj, prop, {
2361
+ get: () => values[prop],
2362
+ set: (value) =>
2363
+ {
2364
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
2365
+ },
2366
+ enumerable: true
2367
+ });
2368
+ });
2369
+ }
2370
+
2371
+ // freeze the object to prevent adding new properties
2372
+ return Object.freeze(obj);
2272
2373
  }
2273
2374
  /**
2274
2375
  * LittleJS Engine Settings
@@ -2576,7 +2677,7 @@ let medalsPreventUnlock = false;
2576
2677
  /** Set position of camera in world space
2577
2678
  * @param {Vector2} pos
2578
2679
  * @memberof Settings */
2579
- function setCameraPos(pos) { cameraPos = pos; }
2680
+ function setCameraPos(pos) { cameraPos = pos.copy(); }
2580
2681
 
2581
2682
  /** Set angle of camera in world space
2582
2683
  * @param {number} angle
@@ -2599,17 +2700,17 @@ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2599
2700
  /** Set color to clear the canvas to before render
2600
2701
  * @param {Color} color
2601
2702
  * @memberof Settings */
2602
- function setCanvasClearColor(color) { canvasClearColor = color; }
2703
+ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
2603
2704
 
2604
2705
  /** Set max size of the canvas
2605
2706
  * @param {Vector2} size
2606
2707
  * @memberof Settings */
2607
- function setCanvasMaxSize(size) { canvasMaxSize = size; }
2708
+ function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
2608
2709
 
2609
2710
  /** Set fixed size of the canvas
2610
2711
  * @param {Vector2} size
2611
2712
  * @memberof Settings */
2612
- function setCanvasFixedSize(size) { canvasFixedSize = size; }
2713
+ function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
2613
2714
 
2614
2715
  /** Use nearest scaling algorithm for canvas for more pixelated look
2615
2716
  * - If enabled sets css image-rendering:pixelated
@@ -2674,7 +2775,7 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
2674
2775
  /** Set default size of tiles in pixels
2675
2776
  * @param {Vector2} size
2676
2777
  * @memberof Settings */
2677
- function setTileSizeDefault(size) { tileSizeDefault = size; }
2778
+ function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
2678
2779
 
2679
2780
  /** Set to prevent tile bleeding from neighbors in pixels
2680
2781
  * @param {number} scale
@@ -2719,7 +2820,7 @@ function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
2719
2820
  /** Set how much gravity to apply to objects
2720
2821
  * @param {Vector2} newGravity
2721
2822
  * @memberof Settings */
2722
- function setGravity(newGravity) { gravity = newGravity; }
2823
+ function setGravity(newGravity) { gravity = newGravity.copy(); }
2723
2824
 
2724
2825
  /** Set to scales emit rate of particles
2725
2826
  * @param {number} scale
@@ -2809,7 +2910,7 @@ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
2809
2910
  /** Set size of medal display
2810
2911
  * @param {Vector2} size
2811
2912
  * @memberof Settings */
2812
- function setMedalDisplaySize(size) { medalDisplaySize = size; }
2913
+ function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
2813
2914
 
2814
2915
  /** Set to stop medals from being unlockable
2815
2916
  * @param {boolean} preventUnlock
@@ -2849,6 +2950,7 @@ function setDebugKey(key) { debugKey = key; }
2849
2950
  * - Collision for objects can be set to be solid to block other objects
2850
2951
  * - Objects may get pushed into overlapping other solid objects, if so they will push away
2851
2952
  * - Solid objects are more performance intensive and should be used sparingly
2953
+ * @memberof Engine
2852
2954
  * @example
2853
2955
  * // create an engine object, normally you would first extend the class with your own
2854
2956
  * const pos = vec2(2,3);
@@ -2857,18 +2959,18 @@ function setDebugKey(key) { debugKey = key; }
2857
2959
  class EngineObject
2858
2960
  {
2859
2961
  /** Create an engine object and adds it to the list of objects
2860
- * @param {Vector2} [pos=(0,0)] - World space position of the object
2861
- * @param {Vector2} [size=(1,1)] - World space size of the object
2862
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2863
- * @param {number} [angle] - Angle the object is rotated by
2864
- * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
2865
- * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2962
+ * @param {Vector2} [pos=(0,0)] - World space position of the object
2963
+ * @param {Vector2} [size=(1,1)] - World space size of the object
2964
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2965
+ * @param {number} [angle] - Angle the object is rotated by
2966
+ * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2967
+ * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2866
2968
  */
2867
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
2969
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
2868
2970
  {
2869
2971
  // check passed in params
2870
- ASSERT(isVector2(pos), 'object pos should be a vec2');
2871
- ASSERT(isVector2(size), 'object size should be a vec2');
2972
+ ASSERT(isVector2(pos), 'object pos must be a vec2');
2973
+ ASSERT(isVector2(size), 'object size must be a vec2');
2872
2974
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2873
2975
  ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2874
2976
  ASSERT(isColor(color), 'object color should be a valid rgba color');
@@ -3153,7 +3255,7 @@ class EngineObject
3153
3255
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3154
3256
  }
3155
3257
 
3156
- /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
3258
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
3157
3259
  destroy()
3158
3260
  {
3159
3261
  if (this.destroyed)
@@ -3227,6 +3329,8 @@ class EngineObject
3227
3329
  addChild(child, localPos=vec2(), localAngle=0)
3228
3330
  {
3229
3331
  ASSERT(!child.parent && !this.children.includes(child));
3332
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
3333
+ ASSERT(child !== this, 'cannot add self as child');
3230
3334
  this.children.push(child);
3231
3335
  child.parent = this;
3232
3336
  child.localPos = localPos.copy();
@@ -3238,6 +3342,7 @@ class EngineObject
3238
3342
  removeChild(child)
3239
3343
  {
3240
3344
  ASSERT(child.parent === this && this.children.includes(child));
3345
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
3241
3346
  this.children.splice(this.children.indexOf(child), 1);
3242
3347
  child.parent = 0;
3243
3348
  }
@@ -3376,7 +3481,7 @@ let drawCount;
3376
3481
  * Create a tile info object using a grid based system
3377
3482
  * - This can take vecs or floats for easier use and conversion
3378
3483
  * - If an index is passed in, the tile size and index will determine the position
3379
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
3484
+ * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3380
3485
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3381
3486
  * @param {number} [textureIndex] - Texture index to use
3382
3487
  * @param {number} [padding] - How many pixels padding around tiles
@@ -3421,6 +3526,7 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3421
3526
 
3422
3527
  /**
3423
3528
  * Tile Info - Stores info about how to draw a tile
3529
+ * @memberof Draw
3424
3530
  */
3425
3531
  class TileInfo
3426
3532
  {
@@ -3481,7 +3587,10 @@ class TileInfo
3481
3587
  }
3482
3588
  }
3483
3589
 
3484
- /** Texture Info - Stores info about each texture */
3590
+ /**
3591
+ * Tile Info - Stores info about each texture
3592
+ * @memberof Draw
3593
+ */
3485
3594
  class TextureInfo
3486
3595
  {
3487
3596
  /**
@@ -3527,10 +3636,11 @@ class TextureInfo
3527
3636
  function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3528
3637
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3529
3638
  {
3530
- ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
3531
- ASSERT(isVector2(size), 'drawTile size should be a vec2');
3532
- ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
3533
- ASSERT(isNumber(angle), 'drawTile angle should be a number');
3639
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3640
+ ASSERT(isVector2(size), 'size must be a vec2');
3641
+ ASSERT(isColor(color), 'color is invalid');
3642
+ ASSERT(isNumber(angle), 'angle must be a number');
3643
+ ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
3534
3644
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3535
3645
 
3536
3646
  const textureInfo = tileInfo && tileInfo.textureInfo;
@@ -3625,10 +3735,10 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3625
3735
  * @memberof Draw */
3626
3736
  function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3627
3737
  {
3628
- ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
3629
- ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
3630
- ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
3631
- ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
3738
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3739
+ ASSERT(isVector2(size), 'size must be a vec2');
3740
+ ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
3741
+ ASSERT(isNumber(angle), 'angle must be a number');
3632
3742
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3633
3743
  if (useWebGL)
3634
3744
  {
@@ -3686,11 +3796,11 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3686
3796
  * @memberof Draw */
3687
3797
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3688
3798
  {
3689
- ASSERT(Array.isArray(points), 'drawLineList points should be an array');
3690
- ASSERT(isNumber(width), 'drawLineList width should be a number');
3691
- ASSERT(isColor(color), 'drawLineList color is invalid');
3692
- ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
3693
- ASSERT(isNumber(angle), 'drawLineList angle should be a number');
3799
+ ASSERT(Array.isArray(points), 'points must be an array');
3800
+ ASSERT(isNumber(width), 'width must be a number');
3801
+ ASSERT(isColor(color), 'color is invalid');
3802
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3803
+ ASSERT(isNumber(angle), 'angle must be a number');
3694
3804
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3695
3805
  if (useWebGL)
3696
3806
  {
@@ -3761,8 +3871,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3761
3871
  * @memberof Draw */
3762
3872
  function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3763
3873
  {
3764
- ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
3765
- ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
3874
+ ASSERT(isVector2(size), 'size must be a vec2');
3875
+ ASSERT(isNumber(sides), 'sides must be a number');
3766
3876
 
3767
3877
  // build regular polygon points
3768
3878
  const points = [];
@@ -3788,12 +3898,13 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3788
3898
  * @memberof Draw */
3789
3899
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3790
3900
  {
3791
- ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
3792
- ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3793
- ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3794
- ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3795
- ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3901
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3902
+ ASSERT(Array.isArray(points), 'points must be an array');
3903
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3904
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3905
+ ASSERT(isNumber(angle), 'angle must be a number');
3796
3906
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3907
+
3797
3908
  if (useWebGL)
3798
3909
  {
3799
3910
  let scale = 1;
@@ -3840,13 +3951,14 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3840
3951
  * @memberof Draw */
3841
3952
  function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3842
3953
  {
3843
- ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
3844
- ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
3845
- ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3846
- ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3847
- ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3848
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3954
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3955
+ ASSERT(isVector2(size), 'size must be a vec2');
3956
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3957
+ ASSERT(isNumber(angle), 'angle must be a number');
3958
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3959
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3849
3960
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3961
+
3850
3962
  if (useWebGL)
3851
3963
  {
3852
3964
  // draw as a regular polygon
@@ -3883,21 +3995,32 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3883
3995
  * @memberof Draw */
3884
3996
  function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3885
3997
  {
3886
- ASSERT(isNumber(size), 'drawCircle size should be a number');
3998
+ ASSERT(isNumber(size), 'size must be a number');
3887
3999
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3888
4000
  }
3889
4001
 
4002
+ /**
4003
+ * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
4004
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4005
+ * @memberof Draw
4006
+ */
4007
+
3890
4008
  /** Draw directly to a 2d canvas context in world space
3891
4009
  * @param {Vector2} pos
3892
4010
  * @param {Vector2} size
3893
4011
  * @param {number} angle
3894
4012
  * @param {boolean} [mirror]
3895
- * @param {Function} [drawFunction]
4013
+ * @param {Canvas2DDrawFunction} [drawFunction]
3896
4014
  * @param {boolean} [screenSpace=false]
3897
4015
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3898
4016
  * @memberof Draw */
3899
4017
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
3900
4018
  {
4019
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4020
+ ASSERT(isVector2(size), 'size must be a vec2');
4021
+ ASSERT(isNumber(angle), 'angle must be a number');
4022
+ ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
4023
+
3901
4024
  if (!screenSpace)
3902
4025
  {
3903
4026
  // transform from world space to screen space
@@ -3965,6 +4088,16 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3965
4088
  * @memberof Draw */
3966
4089
  function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3967
4090
  {
4091
+ ASSERT(isString(text), 'text must be a string');
4092
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4093
+ ASSERT(isNumber(size), 'size must be a number');
4094
+ ASSERT(isColor(color), 'color must be a color');
4095
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4096
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
4097
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
4098
+ ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4099
+ ASSERT(isString(font), 'font must be a string');
4100
+
3968
4101
  context.fillStyle = color.toString();
3969
4102
  context.strokeStyle = lineColor.toString();
3970
4103
  context.lineWidth = lineWidth;
@@ -4166,6 +4299,7 @@ let engineFontImage;
4166
4299
  * - 96 characters (from space to tilde) are stored in an image
4167
4300
  * - Uses a default 8x8 font if none is supplied
4168
4301
  * - You can also use fonts from the main tile sheet
4302
+ * @memberof Draw
4169
4303
  * @example
4170
4304
  * // use built in font
4171
4305
  * const font = new FontImage;
@@ -4222,7 +4356,7 @@ class FontImage
4222
4356
  * @param {boolean} [center]
4223
4357
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4224
4358
  */
4225
- drawTextScreen(text, pos, scale=4, center, context=overlayContext)
4359
+ drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
4226
4360
  {
4227
4361
  context.save();
4228
4362
  const size = this.tileSize;
@@ -4916,6 +5050,7 @@ function audioInit()
4916
5050
  * Sound Object - Stores a sound for later use and can be played positionally
4917
5051
  *
4918
5052
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5053
+ * @memberof Audio
4919
5054
  * @example
4920
5055
  * // create a sound
4921
5056
  * const sound_example = new Sound([.5,.5]);
@@ -5010,12 +5145,12 @@ class Sound
5010
5145
 
5011
5146
  /** Play the sound as a musical note with a semitone offset
5012
5147
  * This can be used to play music with chromatic scales
5013
- * @param {number} semitoneOffset - How many semitones to offset pitch
5148
+ * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5014
5149
  * @param {Vector2} [pos] - World space position to play the sound if any
5015
5150
  * @param {number} [volume=1] - How much to scale volume by
5016
5151
  * @return {SoundInstance} - The audio source node
5017
5152
  */
5018
- playNote(semitoneOffset, pos, volume)
5153
+ playNote(semitoneOffset=0, pos, volume)
5019
5154
  {
5020
5155
  const pitch = getNoteFrequency(semitoneOffset, 1);
5021
5156
  return this.play(pos, volume, pitch, 0);
@@ -5038,6 +5173,8 @@ class Sound
5038
5173
  /**
5039
5174
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
5040
5175
  * - this can be used to play wave, mp3, and ogg files
5176
+ * @extends Sound
5177
+ * @memberof Audio
5041
5178
  * @example
5042
5179
  * // create a sound
5043
5180
  * const sound_example = new SoundWave('sound.mp3');
@@ -5047,22 +5184,29 @@ class Sound
5047
5184
  */
5048
5185
  class SoundWave extends Sound
5049
5186
  {
5187
+ /**
5188
+ * @callback SoundLoadCallback - Function called when sound is loaded
5189
+ * @param {SoundWave} sound
5190
+ * @memberof Audio
5191
+ */
5192
+
5050
5193
  /** Create a sound object and cache the wave file for later use
5051
5194
  * @param {string} filename - Filename of audio file to load
5052
5195
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
5053
5196
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5054
5197
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5055
- * @param {Function} [onloadCallback] - callback function to call when sound is loaded
5198
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5056
5199
  */
5057
5200
  constructor(filename, randomness=0, range, taper, onloadCallback)
5058
5201
  {
5059
5202
  super(undefined, range, taper);
5060
5203
  if (!soundEnable || headlessMode) return;
5204
+ ASSERT(!filename || isString(filename), 'filename must be a string');
5061
5205
 
5062
- /** @property {Function} - callback function to call when sound is loaded */
5206
+ /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5063
5207
  this.onloadCallback = onloadCallback;
5064
5208
  this.randomness = randomness;
5065
- this.loadSound(filename);
5209
+ filename && this.loadSound(filename);
5066
5210
  }
5067
5211
 
5068
5212
  /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
@@ -5071,6 +5215,8 @@ class SoundWave extends Sound
5071
5215
  async loadSound(filename)
5072
5216
  {
5073
5217
  const response = await fetch(filename);
5218
+ if (!response.ok)
5219
+ throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
5074
5220
  const arrayBuffer = await response.arrayBuffer();
5075
5221
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
5076
5222
 
@@ -5106,7 +5252,7 @@ class SoundWave extends Sound
5106
5252
  this.sampleChannels = sampleChannels;
5107
5253
  this.loadedPercent = 1;
5108
5254
  if (this.onloadCallback)
5109
- this.onloadCallback();
5255
+ this.onloadCallback(this);
5110
5256
  }
5111
5257
  }
5112
5258
 
@@ -5115,6 +5261,7 @@ class SoundWave extends Sound
5115
5261
  /**
5116
5262
  * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
5117
5263
  * Represents a single playing instance of a sound with pause/resume capabilities
5264
+ * @memberof Audio
5118
5265
  * @example
5119
5266
  * // Play a sound and get an instance for control
5120
5267
  * const jumpSound = new Sound([.5,.5,220]);
@@ -5314,6 +5461,12 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5314
5461
 
5315
5462
  ///////////////////////////////////////////////////////////////////////////////
5316
5463
 
5464
+ /**
5465
+ * @callback AudioEndedCallback - Function called when a sound ends
5466
+ * @param {AudioBufferSourceNode} source
5467
+ * @memberof Audio
5468
+ */
5469
+
5317
5470
  /** Play cached audio samples with given settings
5318
5471
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
5319
5472
  * @param {number} [volume] - How much to scale volume by
@@ -5323,7 +5476,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5323
5476
  * @param {number} [sampleRate=44100] - Sample rate for the sound
5324
5477
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
5325
5478
  * @param {number} [offset] - Offset in seconds to start playback from
5326
- * @param {Function} [onended] - Callback for when the sound ends
5479
+ * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
5327
5480
  * @return {AudioBufferSourceNode} - The audio node of the sound played
5328
5481
  * @memberof Audio */
5329
5482
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
@@ -5355,16 +5508,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
5355
5508
  if (onended)
5356
5509
  source.addEventListener('ended', ()=> onended(source));
5357
5510
 
5511
+ const startOffset = offset * rate;
5358
5512
  if (!audioIsRunning())
5359
5513
  {
5360
- // fix stalled audio, this sound won't be able to play
5361
- audioContext.resume();
5362
- return;
5514
+ // fix stalled audio and start
5515
+ audioContext.resume().then(()=>source.start(0, startOffset));
5363
5516
  }
5364
-
5365
- // play and return sound
5366
- const startOffset = offset * rate;
5367
- source.start(0, startOffset);
5517
+ else
5518
+ source.start(0, startOffset);
5368
5519
  return source;
5369
5520
  }
5370
5521
 
@@ -5529,7 +5680,7 @@ function zzfxG
5529
5680
  * - Unlimited numbers of layers, allocates canvases as needed
5530
5681
  * - Tile layers can be drawn to using their context with canvas2d
5531
5682
  * - Tile layers can also have collision with EngineObjects
5532
- * @namespace TileCollision
5683
+ * @namespace TileLayers
5533
5684
  */
5534
5685
 
5535
5686
  ///////////////////////////////////////////////////////////////////////////////
@@ -5537,13 +5688,13 @@ function zzfxG
5537
5688
 
5538
5689
  /** Keep track of all tile layers with collision
5539
5690
  * @type {Array<TileCollisionLayer>}
5540
- * @memberof TileCollision */
5691
+ * @memberof TileLayers */
5541
5692
  const tileCollisionLayers = [];
5542
5693
 
5543
5694
  /** Get tile collision data for a given cell in the grid
5544
5695
  * @param {Vector2} pos
5545
5696
  * @return {number}
5546
- * @memberof TileCollision */
5697
+ * @memberof TileLayers */
5547
5698
  function tileCollisionGetData(pos)
5548
5699
  {
5549
5700
  // check all tile collision layers
@@ -5559,7 +5710,7 @@ function tileCollisionGetData(pos)
5559
5710
  * @param {EngineObject} [object] - An object or undefined for generic test
5560
5711
  * @param {boolean} [solidOnly] - Only check solid layers if true
5561
5712
  * @return {TileCollisionLayer}
5562
- * @memberof TileCollision */
5713
+ * @memberof TileLayers */
5563
5714
  function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5564
5715
  {
5565
5716
  for (const layer of tileCollisionLayers)
@@ -5577,7 +5728,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5577
5728
  * @param {EngineObject} [object] - An object or undefined for generic test
5578
5729
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5579
5730
  * @return {Vector2}
5580
- * @memberof TileCollision */
5731
+ * @memberof TileLayers */
5581
5732
  function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5582
5733
  {
5583
5734
  for (const layer of tileCollisionLayers)
@@ -5600,8 +5751,8 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5600
5751
  * @param {number} [collisionLayer] - Layer to use for collision if any
5601
5752
  * @param {boolean} [draw] - Should the layer be drawn automatically
5602
5753
  * @return {Array<TileCollisionLayer>}
5603
- * @memberof TileCollision */
5604
- function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5754
+ * @memberof TileLayers */
5755
+ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5605
5756
  {
5606
5757
  if (!tileMapData)
5607
5758
  {
@@ -5631,6 +5782,12 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5631
5782
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
5632
5783
  tileLayers[layerIndex] = tileLayer;
5633
5784
 
5785
+ // apply layer color
5786
+ const layerColor = dataLayer.tintcolor ?
5787
+ new Color().setHex(dataLayer.tintcolor) :
5788
+ dataLayer.color || WHITE;
5789
+ ASSERT(isColor(layerColor), 'layer color is not a color');
5790
+
5634
5791
  for (let x=levelSize.x; x--;)
5635
5792
  for (let y=levelSize.y; y--;)
5636
5793
  {
@@ -5638,7 +5795,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5638
5795
  const data = dataLayer.data[x + y*levelSize.x];
5639
5796
  if (data)
5640
5797
  {
5641
- const layerData = new TileLayerData(data-1);
5798
+ const layerData = new TileLayerData(data-1, 0, false, layerColor);
5642
5799
  tileLayer.setData(pos, layerData);
5643
5800
 
5644
5801
  // set collision for top layer
@@ -5655,6 +5812,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5655
5812
  ///////////////////////////////////////////////////////////////////////////////
5656
5813
  /**
5657
5814
  * Tile layer data object stores info about how to draw a tile
5815
+ * @memberof TileLayers
5658
5816
  * @example
5659
5817
  * // create tile layer data with tile index 0 and random orientation and color
5660
5818
  * const tileIndex = 0;
@@ -5692,6 +5850,7 @@ class TileLayerData
5692
5850
  * - Contains an offscreen canvas that can be rendered to
5693
5851
  * - WebGL rendering is optional, call useWebGL to enable
5694
5852
  * @extends EngineObject
5853
+ * @memberof TileLayers
5695
5854
  * @example
5696
5855
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
5697
5856
  */
@@ -5706,6 +5865,7 @@ class CanvasLayer extends EngineObject
5706
5865
  */
5707
5866
  constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5708
5867
  {
5868
+ ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5709
5869
  super(position, size, undefined, angle, WHITE, renderOrder);
5710
5870
 
5711
5871
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
@@ -5753,12 +5913,18 @@ class CanvasLayer extends EngineObject
5753
5913
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5754
5914
  }
5755
5915
 
5916
+ /**
5917
+ * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
5918
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
5919
+ * @memberof TileLayers
5920
+ */
5921
+
5756
5922
  /** Draw onto the layer canvas in world space (bypass WebGL)
5757
5923
  * @param {Vector2} pos
5758
5924
  * @param {Vector2} size
5759
5925
  * @param {number} angle
5760
5926
  * @param {boolean} mirror
5761
- * @param {Function} drawFunction */
5927
+ * @param {Canvas2DDrawCallback} drawFunction */
5762
5928
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5763
5929
  {
5764
5930
  const context = this.context;
@@ -5795,7 +5961,7 @@ class CanvasLayer extends EngineObject
5795
5961
  else
5796
5962
  {
5797
5963
  // untextured
5798
- context.fillStyle = color;
5964
+ context.fillStyle = color.toString();
5799
5965
  context.fillRect(-.5, -.5, 1, 1);
5800
5966
  }
5801
5967
  });
@@ -5832,7 +5998,9 @@ class CanvasLayer extends EngineObject
5832
5998
  * - To allow dynamic modifications, layers are rendered using canvas 2d
5833
5999
  * - Some devices like mobile phones are limited to 4k texture resolution
5834
6000
  * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
6001
+ * - Tile layers are centered on their corner, so normal levels are at (0,0)
5835
6002
  * @extends CanvasLayer
6003
+ * @memberof TileLayers
5836
6004
  * @example
5837
6005
  * const tileLayer = new TileLayer(vec2(), vec2(200,100));
5838
6006
  */
@@ -5842,15 +6010,14 @@ class TileLayer extends CanvasLayer
5842
6010
  * @param {Vector2} position - World space position
5843
6011
  * @param {Vector2} size - World space size
5844
6012
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
5845
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
5846
6013
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5847
6014
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5848
6015
  */
5849
- constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0, useWebGL=glEnable)
6016
+ constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5850
6017
  {
5851
6018
  super(position, size, 0, renderOrder, size);
6019
+
5852
6020
  this.tileInfo = tileInfo;
5853
-
5854
6021
  const canvasSize = size.multiply(tileInfo.size);
5855
6022
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
5856
6023
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
@@ -5887,6 +6054,8 @@ class TileLayer extends CanvasLayer
5887
6054
  * @param {boolean} [redraw] - Force the tile to redraw if true */
5888
6055
  setData(layerPos, data, redraw=false)
5889
6056
  {
6057
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6058
+ ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
5890
6059
  if (layerPos.arrayCheck(this.size))
5891
6060
  {
5892
6061
  this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
@@ -5898,7 +6067,10 @@ class TileLayer extends CanvasLayer
5898
6067
  * @param {Vector2} layerPos - Local position in array
5899
6068
  * @return {TileLayerData} */
5900
6069
  getData(layerPos)
5901
- { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
6070
+ {
6071
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6072
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6073
+ }
5902
6074
 
5903
6075
  // Render the tile layer, called automatically by the engine
5904
6076
  render()
@@ -5907,9 +6079,10 @@ class TileLayer extends CanvasLayer
5907
6079
 
5908
6080
  // draw the tile layer as a single tile
5909
6081
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5910
- const pos = this.pos.add(this.size.scale(.5));
6082
+ const size = this.drawSize || this.size;
6083
+ const pos = this.pos.add(size.scale(.5));
5911
6084
  const useWebGL = glEnable && this.glTexture !== undefined;
5912
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6085
+ drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5913
6086
  }
5914
6087
 
5915
6088
  /** Draw all the tile data to an offscreen canvas
@@ -6001,6 +6174,7 @@ class TileLayer extends CanvasLayer
6001
6174
  * - there can be multiple tile collision layers
6002
6175
  * - tile collision layers should not overlap each other
6003
6176
  * @extends TileLayer
6177
+ * @memberof TileLayers
6004
6178
  */
6005
6179
  class TileCollisionLayer extends TileLayer
6006
6180
  {
@@ -6013,8 +6187,7 @@ class TileCollisionLayer extends TileLayer
6013
6187
  */
6014
6188
  constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
6015
6189
  {
6016
- const scale = vec2(1); // collision layers are not scaled
6017
- super(position, size.floor(), tileInfo, scale, renderOrder, useWebGL);
6190
+ super(position, size.floor(), tileInfo, renderOrder, useWebGL);
6018
6191
 
6019
6192
  /** @property {Array<number>} - The tile collision grid */
6020
6193
  this.collisionData = [];
@@ -6044,6 +6217,7 @@ class TileCollisionLayer extends TileLayer
6044
6217
  * @param {Vector2} size - width and height of tile collision 2d grid */
6045
6218
  initCollision(size)
6046
6219
  {
6220
+ ASSERT(isVector2(size), 'size must be a Vector2');
6047
6221
  this.size = size.floor();
6048
6222
  this.collisionData = [];
6049
6223
  this.collisionData.length = size.area();
@@ -6055,6 +6229,7 @@ class TileCollisionLayer extends TileLayer
6055
6229
  * @param {number} [data] */
6056
6230
  setCollisionData(gridPos, data=1)
6057
6231
  {
6232
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6058
6233
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6059
6234
  gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6060
6235
  }
@@ -6064,6 +6239,7 @@ class TileCollisionLayer extends TileLayer
6064
6239
  * @return {number} */
6065
6240
  getCollisionData(gridPos)
6066
6241
  {
6242
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6067
6243
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6068
6244
  return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6069
6245
  }
@@ -6075,6 +6251,9 @@ class TileCollisionLayer extends TileLayer
6075
6251
  * @return {boolean} */
6076
6252
  collisionTest(pos, size=new Vector2, object)
6077
6253
  {
6254
+ ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
6255
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6256
+
6078
6257
  // transform to local layer space
6079
6258
  const posX = pos.x - this.pos.x;
6080
6259
  const posY = pos.y - this.pos.y;
@@ -6105,6 +6284,9 @@ class TileCollisionLayer extends TileLayer
6105
6284
  * @return {Vector2} */
6106
6285
  collisionRaycast(posStart, posEnd, object)
6107
6286
  {
6287
+ ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6288
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6289
+
6108
6290
  // transform to local layer space
6109
6291
  const posStartX = posStart.x - this.pos.x;
6110
6292
  const posStartY = posStart.y - this.pos.y;
@@ -6154,9 +6336,16 @@ class TileCollisionLayer extends TileLayer
6154
6336
  * LittleJS Particle System
6155
6337
  */
6156
6338
 
6339
+ /**
6340
+ * @callback ParticleCallbackFunction - Function that processes a particle
6341
+ * @param {Particle} particle
6342
+ * @memberof Engine
6343
+ */
6344
+
6157
6345
  /**
6158
6346
  * Particle Emitter - Spawns particles with the given settings
6159
6347
  * @extends EngineObject
6348
+ * @memberof Engine
6160
6349
  * @example
6161
6350
  * // create a particle emitter
6162
6351
  * let pos = vec2(2,3);
@@ -6181,10 +6370,10 @@ class ParticleEmitter extends EngineObject
6181
6370
  * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
6182
6371
  * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
6183
6372
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
6184
- * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
6185
- * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
6186
- * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
6187
- * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
6373
+ * @param {Color} [colorStartA=WHITE] - Color at start of life 1, randomized between start colors
6374
+ * @param {Color} [colorStartB=WHITE] - Color at start of life 2, randomized between start colors
6375
+ * @param {Color} [colorEndA=CLEAR_WHITE] - Color at end of life 1, randomized between end colors
6376
+ * @param {Color} [colorEndB=CLEAR_WHITE] - Color at end of life 2, randomized between end colors
6188
6377
  * @param {number} [particleTime] - How long particles live
6189
6378
  * @param {number} [sizeStart] - How big are particles at start
6190
6379
  * @param {number} [sizeEnd] - How big are particles at end
@@ -6211,10 +6400,10 @@ class ParticleEmitter extends EngineObject
6211
6400
  emitRate = 100,
6212
6401
  emitConeAngle = PI,
6213
6402
  tileInfo,
6214
- colorStartA = new Color,
6215
- colorStartB = new Color,
6216
- colorEndA = new Color(1,1,1,0),
6217
- colorEndB = new Color(1,1,1,0),
6403
+ colorStartA = WHITE,
6404
+ colorStartB = WHITE,
6405
+ colorEndA = CLEAR_WHITE,
6406
+ colorEndB = CLEAR_WHITE,
6218
6407
  particleTime = .5,
6219
6408
  sizeStart = .1,
6220
6409
  sizeEnd = 1,
@@ -6237,7 +6426,8 @@ class ParticleEmitter extends EngineObject
6237
6426
 
6238
6427
  // emitter settings
6239
6428
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
6240
- this.emitSize = emitSize
6429
+ this.emitSize = emitSize instanceof Vector2 ?
6430
+ emitSize.copy() : emitSize;
6241
6431
  /** @property {number} - How long to stay alive (0 is forever) */
6242
6432
  this.emitTime = emitTime;
6243
6433
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -6247,13 +6437,13 @@ class ParticleEmitter extends EngineObject
6247
6437
 
6248
6438
  // color settings
6249
6439
  /** @property {Color} - Color at start of life 1, randomized between start colors */
6250
- this.colorStartA = colorStartA;
6440
+ this.colorStartA = colorStartA.copy();
6251
6441
  /** @property {Color} - Color at start of life 2, randomized between start colors */
6252
- this.colorStartB = colorStartB;
6442
+ this.colorStartB = colorStartB.copy();
6253
6443
  /** @property {Color} - Color at end of life 1, randomized between end colors */
6254
- this.colorEndA = colorEndA;
6444
+ this.colorEndA = colorEndA.copy();
6255
6445
  /** @property {Color} - Color at end of life 2, randomized between end colors */
6256
- this.colorEndB = colorEndB;
6446
+ this.colorEndB = colorEndB.copy();
6257
6447
  /** @property {boolean} - Should color be randomized linearly or across each component */
6258
6448
  this.randomColorLinear = randomColorLinear;
6259
6449
 
@@ -6288,9 +6478,9 @@ class ParticleEmitter extends EngineObject
6288
6478
  this.localSpace = localSpace;
6289
6479
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
6290
6480
  this.trailScale = 0;
6291
- /** @property {Function} - Callback when particle is destroyed */
6481
+ /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
6292
6482
  this.particleDestroyCallback = undefined;
6293
- /** @property {Function} - Callback when particle is created */
6483
+ /** @property {ParticleCallbackFunction} - Callback when particle is created */
6294
6484
  this.particleCreateCallback = undefined;
6295
6485
  /** @property {number} - Track particle emit time */
6296
6486
  this.emitTimeBuffer = 0;
@@ -6391,6 +6581,7 @@ class ParticleEmitter extends EngineObject
6391
6581
  /**
6392
6582
  * Particle Object - Created automatically by Particle Emitters
6393
6583
  * @extends EngineObject
6584
+ * @memberof Engine
6394
6585
  */
6395
6586
  class Particle extends EngineObject
6396
6587
  {
@@ -6409,7 +6600,7 @@ class Particle extends EngineObject
6409
6600
  * @param {boolean} additive - Does it use additive blend mode
6410
6601
  * @param {number} trailScale - If a trail, how long to make it
6411
6602
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
6412
- * @param {Function} [destroyCallback] - Callback when particle dies
6603
+ * @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
6413
6604
  */
6414
6605
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
6415
6606
  )
@@ -6418,14 +6609,14 @@ class Particle extends EngineObject
6418
6609
 
6419
6610
  /** @property {Color} - Color at start of life */
6420
6611
  this.colorStart = colorStart;
6421
- /** @property {Color} - Calculated change in color */
6422
- this.colorEndDelta = colorEnd.subtract(colorStart);
6612
+ /** @property {Color} - Color at end of life */
6613
+ this.colorEnd = colorEnd;
6423
6614
  /** @property {number} - How long to live for */
6424
6615
  this.lifeTime = lifeTime;
6425
6616
  /** @property {number} - Size at start of life */
6426
6617
  this.sizeStart = sizeStart;
6427
- /** @property {number} - Calculated change in size */
6428
- this.sizeEndDelta = sizeEnd - sizeStart;
6618
+ /** @property {number} - Size at end of life */
6619
+ this.sizeEnd = sizeEnd;
6429
6620
  /** @property {number} - How quick to fade in/out */
6430
6621
  this.fadeRate = fadeRate;
6431
6622
  /** @property {boolean} - Is it additive */
@@ -6434,7 +6625,7 @@ class Particle extends EngineObject
6434
6625
  this.trailScale = trailScale;
6435
6626
  /** @property {ParticleEmitter} - Parent emitter if local space */
6436
6627
  this.localSpaceEmitter = localSpaceEmitter;
6437
- /** @property {Function} - Called when particle dies */
6628
+ /** @property {ParticleCallbackFunction} - Called when particle dies */
6438
6629
  this.destroyCallback = destroyCallback;
6439
6630
  // particles do not clamp speed by default
6440
6631
  this.clampSpeed = false;
@@ -6461,54 +6652,58 @@ class Particle extends EngineObject
6461
6652
  /** Render the particle, automatically called each frame, sorted by renderOrder */
6462
6653
  render()
6463
6654
  {
6464
- // modulate size and color
6465
- const p = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1;
6466
- const radius = this.sizeStart + p * this.sizeEndDelta;
6467
- const size = vec2(radius);
6655
+ // lerp color and size
6656
+ const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6657
+ const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6658
+ this.size.x = this.size.y = radius;
6659
+ this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6660
+ this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6661
+ this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
6662
+ this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
6663
+
6664
+ // fade alpha
6468
6665
  const fadeRate = this.fadeRate/2;
6469
- const color = new Color(
6470
- this.colorStart.r + p * this.colorEndDelta.r,
6471
- this.colorStart.g + p * this.colorEndDelta.g,
6472
- this.colorStart.b + p * this.colorEndDelta.b,
6473
- (this.colorStart.a + p * this.colorEndDelta.a) *
6474
- (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
6666
+ this.color.a *= p1 < fadeRate ? p1/fadeRate :
6667
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
6475
6668
 
6476
6669
  // draw the particle
6477
6670
  this.additive && setBlendMode(true);
6478
6671
 
6672
+ // update the position and angle for drawing
6479
6673
  let pos = this.pos, angle = this.angle;
6480
6674
  if (this.localSpaceEmitter)
6481
6675
  {
6482
6676
  // in local space of emitter
6483
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6677
+ const a = this.localSpaceEmitter.angle;
6678
+ const c = Math.cos(a), s = Math.sin(a);
6679
+ pos = this.localSpaceEmitter.pos.add(
6680
+ new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6484
6681
  angle += this.localSpaceEmitter.angle;
6485
6682
  }
6486
6683
  if (this.trailScale)
6487
6684
  {
6488
6685
  // trail style particles
6489
- let velocity = this.velocity;
6490
- if (this.localSpaceEmitter)
6491
- velocity = velocity.rotate(-this.localSpaceEmitter.angle);
6492
- const speed = velocity.length();
6686
+ const direction = this.localSpaceEmitter ?
6687
+ this.velocity.rotate(-this.localSpaceEmitter.angle) :
6688
+ this.velocity;
6689
+ const speed = direction.length();
6493
6690
  if (speed)
6494
6691
  {
6495
- const direction = velocity.scale(1/speed);
6692
+ // stretch in direction of motion
6496
6693
  const trailLength = speed * this.trailScale;
6497
- size.y = max(size.x, trailLength);
6498
- angle = direction.angle();
6499
- drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileInfo, color, angle, this.mirror);
6694
+ this.size.y = max(this.size.x, trailLength);
6695
+ angle = Math.atan2(direction.x, direction.y);
6696
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6500
6697
  }
6501
6698
  }
6502
6699
  else
6503
- drawTile(pos, size, this.tileInfo, color, angle, this.mirror);
6700
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6504
6701
  this.additive && setBlendMode();
6505
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
6702
+ debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6506
6703
 
6507
- if (p === 1)
6704
+ if (p1 === 1)
6508
6705
  {
6509
- // destroy particle when it's time runs out
6510
- this.color = color;
6511
- this.size = size;
6706
+ // destroy particle when its time runs out
6512
6707
  this.destroyCallback && this.destroyCallback(this);
6513
6708
  this.destroyed = 1;
6514
6709
  }
@@ -6573,8 +6768,14 @@ function medalsInit(saveName)
6573
6768
  }
6574
6769
  }
6575
6770
 
6771
+ /**
6772
+ * @callback MedalCallbackFunction - Function that processes a medal
6773
+ * @param {Medal} medal
6774
+ * @memberof Medals
6775
+ */
6776
+
6576
6777
  /** Calls a function for each medal
6577
- * @param {Function} callback
6778
+ * @param {MedalCallbackFunction} callback
6578
6779
  * @memberof Medals */
6579
6780
  function medalsForEach(callback)
6580
6781
  { Object.values(medals).forEach(medal=>callback(medal)); }
@@ -6583,6 +6784,7 @@ function medalsForEach(callback)
6583
6784
 
6584
6785
  /**
6585
6786
  * Medal - Tracks an unlockable medal
6787
+ * @memberof Medals
6586
6788
  * @example
6587
6789
  * // create a medal
6588
6790
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
@@ -6651,11 +6853,12 @@ class Medal
6651
6853
  const height = medalDisplaySize.y;
6652
6854
  const x = overlayCanvas.width - width;
6653
6855
  const y = -height*hidePercent;
6856
+ const backgroundColor = hsl(0,0,.9);
6654
6857
 
6655
6858
  // draw containing rect and clip to that region
6656
6859
  context.save();
6657
6860
  context.beginPath();
6658
- context.fillStyle = new Color(.9,.9,.9).toString();
6861
+ context.fillStyle = backgroundColor.toString();
6659
6862
  context.strokeStyle = BLACK.toString();
6660
6863
  context.lineWidth = 3;
6661
6864
  context.rect(x, y, width, height);
@@ -7457,24 +7660,25 @@ function glPolyStrip(points)
7457
7660
  return strip;
7458
7661
  }
7459
7662
  /**
7460
- * LittleJS Newgrounds API
7663
+ * LittleJS Newgrounds Plugin
7461
7664
  * - NewgroundsMedal extends Medal with Newgrounds API functionality
7462
- * - Call new NewgroundsPlugin() to setup Newgrounds
7665
+ * - Call new NewgroundsPlugin(app_id) to setup Newgrounds
7463
7666
  * - Uses CryptoJS for encryption if optional cipher is provided
7667
+ * - provides functions to interact with medals scoreboards
7464
7668
  * - Keeps connection alive and logs views
7465
- * - Functions to interact with scoreboards
7466
- * - Functions to unlock medals
7669
+ * @namespace Newgrounds
7467
7670
  */
7468
7671
 
7469
7672
  /** Global Newgrounds object
7470
7673
  * @type {NewgroundsPlugin}
7471
- * @memberof Medal */
7674
+ * @memberof Newgrounds */
7472
7675
  let newgrounds;
7473
7676
 
7474
7677
  ///////////////////////////////////////////////////////////////////////////////
7475
7678
  /**
7476
7679
  * Newgrounds medal auto unlocks in newgrounds API
7477
7680
  * @extends Medal
7681
+ * @memberof Newgrounds
7478
7682
  */
7479
7683
  class NewgroundsMedal extends Medal
7480
7684
  {
@@ -7499,6 +7703,7 @@ class NewgroundsMedal extends Medal
7499
7703
  ///////////////////////////////////////////////////////////////////////////////
7500
7704
  /**
7501
7705
  * Newgrounds API object
7706
+ * @memberof Newgrounds
7502
7707
  */
7503
7708
  class NewgroundsPlugin
7504
7709
  {
@@ -7532,7 +7737,7 @@ class NewgroundsPlugin
7532
7737
  // get medals
7533
7738
  const medalsResult = this.call('Medal.getList');
7534
7739
  this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
7535
- debugMedals && console.log(this.medals);
7740
+ debugMedals && LOG(this.medals);
7536
7741
  for (const newgroundsMedal of this.medals)
7537
7742
  {
7538
7743
  const medal = medals[newgroundsMedal['id']];
@@ -7555,7 +7760,7 @@ class NewgroundsPlugin
7555
7760
  // get scoreboards
7556
7761
  const scoreboardResult = this.call('ScoreBoard.getBoards');
7557
7762
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
7558
- debugMedals && console.log(this.scoreboards);
7763
+ debugMedals && LOG(this.scoreboards);
7559
7764
 
7560
7765
  // keep the session alive with a ping every minute
7561
7766
  const keepAliveMS = 60 * 1e3;
@@ -7624,29 +7829,32 @@ class NewgroundsPlugin
7624
7829
  try { xmlHttp.send(formData); }
7625
7830
  catch(e)
7626
7831
  {
7627
- debugMedals && console.log('newgrounds call failed', e);
7832
+ debugMedals && LOG('newgrounds call failed', e);
7628
7833
  return;
7629
7834
  }
7630
- debugMedals && console.log(xmlHttp.responseText);
7835
+ debugMedals && LOG(xmlHttp.responseText);
7631
7836
  return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
7632
7837
  }
7633
7838
  }
7634
7839
  /**
7635
7840
  * LittleJS Post Processing Plugin
7636
7841
  * - Supports shadertoy style post processing shaders
7637
- * - call new new PostProcessPlugin() to setup post processing
7842
+ * - call new PostProcessPlugin() to setup post processing
7638
7843
  * - can be enabled to pass other canvases through a final shader
7844
+ * @namespace PostProcess
7639
7845
  */
7640
7846
 
7641
7847
  ///////////////////////////////////////////////////////////////////////////////
7642
7848
 
7643
7849
  /** Global Post Process plugin object
7644
- * @type {PostProcessPlugin} */
7850
+ * @type {PostProcessPlugin}
7851
+ * @memberof PostProcess */
7645
7852
  let postProcess;
7646
7853
 
7647
7854
  /////////////////////////////////////////////////////////////////////////
7648
7855
  /**
7649
7856
  * UI System Global Object
7857
+ * @memberof PostProcess
7650
7858
  */
7651
7859
  class PostProcessPlugin
7652
7860
  {
@@ -7754,12 +7962,15 @@ class PostProcessPlugin
7754
7962
  }
7755
7963
  /**
7756
7964
  * LittleJS ZzFXM Plugin
7965
+ * @namespace ZzFXM
7757
7966
  */
7758
7967
 
7759
7968
  /**
7760
7969
  * Music Object - Stores a zzfx music track for later use
7761
7970
  *
7762
7971
  * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
7972
+ * @extends Sound
7973
+ * @memberof ZzFXM
7763
7974
  * @example
7764
7975
  * // create some music
7765
7976
  * const music_example = new Music(
@@ -7818,7 +8029,8 @@ class ZzFXMusic extends Sound
7818
8029
  * @param {Array} patterns - Array of pattern data
7819
8030
  * @param {Array} sequence - Array of pattern indexes
7820
8031
  * @param {number} [BPM] - Playback speed of the song in BPM
7821
- * @return {Array} - Left and right channel sample data */
8032
+ * @return {Array} - Left and right channel sample data
8033
+ * @memberof ZzFXM */
7822
8034
  function zzfxM(instruments, patterns, sequence, BPM = 125)
7823
8035
  {
7824
8036
  let i, j, k;
@@ -7921,17 +8133,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
7921
8133
  * - Buttons
7922
8134
  * - Checkboxes
7923
8135
  * - Images
8136
+ * @namespace UISystem
7924
8137
  */
7925
8138
 
7926
8139
  ///////////////////////////////////////////////////////////////////////////////
7927
8140
 
7928
8141
  /** Global UI system plugin object
7929
- * @type {UISystemPlugin} */
8142
+ * @type {UISystemPlugin}
8143
+ * @memberof UISystem */
7930
8144
  let uiSystem;
7931
8145
 
7932
8146
  ///////////////////////////////////////////////////////////////////////////////
7933
8147
  /**
7934
8148
  * UI System Global Object
8149
+ * @memberof UISystem
7935
8150
  */
7936
8151
  class UISystemPlugin
7937
8152
  {
@@ -7976,10 +8191,12 @@ class UISystemPlugin
7976
8191
  this.uiObjects = [];
7977
8192
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
7978
8193
  this.uiContext = context;
7979
- /** @property {UIObject} - Top most object user is over */
7980
- this.hoverObject = undefined;
7981
8194
  /** @property {UIObject} - Object user is currently interacting with */
7982
8195
  this.activeObject = undefined;
8196
+ /** @property {UIObject} - Top most object user is over */
8197
+ this.hoverObject = undefined;
8198
+ /** @property {UIObject} - Hover object at start of update */
8199
+ this.lastHoverObject = undefined;
7983
8200
 
7984
8201
  engineAddPlugin(uiUpdate, uiRender);
7985
8202
 
@@ -8010,6 +8227,7 @@ class UISystemPlugin
8010
8227
  updateInvisibleObject(o);
8011
8228
  }
8012
8229
  // reset hover object at start of update
8230
+ uiSystem.lastHoverObject = uiSystem.hoverObject;
8013
8231
  uiSystem.hoverObject = undefined;
8014
8232
  for (let i = uiSystem.uiObjects.length; i--;)
8015
8233
  {
@@ -8042,6 +8260,13 @@ class UISystemPlugin
8042
8260
  * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
8043
8261
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
8044
8262
  {
8263
+ ASSERT(isVector2(pos), 'pos must be a vec2');
8264
+ ASSERT(isVector2(size), 'size must be a vec2');
8265
+ ASSERT(isColor(color), 'color must be a color');
8266
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
8267
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
8268
+ ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
8269
+
8045
8270
  const context = uiSystem.uiContext;
8046
8271
  context.fillStyle = color.toString();
8047
8272
  context.beginPath();
@@ -8065,6 +8290,11 @@ class UISystemPlugin
8065
8290
  * @param {Color} [lineColor=uiSystem.defaultLineColor] */
8066
8291
  drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
8067
8292
  {
8293
+ ASSERT(isVector2(posA), 'posA must be a vec2');
8294
+ ASSERT(isVector2(posB), 'posB must be a vec2');
8295
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
8296
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
8297
+
8068
8298
  const context = uiSystem.uiContext;
8069
8299
  context.strokeStyle = lineColor.toString();
8070
8300
  context.lineWidth = lineWidth;
@@ -8100,12 +8330,37 @@ class UISystemPlugin
8100
8330
  {
8101
8331
  drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8102
8332
  }
8333
+
8334
+ /**
8335
+ * @callback DragAndDropCallback - Callback for drag and drop events
8336
+ * @param {DragEvent} event - The drag event
8337
+ * @memberof UISystem
8338
+ */
8339
+
8340
+ /** Setup drag and drop event handlers
8341
+ * Automatically prevents defaults and calls the given functions
8342
+ * @param {DragAndDropCallback} [onDrop] - when a file is dropped
8343
+ * @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
8344
+ * @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
8345
+ * @param {DragAndDropCallback} [onDragOver] - continously when dragging over */
8346
+ setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
8347
+ {
8348
+ function setCallback(callback, listenerType)
8349
+ {
8350
+ function listener(e) { e.preventDefault(); callback && callback(e); }
8351
+ document.addEventListener(listenerType, listener);
8352
+ }
8353
+ setCallback(onDrop, 'drop');
8354
+ setCallback(onDragEnter, 'dragenter');
8355
+ setCallback(onDragLeave, 'dragleave');
8356
+ setCallback(onDragOver, 'dragover');
8357
+ }
8103
8358
  }
8104
8359
 
8105
8360
  ///////////////////////////////////////////////////////////////////////////////
8106
8361
  /**
8107
8362
  * UI Object - Base level object for all UI elements
8108
- */
8363
+ * @memberof UISystem */
8109
8364
  class UIObject
8110
8365
  {
8111
8366
  /** Create a UIObject
@@ -8114,6 +8369,9 @@ class UIObject
8114
8369
  */
8115
8370
  constructor(pos=vec2(), size=vec2())
8116
8371
  {
8372
+ ASSERT(isVector2(pos), 'ui object pos must be a vec2');
8373
+ ASSERT(isVector2(size), 'ui object size must be a vec2');
8374
+
8117
8375
  /** @property {Vector2} - Local position of the object */
8118
8376
  this.localPos = pos.copy();
8119
8377
  /** @property {Vector2} - Screen space position of the object */
@@ -8121,21 +8379,21 @@ class UIObject
8121
8379
  /** @property {Vector2} - Screen space size of the object */
8122
8380
  this.size = size.copy();
8123
8381
  /** @property {Color} - Color of the object */
8124
- this.color = uiSystem.defaultColor;
8382
+ this.color = uiSystem.defaultColor.copy();
8125
8383
  /** @property {Color} - Color of the object when active, uses color if undefined */
8126
8384
  this.activeColor = undefined;
8127
8385
  /** @property {string} - Text for this ui object */
8128
8386
  this.text = undefined;
8129
8387
  /** @property {Color} - Color when disabled */
8130
- this.disabledColor = uiSystem.defaultDisabledColor;
8388
+ this.disabledColor = uiSystem.defaultDisabledColor.copy();
8131
8389
  /** @property {boolean} - Is this object disabled? */
8132
8390
  this.disabled = false;
8133
8391
  /** @property {Color} - Color for text */
8134
- this.textColor = uiSystem.defaultTextColor;
8392
+ this.textColor = uiSystem.defaultTextColor.copy()
8135
8393
  /** @property {Color} - Color used when hovering over the object */
8136
- this.hoverColor = uiSystem.defaultHoverColor;
8394
+ this.hoverColor = uiSystem.defaultHoverColor.copy()
8137
8395
  /** @property {Color} - Color for line drawing */
8138
- this.lineColor = uiSystem.defaultLineColor;
8396
+ this.lineColor = uiSystem.defaultLineColor.copy()
8139
8397
  /** @property {number} - Width for line drawing */
8140
8398
  this.lineWidth = uiSystem.defaultLineWidth;
8141
8399
  /** @property {number} - Corner radius for rounded rects */
@@ -8192,7 +8450,7 @@ class UIObject
8192
8450
  /** Update the object, called automatically by plugin once each frame */
8193
8451
  update()
8194
8452
  {
8195
- const wasHover = this.isHoverObject();
8453
+ const wasHover = uiSystem.lastHoverObject === this;
8196
8454
  const isActive = this.isActiveObject();
8197
8455
  const mouseDown = mouseIsDown(0);
8198
8456
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
@@ -8205,15 +8463,14 @@ class UIObject
8205
8463
  }
8206
8464
  if (this.isHoverObject())
8207
8465
  {
8208
- if (mousePress)
8209
- inputClearKey(0,0,0,1,0); // clear mouse was pressed state
8210
8466
  if (!this.disabled)
8211
8467
  {
8212
8468
  if (mousePress)
8213
8469
  {
8214
8470
  if (this.interactive)
8215
8471
  {
8216
- this.onPress();
8472
+ if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
8473
+ this.onPress();
8217
8474
  if (this.soundPress)
8218
8475
  this.soundPress.play();
8219
8476
  if (uiSystem.activeObject && !isActive)
@@ -8221,13 +8478,15 @@ class UIObject
8221
8478
  uiSystem.activeObject = this;
8222
8479
  }
8223
8480
  }
8224
- if (!mouseDown && uiSystem.activeObject === this && this.interactive)
8481
+ if (!mouseDown && this.isActiveObject() && this.interactive)
8225
8482
  {
8226
8483
  this.onClick();
8227
8484
  if (this.soundClick)
8228
8485
  this.soundClick.play();
8229
8486
  }
8230
8487
  }
8488
+ // clear mouse was pressed state even when disabled
8489
+ mousePress && inputClearKey(0,0,0,1,0);
8231
8490
  }
8232
8491
  if (isActive)
8233
8492
  if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
@@ -8238,6 +8497,7 @@ class UIObject
8238
8497
  uiSystem.activeObject = undefined;
8239
8498
  }
8240
8499
 
8500
+ // call enter/leave events
8241
8501
  if (this.isHoverObject() !== wasHover)
8242
8502
  this.isHoverObject() ? this.onEnter() : this.onLeave();
8243
8503
  }
@@ -8248,7 +8508,7 @@ class UIObject
8248
8508
  if (!this.size.x || !this.size.y) return;
8249
8509
 
8250
8510
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8251
- const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8511
+ const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8252
8512
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8253
8513
  }
8254
8514
 
@@ -8299,6 +8559,7 @@ class UIObject
8299
8559
  /**
8300
8560
  * UIText - A UI object that displays text
8301
8561
  * @extends UIObject
8562
+ * @memberof UISystem
8302
8563
  */
8303
8564
  class UIText extends UIObject
8304
8565
  {
@@ -8313,6 +8574,10 @@ class UIText extends UIObject
8313
8574
  {
8314
8575
  super(pos, size);
8315
8576
 
8577
+ ASSERT(isString(text), 'ui text must be a string');
8578
+ ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
8579
+ ASSERT(isString(font), 'ui text font must be a string');
8580
+
8316
8581
  // set properties
8317
8582
  this.text = text;
8318
8583
  this.align = align;
@@ -8332,6 +8597,7 @@ class UIText extends UIObject
8332
8597
  /**
8333
8598
  * UITile - A UI object that displays a tile image
8334
8599
  * @extends UIObject
8600
+ * @memberof UISystem
8335
8601
  */
8336
8602
  class UITile extends UIObject
8337
8603
  {
@@ -8346,15 +8612,19 @@ class UITile extends UIObject
8346
8612
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
8347
8613
  {
8348
8614
  super(pos, size);
8615
+
8616
+ ASSERT(tileInfo instanceof TileInfo, 'ui tile tileInfo must be a TileInfo');
8617
+ ASSERT(isColor(color), 'ui tile color must be a color');
8618
+ ASSERT(isNumber(angle), 'ui tile angle must be a number');
8619
+
8349
8620
  /** @property {TileInfo} - Tile image to use */
8350
8621
  this.tileInfo = tileInfo;
8351
8622
  /** @property {number} - Angle to rotate in radians */
8352
8623
  this.angle = angle;
8353
8624
  /** @property {boolean} - Should it be mirrored? */
8354
8625
  this.mirror = mirror;
8355
-
8356
8626
  // set properties
8357
- this.color = color;
8627
+ this.color = color.copy();
8358
8628
  }
8359
8629
  render()
8360
8630
  {
@@ -8366,6 +8636,7 @@ class UITile extends UIObject
8366
8636
  /**
8367
8637
  * UIButton - A UI object that acts as a button
8368
8638
  * @extends UIObject
8639
+ * @memberof UISystem
8369
8640
  */
8370
8641
  class UIButton extends UIObject
8371
8642
  {
@@ -8379,9 +8650,12 @@ class UIButton extends UIObject
8379
8650
  {
8380
8651
  super(pos, size);
8381
8652
 
8653
+ ASSERT(isString(text), 'ui button must be a string');
8654
+ ASSERT(isColor(color), 'ui button color must be a color');
8655
+
8382
8656
  // set properties
8383
8657
  this.text = text;
8384
- this.color = color;
8658
+ this.color = color.copy()
8385
8659
  this.interactive = true;
8386
8660
  }
8387
8661
  render()
@@ -8399,6 +8673,7 @@ class UIButton extends UIObject
8399
8673
  /**
8400
8674
  * UICheckbox - A UI object that acts as a checkbox
8401
8675
  * @extends UIObject
8676
+ * @memberof UISystem
8402
8677
  */
8403
8678
  class UICheckbox extends UIObject
8404
8679
  {
@@ -8412,12 +8687,15 @@ class UICheckbox extends UIObject
8412
8687
  constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
8413
8688
  {
8414
8689
  super(pos, size);
8690
+
8691
+ ASSERT(isString(text), 'ui checkbox must be a string');
8692
+ ASSERT(isColor(color), 'ui checkbox color must be a color');
8693
+
8415
8694
  /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
8416
8695
  this.checked = checked;
8417
-
8418
8696
  // set properties
8419
8697
  this.text = text;
8420
- this.color = color;
8698
+ this.color = color.copy();
8421
8699
  this.interactive = true;
8422
8700
  }
8423
8701
  onClick()
@@ -8449,6 +8727,7 @@ class UICheckbox extends UIObject
8449
8727
  /**
8450
8728
  * UIScrollbar - A UI object that acts as a scrollbar
8451
8729
  * @extends UIObject
8730
+ * @memberof UISystem
8452
8731
  */
8453
8732
  class UIScrollbar extends UIObject
8454
8733
  {
@@ -8464,14 +8743,19 @@ class UIScrollbar extends UIObject
8464
8743
  {
8465
8744
  super(pos, size);
8466
8745
 
8746
+ ASSERT(isNumber(value), 'ui scrollbar value must be a number');
8747
+ ASSERT(isString(text), 'ui scrollbar must be a string');
8748
+ ASSERT(isColor(color), 'ui scrollbar color must be a color');
8749
+ ASSERT(isColor(handleColor), 'ui scrollbar handleColor must be a color');
8750
+
8467
8751
  /** @property {number} - Current percentage value of this scrollbar 0-1 */
8468
8752
  this.value = value;
8469
8753
  /** @property {Color} - Color for the handle part of the scrollbar */
8470
- this.handleColor = handleColor;
8754
+ this.handleColor = handleColor.copy();
8471
8755
 
8472
8756
  // set properties
8473
8757
  this.text = text;
8474
- this.color = color;
8758
+ this.color = color.copy();
8475
8759
  this.interactive = true;
8476
8760
  }
8477
8761
  update()
@@ -8479,29 +8763,43 @@ class UIScrollbar extends UIObject
8479
8763
  super.update();
8480
8764
  if (this.isActiveObject() && this.interactive)
8481
8765
  {
8766
+ // handle horizontal or vertical scrollbar
8767
+ const isHorizontal = this.size.x > this.size.y;
8768
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8769
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8770
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8771
+
8482
8772
  // check if value changed
8483
- const handleSize = vec2(this.size.y);
8484
- const handleWidth = this.size.x - handleSize.x;
8485
- const p1 = this.pos.x - handleWidth/2;
8486
- const p2 = this.pos.x + handleWidth/2;
8773
+ const handleWidth = barSize - handleSize;
8774
+ const p1 = centerPos - handleWidth/2;
8775
+ const p2 = centerPos + handleWidth/2;
8487
8776
  const oldValue = this.value;
8488
- this.value = percent(mousePosScreen.x, p1, p2);
8777
+ this.value = isHorizontal ?
8778
+ percent(mousePosScreen.x, p1, p2) :
8779
+ percent(mousePosScreen.y, p2, p1);
8489
8780
  this.value === oldValue || this.onChange();
8490
8781
  }
8491
8782
  }
8492
8783
  render()
8493
8784
  {
8494
8785
  super.render();
8495
-
8786
+
8787
+ // handle horizontal or vertical scrollbar
8788
+ const isHorizontal = this.size.x > this.size.y;
8789
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8790
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8791
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8792
+
8496
8793
  // draw the scrollbar handle
8497
- const handleSize = vec2(this.size.y);
8498
- const handleWidth = this.size.x - handleSize.x;
8499
- const p1 = this.pos.x - handleWidth/2;
8500
- const p2 = this.pos.x + handleWidth/2;
8501
- const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
8794
+ const handleWidth = barSize - handleSize;
8795
+ const p1 = centerPos - handleWidth/2;
8796
+ const p2 = centerPos + handleWidth/2;
8797
+ const handlePos = isHorizontal ?
8798
+ vec2(lerp(p1, p2, this.value), this.pos.y) :
8799
+ vec2(this.pos.x, lerp(p2, p1, this.value))
8502
8800
  const handleColor = this.disabled ? this.disabledColor :
8503
8801
  this.interactive && this.isActiveObject() ? this.color : this.handleColor;
8504
- uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8802
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8505
8803
 
8506
8804
  // draw the text scaled to fit on the scrollbar
8507
8805
  const textSize = this.getTextSize();
@@ -8548,6 +8846,7 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
8548
8846
  * - Each object has a Box2D body which can have multiple fixtures and joints
8549
8847
  * - Provides interface for Box2D body and fixture functions
8550
8848
  * @extends EngineObject
8849
+ * @memberof Box2D
8551
8850
  */
8552
8851
  class Box2dObject extends EngineObject
8553
8852
  {
@@ -8559,7 +8858,7 @@ class Box2dObject extends EngineObject
8559
8858
  * @param {Color} [color]
8560
8859
  * @param {number} [bodyType]
8561
8860
  * @param {number} [renderOrder] */
8562
- constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8861
+ constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8563
8862
  {
8564
8863
  super(pos, size, tileInfo, angle, color, renderOrder);
8565
8864
 
@@ -8573,7 +8872,7 @@ class Box2dObject extends EngineObject
8573
8872
  this.lineColor = BLACK;
8574
8873
  }
8575
8874
 
8576
- /** Destroy this object and it's physics body */
8875
+ /** Destroy this object and its physics body */
8577
8876
  destroy()
8578
8877
  {
8579
8878
  // destroy physics body, fixtures, and joints
@@ -9057,6 +9356,7 @@ class Box2dRaycastResult
9057
9356
  * Box2D Joint
9058
9357
  * - Base class for Box2D joints
9059
9358
  * - A joint is used to connect objects together
9359
+ * @memberof Box2D
9060
9360
  */
9061
9361
  class Box2dJoint
9062
9362
  {
@@ -9112,6 +9412,7 @@ class Box2dJoint
9112
9412
  * - This a soft constraint with a max force
9113
9413
  * - This allows the constraint to stretch and without applying huge forces
9114
9414
  * @extends Box2dJoint
9415
+ * @memberof Box2D
9115
9416
  */
9116
9417
  class Box2dTargetJoint extends Box2dJoint
9117
9418
  {
@@ -9161,6 +9462,7 @@ class Box2dTargetJoint extends Box2dJoint
9161
9462
  * - Constrains two points on two objects to remain at a fixed distance
9162
9463
  * - You can view this as a massless, rigid rod
9163
9464
  * @extends Box2dJoint
9465
+ * @memberof Box2D
9164
9466
  */
9165
9467
  class Box2dDistanceJoint extends Box2dJoint
9166
9468
  {
@@ -9224,6 +9526,7 @@ class Box2dDistanceJoint extends Box2dJoint
9224
9526
  * Box2D Pin Joint
9225
9527
  * - Pins two objects together at a point
9226
9528
  * @extends Box2dDistanceJoint
9529
+ * @memberof Box2D
9227
9530
  */
9228
9531
  class Box2dPinJoint extends Box2dDistanceJoint
9229
9532
  {
@@ -9243,6 +9546,7 @@ class Box2dPinJoint extends Box2dDistanceJoint
9243
9546
  * Box2D Rope Joint
9244
9547
  * - Enforces a maximum distance between two points on two objects
9245
9548
  * @extends Box2dJoint
9549
+ * @memberof Box2D
9246
9550
  */
9247
9551
  class Box2dRopeJoint extends Box2dJoint
9248
9552
  {
@@ -9295,6 +9599,7 @@ class Box2dRopeJoint extends Box2dJoint
9295
9599
  * - You can use a motor to drive the relative rotation about the shared point
9296
9600
  * - A maximum motor torque is provided so that infinite forces are not generated
9297
9601
  * @extends Box2dJoint
9602
+ * @memberof Box2D
9298
9603
  */
9299
9604
  class Box2dRevoluteJoint extends Box2dJoint
9300
9605
  {
@@ -9396,6 +9701,7 @@ class Box2dRevoluteJoint extends Box2dJoint
9396
9701
  * - Either joint can be a revolute or prismatic joint
9397
9702
  * - You specify a gear ratio to bind the motions together
9398
9703
  * @extends Box2dJoint
9704
+ * @memberof Box2D
9399
9705
  */
9400
9706
  class Box2dGearJoint extends Box2dJoint
9401
9707
  {
@@ -9444,6 +9750,7 @@ class Box2dGearJoint extends Box2dJoint
9444
9750
  * - You can use a joint limit to restrict the range of motion
9445
9751
  * - You can use a joint motor to drive the motion or to model joint friction
9446
9752
  * @extends Box2dJoint
9753
+ * @memberof Box2D
9447
9754
  */
9448
9755
  class Box2dPrismaticJoint extends Box2dJoint
9449
9756
  {
@@ -9553,6 +9860,7 @@ class Box2dPrismaticJoint extends Box2dJoint
9553
9860
  * - You can use a joint motor to drive the motion or to model joint friction
9554
9861
  * - This joint is designed for vehicle suspensions
9555
9862
  * @extends Box2dJoint
9863
+ * @memberof Box2D
9556
9864
  */
9557
9865
  class Box2dWheelJoint extends Box2dJoint
9558
9866
  {
@@ -9648,6 +9956,7 @@ class Box2dWheelJoint extends Box2dJoint
9648
9956
  * Box2D Weld Joint
9649
9957
  * - Glues two objects together
9650
9958
  * @extends Box2dJoint
9959
+ * @memberof Box2D
9651
9960
  */
9652
9961
  class Box2dWeldJoint extends Box2dJoint
9653
9962
  {
@@ -9706,6 +10015,7 @@ class Box2dWeldJoint extends Box2dJoint
9706
10015
  * - Used to apply top-down friction
9707
10016
  * - Provides 2D translational friction and angular friction
9708
10017
  * @extends Box2dJoint
10018
+ * @memberof Box2D
9709
10019
  */
9710
10020
  class Box2dFrictionJoint extends Box2dJoint
9711
10021
  {
@@ -9760,6 +10070,7 @@ class Box2dFrictionJoint extends Box2dJoint
9760
10070
  * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
9761
10071
  * - The force transmitted is scaled by the ratio
9762
10072
  * @extends Box2dJoint
10073
+ * @memberof Box2D
9763
10074
  */
9764
10075
  class Box2dPulleyJoint extends Box2dJoint
9765
10076
  {
@@ -9827,6 +10138,7 @@ class Box2dPulleyJoint extends Box2dJoint
9827
10138
  * - Controls the relative motion between two objects
9828
10139
  * - Typical usage is to control the movement of a object with respect to the ground
9829
10140
  * @extends Box2dJoint
10141
+ * @memberof Box2D
9830
10142
  */
9831
10143
  class Box2dMotorJoint extends Box2dJoint
9832
10144
  {
@@ -9890,6 +10202,7 @@ class Box2dMotorJoint extends Box2dJoint
9890
10202
  /**
9891
10203
  * Box2D Global Object
9892
10204
  * - Wraps Box2d world and provides global functions
10205
+ * @memberof Box2D
9893
10206
  */
9894
10207
  class Box2dPlugin
9895
10208
  {
@@ -10224,7 +10537,7 @@ async function box2dInit()
10224
10537
  }
10225
10538
  function box2dRender()
10226
10539
  {
10227
- if (box2dDebug || debugPhysics && debugOverlay)
10540
+ if (box2dDebug || debugPhysics)
10228
10541
  box2d.world.DrawDebugData();
10229
10542
  }
10230
10543
 
@@ -10461,6 +10774,7 @@ export
10461
10774
 
10462
10775
  // Debug
10463
10776
  ASSERT,
10777
+ LOG,
10464
10778
  debugRect,
10465
10779
  debugPoly,
10466
10780
  debugCircle,
@@ -10607,6 +10921,7 @@ export
10607
10921
  isColor,
10608
10922
  isVector2,
10609
10923
  isNumber,
10924
+ isString,
10610
10925
 
10611
10926
  // Default Colors
10612
10927
  WHITE,
@@ -10742,7 +11057,7 @@ export
10742
11057
  tileCollisionGetData,
10743
11058
  tileCollisionTest,
10744
11059
  tileCollisionRaycast,
10745
- tileCollisionLoad,
11060
+ tileLayersLoad,
10746
11061
  TileLayerData,
10747
11062
  CanvasLayer,
10748
11063
  TileLayer,