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)
@@ -690,6 +721,7 @@ const debugMedals = 0;
690
721
 
691
722
  // debug commands are automatically removed from the final build
692
723
  function ASSERT (){}
724
+ function LOG (){}
693
725
  function debugInit (){}
694
726
  function debugUpdate (){}
695
727
  function debugRender (){}
@@ -927,7 +959,12 @@ function wave(frequency=1, amplitude=1, t=time, offset=0)
927
959
  * @param {number} t - time in seconds
928
960
  * @return {string}
929
961
  * @memberof Utilities */
930
- function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
962
+ function formatTime(t)
963
+ {
964
+ const sign = t < 0 ? '-' : '';
965
+ t = abs(t)|0;
966
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
967
+ }
931
968
 
932
969
  /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
933
970
  * @param {string} url - URL of JSON file
@@ -936,6 +973,8 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
936
973
  async function fetchJSON(url)
937
974
  {
938
975
  const response = await fetch(url);
976
+ if (!response.ok)
977
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
939
978
  return response.json();
940
979
  }
941
980
 
@@ -946,6 +985,13 @@ async function fetchJSON(url)
946
985
  * @memberof Utilities */
947
986
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
948
987
 
988
+ /**
989
+ * Check if object is a valid string or can be converted to one
990
+ * @param {any} s
991
+ * @return {boolean}
992
+ * @memberof Utilities */
993
+ function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
994
+
949
995
  ///////////////////////////////////////////////////////////////////////////////
950
996
 
951
997
  /** Random global functions
@@ -1008,6 +1054,7 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1008
1054
  /**
1009
1055
  * Seeded random number generator
1010
1056
  * - Can be used to create a deterministic random number sequence
1057
+ * @memberof Engine
1011
1058
  * @example
1012
1059
  * let r = new RandomGenerator(123); // random number generator with seed 123
1013
1060
  * let a = r.float(); // random value between 0 and 1
@@ -1104,6 +1151,7 @@ function ASSERT_VECTOR2_NORMAL(v)
1104
1151
  /**
1105
1152
  * 2D Vector object with vector math library
1106
1153
  * - Functions do not change this so they can be chained together
1154
+ * @memberof Engine
1107
1155
  * @example
1108
1156
  * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
1109
1157
  * let b = new Vector2; // vector with coordinates (0, 0)
@@ -1244,9 +1292,10 @@ class Vector2
1244
1292
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1245
1293
  }
1246
1294
 
1247
- /** Set the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
1295
+ /** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
1248
1296
  * @param {number} [direction]
1249
- * @param {number} [length] */
1297
+ * @param {number} [length]
1298
+ * @return {Vector2} */
1250
1299
  setDirection(direction, length=1)
1251
1300
  {
1252
1301
  ASSERT_NUMBER_VALID(direction);
@@ -1254,8 +1303,10 @@ class Vector2
1254
1303
  direction = mod(direction, 4);
1255
1304
  ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1256
1305
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1257
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1258
- direction%2 ? 0 : direction ? -length : length);
1306
+
1307
+ this.x = direction%2 ? direction-1 ? -length : length : 0;
1308
+ this.y = direction%2 ? 0 : direction ? -length : length;
1309
+ return this;
1259
1310
  }
1260
1311
 
1261
1312
  /** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
@@ -1263,11 +1314,7 @@ class Vector2
1263
1314
  direction()
1264
1315
  { return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }
1265
1316
 
1266
- /** Returns a copy of this vector that has been inverted
1267
- * @return {Vector2} */
1268
- invert() { return new Vector2(this.y, -this.x); }
1269
-
1270
- /** Returns a copy of this vector absolute values
1317
+ /** Returns a copy of this vector with absolute values
1271
1318
  * @return {Vector2} */
1272
1319
  abs() { return new Vector2(abs(this.x), abs(this.y)); }
1273
1320
 
@@ -1309,13 +1356,10 @@ class Vector2
1309
1356
  toString(digits=3)
1310
1357
  {
1311
1358
  ASSERT_NUMBER_VALID(digits);
1312
- if (debug)
1313
- {
1314
- if (this.isValid())
1315
- return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1316
- else
1317
- return `(${this.x}, ${this.y})`;
1318
- }
1359
+ if (this.isValid())
1360
+ return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1361
+ else
1362
+ return `(${this.x}, ${this.y})`;
1319
1363
  }
1320
1364
 
1321
1365
  /** Checks if this is a valid vector
@@ -1358,6 +1402,7 @@ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
1358
1402
 
1359
1403
  /**
1360
1404
  * Color object (red, green, blue, alpha) with some helpful functions
1405
+ * @memberof Engine
1361
1406
  * @example
1362
1407
  * let a = new Color; // white
1363
1408
  * let b = new Color(1, 0, 0); // red
@@ -1535,7 +1580,8 @@ class Color
1535
1580
  * @return {Color} */
1536
1581
  setHex(hex)
1537
1582
  {
1538
- ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
1583
+ ASSERT(isString(hex), 'Color hex code must be a string');
1584
+ ASSERT(hex[0] === '#', 'Color hex code must start with #');
1539
1585
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1540
1586
 
1541
1587
  if (hex.length < 6)
@@ -1577,77 +1623,78 @@ class Color
1577
1623
  }
1578
1624
 
1579
1625
  ///////////////////////////////////////////////////////////////////////////////
1580
- // default colors
1626
+ // Default Colors
1581
1627
 
1582
1628
  /** Color - White #ffffff
1583
1629
  * @type {Color}
1584
1630
  * @memberof Utilities */
1585
- const WHITE = rgb();
1631
+ const WHITE = protectEngineConstant(rgb());
1586
1632
 
1587
- /** Color - Clear White #ffffff with 0 alpha
1633
+ /** Color - Clear White #757474ff with 0 alpha
1588
1634
  * @type {Color}
1589
1635
  * @memberof Utilities */
1590
- const CLEAR_WHITE = rgb(1,1,1,0);
1636
+ const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
1591
1637
 
1592
1638
  /** Color - Black #000000
1593
1639
  * @type {Color}
1594
1640
  * @memberof Utilities */
1595
- const BLACK = rgb(0,0,0);
1641
+ const BLACK = protectEngineConstant(rgb(0,0,0));
1596
1642
 
1597
1643
  /** Color - Clear Black #000000 with 0 alpha
1598
1644
  * @type {Color}
1599
1645
  * @memberof Utilities */
1600
- const CLEAR_BLACK = rgb(0,0,0,0);
1646
+ const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
1601
1647
 
1602
1648
  /** Color - Gray #808080
1603
1649
  * @type {Color}
1604
1650
  * @memberof Utilities */
1605
- const GRAY = rgb(.5,.5,.5);
1651
+ const GRAY = protectEngineConstant(rgb(.5,.5,.5));
1606
1652
 
1607
1653
  /** Color - Red #ff0000
1608
1654
  * @type {Color}
1609
1655
  * @memberof Utilities */
1610
- const RED = rgb(1,0,0);
1656
+ const RED = protectEngineConstant(rgb(1,0,0));
1611
1657
 
1612
1658
  /** Color - Orange #ff8000
1613
1659
  * @type {Color}
1614
1660
  * @memberof Utilities */
1615
- const ORANGE = rgb(1,.5,0);
1661
+ const ORANGE = protectEngineConstant(rgb(1,.5,0));
1616
1662
 
1617
1663
  /** Color - Yellow #ffff00
1618
1664
  * @type {Color}
1619
1665
  * @memberof Utilities */
1620
- const YELLOW = rgb(1,1,0);
1666
+ const YELLOW = protectEngineConstant(rgb(1,1,0));
1621
1667
 
1622
1668
  /** Color - Green #00ff00
1623
1669
  * @type {Color}
1624
1670
  * @memberof Utilities */
1625
- const GREEN = rgb(0,1,0);
1671
+ const GREEN = protectEngineConstant(rgb(0,1,0));
1626
1672
 
1627
1673
  /** Color - Cyan #00ffff
1628
1674
  * @type {Color}
1629
1675
  * @memberof Utilities */
1630
- const CYAN = rgb(0,1,1);
1676
+ const CYAN = protectEngineConstant(rgb(0,1,1));
1631
1677
 
1632
1678
  /** Color - Blue #0000ff
1633
1679
  * @type {Color}
1634
1680
  * @memberof Utilities */
1635
- const BLUE = rgb(0,0,1);
1681
+ const BLUE = protectEngineConstant(rgb(0,0,1));
1636
1682
 
1637
1683
  /** Color - Purple #8000ff
1638
1684
  * @type {Color}
1639
1685
  * @memberof Utilities */
1640
- const PURPLE = rgb(.5,0,1);
1686
+ const PURPLE = protectEngineConstant(rgb(.5,0,1));
1641
1687
 
1642
1688
  /** Color - Magenta #ff00ff
1643
1689
  * @type {Color}
1644
1690
  * @memberof Utilities */
1645
- const MAGENTA = rgb(1,0,1);
1691
+ const MAGENTA = protectEngineConstant(rgb(1,0,1));
1646
1692
 
1647
1693
  ///////////////////////////////////////////////////////////////////////////////
1648
1694
 
1649
1695
  /**
1650
1696
  * Timer object tracks how long has passed since it was set
1697
+ * @memberof Engine
1651
1698
  * @example
1652
1699
  * let a = new Timer; // creates a timer that is not set
1653
1700
  * a.set(3); // sets the timer to 3 seconds
@@ -1704,11 +1751,41 @@ class Timer
1704
1751
 
1705
1752
  /** Returns this timer expressed as a string
1706
1753
  * @return {string} */
1707
- toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
1754
+ toString() { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1708
1755
 
1709
1756
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1710
1757
  * @return {number} */
1711
1758
  valueOf() { return this.get(); }
1759
+ }
1760
+
1761
+ ///////////////////////////////////////////////////////////////////////////////
1762
+ // Helper functions used by the engine
1763
+
1764
+ // make color constants immutable with debug assertions
1765
+ function protectEngineConstant(obj)
1766
+ {
1767
+ if (debug)
1768
+ {
1769
+ // get properties and store original values
1770
+ const props = Object.keys(obj), values = {};
1771
+ props.forEach(prop => values[prop] = obj[prop]);
1772
+
1773
+ // replace with getters/setters that assert
1774
+ props.forEach(prop =>
1775
+ {
1776
+ Object.defineProperty(obj, prop, {
1777
+ get: () => values[prop],
1778
+ set: (value) =>
1779
+ {
1780
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1781
+ },
1782
+ enumerable: true
1783
+ });
1784
+ });
1785
+ }
1786
+
1787
+ // freeze the object to prevent adding new properties
1788
+ return Object.freeze(obj);
1712
1789
  }
1713
1790
  /**
1714
1791
  * LittleJS Engine Settings
@@ -2016,7 +2093,7 @@ let medalsPreventUnlock = false;
2016
2093
  /** Set position of camera in world space
2017
2094
  * @param {Vector2} pos
2018
2095
  * @memberof Settings */
2019
- function setCameraPos(pos) { cameraPos = pos; }
2096
+ function setCameraPos(pos) { cameraPos = pos.copy(); }
2020
2097
 
2021
2098
  /** Set angle of camera in world space
2022
2099
  * @param {number} angle
@@ -2039,17 +2116,17 @@ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2039
2116
  /** Set color to clear the canvas to before render
2040
2117
  * @param {Color} color
2041
2118
  * @memberof Settings */
2042
- function setCanvasClearColor(color) { canvasClearColor = color; }
2119
+ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
2043
2120
 
2044
2121
  /** Set max size of the canvas
2045
2122
  * @param {Vector2} size
2046
2123
  * @memberof Settings */
2047
- function setCanvasMaxSize(size) { canvasMaxSize = size; }
2124
+ function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
2048
2125
 
2049
2126
  /** Set fixed size of the canvas
2050
2127
  * @param {Vector2} size
2051
2128
  * @memberof Settings */
2052
- function setCanvasFixedSize(size) { canvasFixedSize = size; }
2129
+ function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
2053
2130
 
2054
2131
  /** Use nearest scaling algorithm for canvas for more pixelated look
2055
2132
  * - If enabled sets css image-rendering:pixelated
@@ -2114,7 +2191,7 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
2114
2191
  /** Set default size of tiles in pixels
2115
2192
  * @param {Vector2} size
2116
2193
  * @memberof Settings */
2117
- function setTileSizeDefault(size) { tileSizeDefault = size; }
2194
+ function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
2118
2195
 
2119
2196
  /** Set to prevent tile bleeding from neighbors in pixels
2120
2197
  * @param {number} scale
@@ -2159,7 +2236,7 @@ function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
2159
2236
  /** Set how much gravity to apply to objects
2160
2237
  * @param {Vector2} newGravity
2161
2238
  * @memberof Settings */
2162
- function setGravity(newGravity) { gravity = newGravity; }
2239
+ function setGravity(newGravity) { gravity = newGravity.copy(); }
2163
2240
 
2164
2241
  /** Set to scales emit rate of particles
2165
2242
  * @param {number} scale
@@ -2249,7 +2326,7 @@ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
2249
2326
  /** Set size of medal display
2250
2327
  * @param {Vector2} size
2251
2328
  * @memberof Settings */
2252
- function setMedalDisplaySize(size) { medalDisplaySize = size; }
2329
+ function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
2253
2330
 
2254
2331
  /** Set to stop medals from being unlockable
2255
2332
  * @param {boolean} preventUnlock
@@ -2289,6 +2366,7 @@ function setDebugKey(key) { debugKey = key; }
2289
2366
  * - Collision for objects can be set to be solid to block other objects
2290
2367
  * - Objects may get pushed into overlapping other solid objects, if so they will push away
2291
2368
  * - Solid objects are more performance intensive and should be used sparingly
2369
+ * @memberof Engine
2292
2370
  * @example
2293
2371
  * // create an engine object, normally you would first extend the class with your own
2294
2372
  * const pos = vec2(2,3);
@@ -2297,18 +2375,18 @@ function setDebugKey(key) { debugKey = key; }
2297
2375
  class EngineObject
2298
2376
  {
2299
2377
  /** Create an engine object and adds it to the list of objects
2300
- * @param {Vector2} [pos=(0,0)] - World space position of the object
2301
- * @param {Vector2} [size=(1,1)] - World space size of the object
2302
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2303
- * @param {number} [angle] - Angle the object is rotated by
2304
- * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
2305
- * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2378
+ * @param {Vector2} [pos=(0,0)] - World space position of the object
2379
+ * @param {Vector2} [size=(1,1)] - World space size of the object
2380
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2381
+ * @param {number} [angle] - Angle the object is rotated by
2382
+ * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2383
+ * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2306
2384
  */
2307
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
2385
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
2308
2386
  {
2309
2387
  // check passed in params
2310
- ASSERT(isVector2(pos), 'object pos should be a vec2');
2311
- ASSERT(isVector2(size), 'object size should be a vec2');
2388
+ ASSERT(isVector2(pos), 'object pos must be a vec2');
2389
+ ASSERT(isVector2(size), 'object size must be a vec2');
2312
2390
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2313
2391
  ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2314
2392
  ASSERT(isColor(color), 'object color should be a valid rgba color');
@@ -2593,7 +2671,7 @@ class EngineObject
2593
2671
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2594
2672
  }
2595
2673
 
2596
- /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
2674
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
2597
2675
  destroy()
2598
2676
  {
2599
2677
  if (this.destroyed)
@@ -2667,6 +2745,8 @@ class EngineObject
2667
2745
  addChild(child, localPos=vec2(), localAngle=0)
2668
2746
  {
2669
2747
  ASSERT(!child.parent && !this.children.includes(child));
2748
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
2749
+ ASSERT(child !== this, 'cannot add self as child');
2670
2750
  this.children.push(child);
2671
2751
  child.parent = this;
2672
2752
  child.localPos = localPos.copy();
@@ -2678,6 +2758,7 @@ class EngineObject
2678
2758
  removeChild(child)
2679
2759
  {
2680
2760
  ASSERT(child.parent === this && this.children.includes(child));
2761
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
2681
2762
  this.children.splice(this.children.indexOf(child), 1);
2682
2763
  child.parent = 0;
2683
2764
  }
@@ -2816,7 +2897,7 @@ let drawCount;
2816
2897
  * Create a tile info object using a grid based system
2817
2898
  * - This can take vecs or floats for easier use and conversion
2818
2899
  * - If an index is passed in, the tile size and index will determine the position
2819
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
2900
+ * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
2820
2901
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
2821
2902
  * @param {number} [textureIndex] - Texture index to use
2822
2903
  * @param {number} [padding] - How many pixels padding around tiles
@@ -2861,6 +2942,7 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2861
2942
 
2862
2943
  /**
2863
2944
  * Tile Info - Stores info about how to draw a tile
2945
+ * @memberof Draw
2864
2946
  */
2865
2947
  class TileInfo
2866
2948
  {
@@ -2921,7 +3003,10 @@ class TileInfo
2921
3003
  }
2922
3004
  }
2923
3005
 
2924
- /** Texture Info - Stores info about each texture */
3006
+ /**
3007
+ * Tile Info - Stores info about each texture
3008
+ * @memberof Draw
3009
+ */
2925
3010
  class TextureInfo
2926
3011
  {
2927
3012
  /**
@@ -2967,10 +3052,11 @@ class TextureInfo
2967
3052
  function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2968
3053
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2969
3054
  {
2970
- ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
2971
- ASSERT(isVector2(size), 'drawTile size should be a vec2');
2972
- ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
2973
- ASSERT(isNumber(angle), 'drawTile angle should be a number');
3055
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3056
+ ASSERT(isVector2(size), 'size must be a vec2');
3057
+ ASSERT(isColor(color), 'color is invalid');
3058
+ ASSERT(isNumber(angle), 'angle must be a number');
3059
+ ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
2974
3060
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2975
3061
 
2976
3062
  const textureInfo = tileInfo && tileInfo.textureInfo;
@@ -3065,10 +3151,10 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3065
3151
  * @memberof Draw */
3066
3152
  function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3067
3153
  {
3068
- ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
3069
- ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
3070
- ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
3071
- ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
3154
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3155
+ ASSERT(isVector2(size), 'size must be a vec2');
3156
+ ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
3157
+ ASSERT(isNumber(angle), 'angle must be a number');
3072
3158
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3073
3159
  if (useWebGL)
3074
3160
  {
@@ -3126,11 +3212,11 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3126
3212
  * @memberof Draw */
3127
3213
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3128
3214
  {
3129
- ASSERT(Array.isArray(points), 'drawLineList points should be an array');
3130
- ASSERT(isNumber(width), 'drawLineList width should be a number');
3131
- ASSERT(isColor(color), 'drawLineList color is invalid');
3132
- ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
3133
- ASSERT(isNumber(angle), 'drawLineList angle should be a number');
3215
+ ASSERT(Array.isArray(points), 'points must be an array');
3216
+ ASSERT(isNumber(width), 'width must be a number');
3217
+ ASSERT(isColor(color), 'color is invalid');
3218
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3219
+ ASSERT(isNumber(angle), 'angle must be a number');
3134
3220
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3135
3221
  if (useWebGL)
3136
3222
  {
@@ -3201,8 +3287,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3201
3287
  * @memberof Draw */
3202
3288
  function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3203
3289
  {
3204
- ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
3205
- ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
3290
+ ASSERT(isVector2(size), 'size must be a vec2');
3291
+ ASSERT(isNumber(sides), 'sides must be a number');
3206
3292
 
3207
3293
  // build regular polygon points
3208
3294
  const points = [];
@@ -3228,12 +3314,13 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3228
3314
  * @memberof Draw */
3229
3315
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3230
3316
  {
3231
- ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
3232
- ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3233
- ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3234
- ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3235
- ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3317
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3318
+ ASSERT(Array.isArray(points), 'points must be an array');
3319
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3320
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3321
+ ASSERT(isNumber(angle), 'angle must be a number');
3236
3322
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3323
+
3237
3324
  if (useWebGL)
3238
3325
  {
3239
3326
  let scale = 1;
@@ -3280,13 +3367,14 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3280
3367
  * @memberof Draw */
3281
3368
  function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3282
3369
  {
3283
- ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
3284
- ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
3285
- ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3286
- ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3287
- ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3288
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3370
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3371
+ ASSERT(isVector2(size), 'size must be a vec2');
3372
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3373
+ ASSERT(isNumber(angle), 'angle must be a number');
3374
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3375
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3289
3376
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3377
+
3290
3378
  if (useWebGL)
3291
3379
  {
3292
3380
  // draw as a regular polygon
@@ -3323,21 +3411,32 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3323
3411
  * @memberof Draw */
3324
3412
  function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3325
3413
  {
3326
- ASSERT(isNumber(size), 'drawCircle size should be a number');
3414
+ ASSERT(isNumber(size), 'size must be a number');
3327
3415
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3328
3416
  }
3329
3417
 
3418
+ /**
3419
+ * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
3420
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
3421
+ * @memberof Draw
3422
+ */
3423
+
3330
3424
  /** Draw directly to a 2d canvas context in world space
3331
3425
  * @param {Vector2} pos
3332
3426
  * @param {Vector2} size
3333
3427
  * @param {number} angle
3334
3428
  * @param {boolean} [mirror]
3335
- * @param {Function} [drawFunction]
3429
+ * @param {Canvas2DDrawFunction} [drawFunction]
3336
3430
  * @param {boolean} [screenSpace=false]
3337
3431
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3338
3432
  * @memberof Draw */
3339
3433
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
3340
3434
  {
3435
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3436
+ ASSERT(isVector2(size), 'size must be a vec2');
3437
+ ASSERT(isNumber(angle), 'angle must be a number');
3438
+ ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
3439
+
3341
3440
  if (!screenSpace)
3342
3441
  {
3343
3442
  // transform from world space to screen space
@@ -3405,6 +3504,16 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3405
3504
  * @memberof Draw */
3406
3505
  function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3407
3506
  {
3507
+ ASSERT(isString(text), 'text must be a string');
3508
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3509
+ ASSERT(isNumber(size), 'size must be a number');
3510
+ ASSERT(isColor(color), 'color must be a color');
3511
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3512
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
3513
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
3514
+ ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3515
+ ASSERT(isString(font), 'font must be a string');
3516
+
3408
3517
  context.fillStyle = color.toString();
3409
3518
  context.strokeStyle = lineColor.toString();
3410
3519
  context.lineWidth = lineWidth;
@@ -3606,6 +3715,7 @@ let engineFontImage;
3606
3715
  * - 96 characters (from space to tilde) are stored in an image
3607
3716
  * - Uses a default 8x8 font if none is supplied
3608
3717
  * - You can also use fonts from the main tile sheet
3718
+ * @memberof Draw
3609
3719
  * @example
3610
3720
  * // use built in font
3611
3721
  * const font = new FontImage;
@@ -3662,7 +3772,7 @@ class FontImage
3662
3772
  * @param {boolean} [center]
3663
3773
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3664
3774
  */
3665
- drawTextScreen(text, pos, scale=4, center, context=overlayContext)
3775
+ drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
3666
3776
  {
3667
3777
  context.save();
3668
3778
  const size = this.tileSize;
@@ -4356,6 +4466,7 @@ function audioInit()
4356
4466
  * Sound Object - Stores a sound for later use and can be played positionally
4357
4467
  *
4358
4468
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4469
+ * @memberof Audio
4359
4470
  * @example
4360
4471
  * // create a sound
4361
4472
  * const sound_example = new Sound([.5,.5]);
@@ -4450,12 +4561,12 @@ class Sound
4450
4561
 
4451
4562
  /** Play the sound as a musical note with a semitone offset
4452
4563
  * This can be used to play music with chromatic scales
4453
- * @param {number} semitoneOffset - How many semitones to offset pitch
4564
+ * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
4454
4565
  * @param {Vector2} [pos] - World space position to play the sound if any
4455
4566
  * @param {number} [volume=1] - How much to scale volume by
4456
4567
  * @return {SoundInstance} - The audio source node
4457
4568
  */
4458
- playNote(semitoneOffset, pos, volume)
4569
+ playNote(semitoneOffset=0, pos, volume)
4459
4570
  {
4460
4571
  const pitch = getNoteFrequency(semitoneOffset, 1);
4461
4572
  return this.play(pos, volume, pitch, 0);
@@ -4478,6 +4589,8 @@ class Sound
4478
4589
  /**
4479
4590
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
4480
4591
  * - this can be used to play wave, mp3, and ogg files
4592
+ * @extends Sound
4593
+ * @memberof Audio
4481
4594
  * @example
4482
4595
  * // create a sound
4483
4596
  * const sound_example = new SoundWave('sound.mp3');
@@ -4487,22 +4600,29 @@ class Sound
4487
4600
  */
4488
4601
  class SoundWave extends Sound
4489
4602
  {
4603
+ /**
4604
+ * @callback SoundLoadCallback - Function called when sound is loaded
4605
+ * @param {SoundWave} sound
4606
+ * @memberof Audio
4607
+ */
4608
+
4490
4609
  /** Create a sound object and cache the wave file for later use
4491
4610
  * @param {string} filename - Filename of audio file to load
4492
4611
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
4493
4612
  * @param {number} [range=soundDefaultRange] - World space max range of sound
4494
4613
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
4495
- * @param {Function} [onloadCallback] - callback function to call when sound is loaded
4614
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
4496
4615
  */
4497
4616
  constructor(filename, randomness=0, range, taper, onloadCallback)
4498
4617
  {
4499
4618
  super(undefined, range, taper);
4500
4619
  if (!soundEnable || headlessMode) return;
4620
+ ASSERT(!filename || isString(filename), 'filename must be a string');
4501
4621
 
4502
- /** @property {Function} - callback function to call when sound is loaded */
4622
+ /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
4503
4623
  this.onloadCallback = onloadCallback;
4504
4624
  this.randomness = randomness;
4505
- this.loadSound(filename);
4625
+ filename && this.loadSound(filename);
4506
4626
  }
4507
4627
 
4508
4628
  /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
@@ -4511,6 +4631,8 @@ class SoundWave extends Sound
4511
4631
  async loadSound(filename)
4512
4632
  {
4513
4633
  const response = await fetch(filename);
4634
+ if (!response.ok)
4635
+ throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
4514
4636
  const arrayBuffer = await response.arrayBuffer();
4515
4637
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4516
4638
 
@@ -4546,7 +4668,7 @@ class SoundWave extends Sound
4546
4668
  this.sampleChannels = sampleChannels;
4547
4669
  this.loadedPercent = 1;
4548
4670
  if (this.onloadCallback)
4549
- this.onloadCallback();
4671
+ this.onloadCallback(this);
4550
4672
  }
4551
4673
  }
4552
4674
 
@@ -4555,6 +4677,7 @@ class SoundWave extends Sound
4555
4677
  /**
4556
4678
  * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
4557
4679
  * Represents a single playing instance of a sound with pause/resume capabilities
4680
+ * @memberof Audio
4558
4681
  * @example
4559
4682
  * // Play a sound and get an instance for control
4560
4683
  * const jumpSound = new Sound([.5,.5,220]);
@@ -4754,6 +4877,12 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4754
4877
 
4755
4878
  ///////////////////////////////////////////////////////////////////////////////
4756
4879
 
4880
+ /**
4881
+ * @callback AudioEndedCallback - Function called when a sound ends
4882
+ * @param {AudioBufferSourceNode} source
4883
+ * @memberof Audio
4884
+ */
4885
+
4757
4886
  /** Play cached audio samples with given settings
4758
4887
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
4759
4888
  * @param {number} [volume] - How much to scale volume by
@@ -4763,7 +4892,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4763
4892
  * @param {number} [sampleRate=44100] - Sample rate for the sound
4764
4893
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
4765
4894
  * @param {number} [offset] - Offset in seconds to start playback from
4766
- * @param {Function} [onended] - Callback for when the sound ends
4895
+ * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
4767
4896
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4768
4897
  * @memberof Audio */
4769
4898
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
@@ -4795,16 +4924,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4795
4924
  if (onended)
4796
4925
  source.addEventListener('ended', ()=> onended(source));
4797
4926
 
4927
+ const startOffset = offset * rate;
4798
4928
  if (!audioIsRunning())
4799
4929
  {
4800
- // fix stalled audio, this sound won't be able to play
4801
- audioContext.resume();
4802
- return;
4930
+ // fix stalled audio and start
4931
+ audioContext.resume().then(()=>source.start(0, startOffset));
4803
4932
  }
4804
-
4805
- // play and return sound
4806
- const startOffset = offset * rate;
4807
- source.start(0, startOffset);
4933
+ else
4934
+ source.start(0, startOffset);
4808
4935
  return source;
4809
4936
  }
4810
4937
 
@@ -4969,7 +5096,7 @@ function zzfxG
4969
5096
  * - Unlimited numbers of layers, allocates canvases as needed
4970
5097
  * - Tile layers can be drawn to using their context with canvas2d
4971
5098
  * - Tile layers can also have collision with EngineObjects
4972
- * @namespace TileCollision
5099
+ * @namespace TileLayers
4973
5100
  */
4974
5101
 
4975
5102
  ///////////////////////////////////////////////////////////////////////////////
@@ -4977,13 +5104,13 @@ function zzfxG
4977
5104
 
4978
5105
  /** Keep track of all tile layers with collision
4979
5106
  * @type {Array<TileCollisionLayer>}
4980
- * @memberof TileCollision */
5107
+ * @memberof TileLayers */
4981
5108
  const tileCollisionLayers = [];
4982
5109
 
4983
5110
  /** Get tile collision data for a given cell in the grid
4984
5111
  * @param {Vector2} pos
4985
5112
  * @return {number}
4986
- * @memberof TileCollision */
5113
+ * @memberof TileLayers */
4987
5114
  function tileCollisionGetData(pos)
4988
5115
  {
4989
5116
  // check all tile collision layers
@@ -4999,7 +5126,7 @@ function tileCollisionGetData(pos)
4999
5126
  * @param {EngineObject} [object] - An object or undefined for generic test
5000
5127
  * @param {boolean} [solidOnly] - Only check solid layers if true
5001
5128
  * @return {TileCollisionLayer}
5002
- * @memberof TileCollision */
5129
+ * @memberof TileLayers */
5003
5130
  function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5004
5131
  {
5005
5132
  for (const layer of tileCollisionLayers)
@@ -5017,7 +5144,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5017
5144
  * @param {EngineObject} [object] - An object or undefined for generic test
5018
5145
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5019
5146
  * @return {Vector2}
5020
- * @memberof TileCollision */
5147
+ * @memberof TileLayers */
5021
5148
  function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5022
5149
  {
5023
5150
  for (const layer of tileCollisionLayers)
@@ -5040,8 +5167,8 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5040
5167
  * @param {number} [collisionLayer] - Layer to use for collision if any
5041
5168
  * @param {boolean} [draw] - Should the layer be drawn automatically
5042
5169
  * @return {Array<TileCollisionLayer>}
5043
- * @memberof TileCollision */
5044
- function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5170
+ * @memberof TileLayers */
5171
+ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5045
5172
  {
5046
5173
  if (!tileMapData)
5047
5174
  {
@@ -5071,6 +5198,12 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5071
5198
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
5072
5199
  tileLayers[layerIndex] = tileLayer;
5073
5200
 
5201
+ // apply layer color
5202
+ const layerColor = dataLayer.tintcolor ?
5203
+ new Color().setHex(dataLayer.tintcolor) :
5204
+ dataLayer.color || WHITE;
5205
+ ASSERT(isColor(layerColor), 'layer color is not a color');
5206
+
5074
5207
  for (let x=levelSize.x; x--;)
5075
5208
  for (let y=levelSize.y; y--;)
5076
5209
  {
@@ -5078,7 +5211,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5078
5211
  const data = dataLayer.data[x + y*levelSize.x];
5079
5212
  if (data)
5080
5213
  {
5081
- const layerData = new TileLayerData(data-1);
5214
+ const layerData = new TileLayerData(data-1, 0, false, layerColor);
5082
5215
  tileLayer.setData(pos, layerData);
5083
5216
 
5084
5217
  // set collision for top layer
@@ -5095,6 +5228,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5095
5228
  ///////////////////////////////////////////////////////////////////////////////
5096
5229
  /**
5097
5230
  * Tile layer data object stores info about how to draw a tile
5231
+ * @memberof TileLayers
5098
5232
  * @example
5099
5233
  * // create tile layer data with tile index 0 and random orientation and color
5100
5234
  * const tileIndex = 0;
@@ -5132,6 +5266,7 @@ class TileLayerData
5132
5266
  * - Contains an offscreen canvas that can be rendered to
5133
5267
  * - WebGL rendering is optional, call useWebGL to enable
5134
5268
  * @extends EngineObject
5269
+ * @memberof TileLayers
5135
5270
  * @example
5136
5271
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
5137
5272
  */
@@ -5146,6 +5281,7 @@ class CanvasLayer extends EngineObject
5146
5281
  */
5147
5282
  constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5148
5283
  {
5284
+ ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5149
5285
  super(position, size, undefined, angle, WHITE, renderOrder);
5150
5286
 
5151
5287
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
@@ -5193,12 +5329,18 @@ class CanvasLayer extends EngineObject
5193
5329
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5194
5330
  }
5195
5331
 
5332
+ /**
5333
+ * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
5334
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
5335
+ * @memberof TileLayers
5336
+ */
5337
+
5196
5338
  /** Draw onto the layer canvas in world space (bypass WebGL)
5197
5339
  * @param {Vector2} pos
5198
5340
  * @param {Vector2} size
5199
5341
  * @param {number} angle
5200
5342
  * @param {boolean} mirror
5201
- * @param {Function} drawFunction */
5343
+ * @param {Canvas2DDrawCallback} drawFunction */
5202
5344
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5203
5345
  {
5204
5346
  const context = this.context;
@@ -5235,7 +5377,7 @@ class CanvasLayer extends EngineObject
5235
5377
  else
5236
5378
  {
5237
5379
  // untextured
5238
- context.fillStyle = color;
5380
+ context.fillStyle = color.toString();
5239
5381
  context.fillRect(-.5, -.5, 1, 1);
5240
5382
  }
5241
5383
  });
@@ -5272,7 +5414,9 @@ class CanvasLayer extends EngineObject
5272
5414
  * - To allow dynamic modifications, layers are rendered using canvas 2d
5273
5415
  * - Some devices like mobile phones are limited to 4k texture resolution
5274
5416
  * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
5417
+ * - Tile layers are centered on their corner, so normal levels are at (0,0)
5275
5418
  * @extends CanvasLayer
5419
+ * @memberof TileLayers
5276
5420
  * @example
5277
5421
  * const tileLayer = new TileLayer(vec2(), vec2(200,100));
5278
5422
  */
@@ -5282,15 +5426,14 @@ class TileLayer extends CanvasLayer
5282
5426
  * @param {Vector2} position - World space position
5283
5427
  * @param {Vector2} size - World space size
5284
5428
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
5285
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
5286
5429
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5287
5430
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5288
5431
  */
5289
- constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0, useWebGL=glEnable)
5432
+ constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5290
5433
  {
5291
5434
  super(position, size, 0, renderOrder, size);
5435
+
5292
5436
  this.tileInfo = tileInfo;
5293
-
5294
5437
  const canvasSize = size.multiply(tileInfo.size);
5295
5438
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
5296
5439
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
@@ -5327,6 +5470,8 @@ class TileLayer extends CanvasLayer
5327
5470
  * @param {boolean} [redraw] - Force the tile to redraw if true */
5328
5471
  setData(layerPos, data, redraw=false)
5329
5472
  {
5473
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
5474
+ ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
5330
5475
  if (layerPos.arrayCheck(this.size))
5331
5476
  {
5332
5477
  this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
@@ -5338,7 +5483,10 @@ class TileLayer extends CanvasLayer
5338
5483
  * @param {Vector2} layerPos - Local position in array
5339
5484
  * @return {TileLayerData} */
5340
5485
  getData(layerPos)
5341
- { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
5486
+ {
5487
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
5488
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
5489
+ }
5342
5490
 
5343
5491
  // Render the tile layer, called automatically by the engine
5344
5492
  render()
@@ -5347,9 +5495,10 @@ class TileLayer extends CanvasLayer
5347
5495
 
5348
5496
  // draw the tile layer as a single tile
5349
5497
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5350
- const pos = this.pos.add(this.size.scale(.5));
5498
+ const size = this.drawSize || this.size;
5499
+ const pos = this.pos.add(size.scale(.5));
5351
5500
  const useWebGL = glEnable && this.glTexture !== undefined;
5352
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5501
+ drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5353
5502
  }
5354
5503
 
5355
5504
  /** Draw all the tile data to an offscreen canvas
@@ -5441,6 +5590,7 @@ class TileLayer extends CanvasLayer
5441
5590
  * - there can be multiple tile collision layers
5442
5591
  * - tile collision layers should not overlap each other
5443
5592
  * @extends TileLayer
5593
+ * @memberof TileLayers
5444
5594
  */
5445
5595
  class TileCollisionLayer extends TileLayer
5446
5596
  {
@@ -5453,8 +5603,7 @@ class TileCollisionLayer extends TileLayer
5453
5603
  */
5454
5604
  constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5455
5605
  {
5456
- const scale = vec2(1); // collision layers are not scaled
5457
- super(position, size.floor(), tileInfo, scale, renderOrder, useWebGL);
5606
+ super(position, size.floor(), tileInfo, renderOrder, useWebGL);
5458
5607
 
5459
5608
  /** @property {Array<number>} - The tile collision grid */
5460
5609
  this.collisionData = [];
@@ -5484,6 +5633,7 @@ class TileCollisionLayer extends TileLayer
5484
5633
  * @param {Vector2} size - width and height of tile collision 2d grid */
5485
5634
  initCollision(size)
5486
5635
  {
5636
+ ASSERT(isVector2(size), 'size must be a Vector2');
5487
5637
  this.size = size.floor();
5488
5638
  this.collisionData = [];
5489
5639
  this.collisionData.length = size.area();
@@ -5495,6 +5645,7 @@ class TileCollisionLayer extends TileLayer
5495
5645
  * @param {number} [data] */
5496
5646
  setCollisionData(gridPos, data=1)
5497
5647
  {
5648
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
5498
5649
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
5499
5650
  gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
5500
5651
  }
@@ -5504,6 +5655,7 @@ class TileCollisionLayer extends TileLayer
5504
5655
  * @return {number} */
5505
5656
  getCollisionData(gridPos)
5506
5657
  {
5658
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
5507
5659
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
5508
5660
  return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
5509
5661
  }
@@ -5515,6 +5667,9 @@ class TileCollisionLayer extends TileLayer
5515
5667
  * @return {boolean} */
5516
5668
  collisionTest(pos, size=new Vector2, object)
5517
5669
  {
5670
+ ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
5671
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
5672
+
5518
5673
  // transform to local layer space
5519
5674
  const posX = pos.x - this.pos.x;
5520
5675
  const posY = pos.y - this.pos.y;
@@ -5545,6 +5700,9 @@ class TileCollisionLayer extends TileLayer
5545
5700
  * @return {Vector2} */
5546
5701
  collisionRaycast(posStart, posEnd, object)
5547
5702
  {
5703
+ ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
5704
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
5705
+
5548
5706
  // transform to local layer space
5549
5707
  const posStartX = posStart.x - this.pos.x;
5550
5708
  const posStartY = posStart.y - this.pos.y;
@@ -5594,9 +5752,16 @@ class TileCollisionLayer extends TileLayer
5594
5752
  * LittleJS Particle System
5595
5753
  */
5596
5754
 
5755
+ /**
5756
+ * @callback ParticleCallbackFunction - Function that processes a particle
5757
+ * @param {Particle} particle
5758
+ * @memberof Engine
5759
+ */
5760
+
5597
5761
  /**
5598
5762
  * Particle Emitter - Spawns particles with the given settings
5599
5763
  * @extends EngineObject
5764
+ * @memberof Engine
5600
5765
  * @example
5601
5766
  * // create a particle emitter
5602
5767
  * let pos = vec2(2,3);
@@ -5621,10 +5786,10 @@ class ParticleEmitter extends EngineObject
5621
5786
  * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
5622
5787
  * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5623
5788
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
5624
- * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
5625
- * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
5626
- * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
5627
- * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
5789
+ * @param {Color} [colorStartA=WHITE] - Color at start of life 1, randomized between start colors
5790
+ * @param {Color} [colorStartB=WHITE] - Color at start of life 2, randomized between start colors
5791
+ * @param {Color} [colorEndA=CLEAR_WHITE] - Color at end of life 1, randomized between end colors
5792
+ * @param {Color} [colorEndB=CLEAR_WHITE] - Color at end of life 2, randomized between end colors
5628
5793
  * @param {number} [particleTime] - How long particles live
5629
5794
  * @param {number} [sizeStart] - How big are particles at start
5630
5795
  * @param {number} [sizeEnd] - How big are particles at end
@@ -5651,10 +5816,10 @@ class ParticleEmitter extends EngineObject
5651
5816
  emitRate = 100,
5652
5817
  emitConeAngle = PI,
5653
5818
  tileInfo,
5654
- colorStartA = new Color,
5655
- colorStartB = new Color,
5656
- colorEndA = new Color(1,1,1,0),
5657
- colorEndB = new Color(1,1,1,0),
5819
+ colorStartA = WHITE,
5820
+ colorStartB = WHITE,
5821
+ colorEndA = CLEAR_WHITE,
5822
+ colorEndB = CLEAR_WHITE,
5658
5823
  particleTime = .5,
5659
5824
  sizeStart = .1,
5660
5825
  sizeEnd = 1,
@@ -5677,7 +5842,8 @@ class ParticleEmitter extends EngineObject
5677
5842
 
5678
5843
  // emitter settings
5679
5844
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5680
- this.emitSize = emitSize
5845
+ this.emitSize = emitSize instanceof Vector2 ?
5846
+ emitSize.copy() : emitSize;
5681
5847
  /** @property {number} - How long to stay alive (0 is forever) */
5682
5848
  this.emitTime = emitTime;
5683
5849
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -5687,13 +5853,13 @@ class ParticleEmitter extends EngineObject
5687
5853
 
5688
5854
  // color settings
5689
5855
  /** @property {Color} - Color at start of life 1, randomized between start colors */
5690
- this.colorStartA = colorStartA;
5856
+ this.colorStartA = colorStartA.copy();
5691
5857
  /** @property {Color} - Color at start of life 2, randomized between start colors */
5692
- this.colorStartB = colorStartB;
5858
+ this.colorStartB = colorStartB.copy();
5693
5859
  /** @property {Color} - Color at end of life 1, randomized between end colors */
5694
- this.colorEndA = colorEndA;
5860
+ this.colorEndA = colorEndA.copy();
5695
5861
  /** @property {Color} - Color at end of life 2, randomized between end colors */
5696
- this.colorEndB = colorEndB;
5862
+ this.colorEndB = colorEndB.copy();
5697
5863
  /** @property {boolean} - Should color be randomized linearly or across each component */
5698
5864
  this.randomColorLinear = randomColorLinear;
5699
5865
 
@@ -5728,9 +5894,9 @@ class ParticleEmitter extends EngineObject
5728
5894
  this.localSpace = localSpace;
5729
5895
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5730
5896
  this.trailScale = 0;
5731
- /** @property {Function} - Callback when particle is destroyed */
5897
+ /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
5732
5898
  this.particleDestroyCallback = undefined;
5733
- /** @property {Function} - Callback when particle is created */
5899
+ /** @property {ParticleCallbackFunction} - Callback when particle is created */
5734
5900
  this.particleCreateCallback = undefined;
5735
5901
  /** @property {number} - Track particle emit time */
5736
5902
  this.emitTimeBuffer = 0;
@@ -5831,6 +5997,7 @@ class ParticleEmitter extends EngineObject
5831
5997
  /**
5832
5998
  * Particle Object - Created automatically by Particle Emitters
5833
5999
  * @extends EngineObject
6000
+ * @memberof Engine
5834
6001
  */
5835
6002
  class Particle extends EngineObject
5836
6003
  {
@@ -5849,7 +6016,7 @@ class Particle extends EngineObject
5849
6016
  * @param {boolean} additive - Does it use additive blend mode
5850
6017
  * @param {number} trailScale - If a trail, how long to make it
5851
6018
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
5852
- * @param {Function} [destroyCallback] - Callback when particle dies
6019
+ * @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
5853
6020
  */
5854
6021
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
5855
6022
  )
@@ -5858,14 +6025,14 @@ class Particle extends EngineObject
5858
6025
 
5859
6026
  /** @property {Color} - Color at start of life */
5860
6027
  this.colorStart = colorStart;
5861
- /** @property {Color} - Calculated change in color */
5862
- this.colorEndDelta = colorEnd.subtract(colorStart);
6028
+ /** @property {Color} - Color at end of life */
6029
+ this.colorEnd = colorEnd;
5863
6030
  /** @property {number} - How long to live for */
5864
6031
  this.lifeTime = lifeTime;
5865
6032
  /** @property {number} - Size at start of life */
5866
6033
  this.sizeStart = sizeStart;
5867
- /** @property {number} - Calculated change in size */
5868
- this.sizeEndDelta = sizeEnd - sizeStart;
6034
+ /** @property {number} - Size at end of life */
6035
+ this.sizeEnd = sizeEnd;
5869
6036
  /** @property {number} - How quick to fade in/out */
5870
6037
  this.fadeRate = fadeRate;
5871
6038
  /** @property {boolean} - Is it additive */
@@ -5874,7 +6041,7 @@ class Particle extends EngineObject
5874
6041
  this.trailScale = trailScale;
5875
6042
  /** @property {ParticleEmitter} - Parent emitter if local space */
5876
6043
  this.localSpaceEmitter = localSpaceEmitter;
5877
- /** @property {Function} - Called when particle dies */
6044
+ /** @property {ParticleCallbackFunction} - Called when particle dies */
5878
6045
  this.destroyCallback = destroyCallback;
5879
6046
  // particles do not clamp speed by default
5880
6047
  this.clampSpeed = false;
@@ -5901,54 +6068,58 @@ class Particle extends EngineObject
5901
6068
  /** Render the particle, automatically called each frame, sorted by renderOrder */
5902
6069
  render()
5903
6070
  {
5904
- // modulate size and color
5905
- const p = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1;
5906
- const radius = this.sizeStart + p * this.sizeEndDelta;
5907
- const size = vec2(radius);
6071
+ // lerp color and size
6072
+ const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6073
+ const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6074
+ this.size.x = this.size.y = radius;
6075
+ this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6076
+ this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6077
+ this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
6078
+ this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
6079
+
6080
+ // fade alpha
5908
6081
  const fadeRate = this.fadeRate/2;
5909
- const color = new Color(
5910
- this.colorStart.r + p * this.colorEndDelta.r,
5911
- this.colorStart.g + p * this.colorEndDelta.g,
5912
- this.colorStart.b + p * this.colorEndDelta.b,
5913
- (this.colorStart.a + p * this.colorEndDelta.a) *
5914
- (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
6082
+ this.color.a *= p1 < fadeRate ? p1/fadeRate :
6083
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
5915
6084
 
5916
6085
  // draw the particle
5917
6086
  this.additive && setBlendMode(true);
5918
6087
 
6088
+ // update the position and angle for drawing
5919
6089
  let pos = this.pos, angle = this.angle;
5920
6090
  if (this.localSpaceEmitter)
5921
6091
  {
5922
6092
  // in local space of emitter
5923
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6093
+ const a = this.localSpaceEmitter.angle;
6094
+ const c = Math.cos(a), s = Math.sin(a);
6095
+ pos = this.localSpaceEmitter.pos.add(
6096
+ new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
5924
6097
  angle += this.localSpaceEmitter.angle;
5925
6098
  }
5926
6099
  if (this.trailScale)
5927
6100
  {
5928
6101
  // trail style particles
5929
- let velocity = this.velocity;
5930
- if (this.localSpaceEmitter)
5931
- velocity = velocity.rotate(-this.localSpaceEmitter.angle);
5932
- const speed = velocity.length();
6102
+ const direction = this.localSpaceEmitter ?
6103
+ this.velocity.rotate(-this.localSpaceEmitter.angle) :
6104
+ this.velocity;
6105
+ const speed = direction.length();
5933
6106
  if (speed)
5934
6107
  {
5935
- const direction = velocity.scale(1/speed);
6108
+ // stretch in direction of motion
5936
6109
  const trailLength = speed * this.trailScale;
5937
- size.y = max(size.x, trailLength);
5938
- angle = direction.angle();
5939
- drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileInfo, color, angle, this.mirror);
6110
+ this.size.y = max(this.size.x, trailLength);
6111
+ angle = Math.atan2(direction.x, direction.y);
6112
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
5940
6113
  }
5941
6114
  }
5942
6115
  else
5943
- drawTile(pos, size, this.tileInfo, color, angle, this.mirror);
6116
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
5944
6117
  this.additive && setBlendMode();
5945
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
6118
+ debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
5946
6119
 
5947
- if (p === 1)
6120
+ if (p1 === 1)
5948
6121
  {
5949
- // destroy particle when it's time runs out
5950
- this.color = color;
5951
- this.size = size;
6122
+ // destroy particle when its time runs out
5952
6123
  this.destroyCallback && this.destroyCallback(this);
5953
6124
  this.destroyed = 1;
5954
6125
  }
@@ -6013,8 +6184,14 @@ function medalsInit(saveName)
6013
6184
  }
6014
6185
  }
6015
6186
 
6187
+ /**
6188
+ * @callback MedalCallbackFunction - Function that processes a medal
6189
+ * @param {Medal} medal
6190
+ * @memberof Medals
6191
+ */
6192
+
6016
6193
  /** Calls a function for each medal
6017
- * @param {Function} callback
6194
+ * @param {MedalCallbackFunction} callback
6018
6195
  * @memberof Medals */
6019
6196
  function medalsForEach(callback)
6020
6197
  { Object.values(medals).forEach(medal=>callback(medal)); }
@@ -6023,6 +6200,7 @@ function medalsForEach(callback)
6023
6200
 
6024
6201
  /**
6025
6202
  * Medal - Tracks an unlockable medal
6203
+ * @memberof Medals
6026
6204
  * @example
6027
6205
  * // create a medal
6028
6206
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
@@ -6091,11 +6269,12 @@ class Medal
6091
6269
  const height = medalDisplaySize.y;
6092
6270
  const x = overlayCanvas.width - width;
6093
6271
  const y = -height*hidePercent;
6272
+ const backgroundColor = hsl(0,0,.9);
6094
6273
 
6095
6274
  // draw containing rect and clip to that region
6096
6275
  context.save();
6097
6276
  context.beginPath();
6098
- context.fillStyle = new Color(.9,.9,.9).toString();
6277
+ context.fillStyle = backgroundColor.toString();
6099
6278
  context.strokeStyle = BLACK.toString();
6100
6279
  context.lineWidth = 3;
6101
6280
  context.rect(x, y, width, height);
@@ -6897,24 +7076,25 @@ function glPolyStrip(points)
6897
7076
  return strip;
6898
7077
  }
6899
7078
  /**
6900
- * LittleJS Newgrounds API
7079
+ * LittleJS Newgrounds Plugin
6901
7080
  * - NewgroundsMedal extends Medal with Newgrounds API functionality
6902
- * - Call new NewgroundsPlugin() to setup Newgrounds
7081
+ * - Call new NewgroundsPlugin(app_id) to setup Newgrounds
6903
7082
  * - Uses CryptoJS for encryption if optional cipher is provided
7083
+ * - provides functions to interact with medals scoreboards
6904
7084
  * - Keeps connection alive and logs views
6905
- * - Functions to interact with scoreboards
6906
- * - Functions to unlock medals
7085
+ * @namespace Newgrounds
6907
7086
  */
6908
7087
 
6909
7088
  /** Global Newgrounds object
6910
7089
  * @type {NewgroundsPlugin}
6911
- * @memberof Medal */
7090
+ * @memberof Newgrounds */
6912
7091
  let newgrounds;
6913
7092
 
6914
7093
  ///////////////////////////////////////////////////////////////////////////////
6915
7094
  /**
6916
7095
  * Newgrounds medal auto unlocks in newgrounds API
6917
7096
  * @extends Medal
7097
+ * @memberof Newgrounds
6918
7098
  */
6919
7099
  class NewgroundsMedal extends Medal
6920
7100
  {
@@ -6939,6 +7119,7 @@ class NewgroundsMedal extends Medal
6939
7119
  ///////////////////////////////////////////////////////////////////////////////
6940
7120
  /**
6941
7121
  * Newgrounds API object
7122
+ * @memberof Newgrounds
6942
7123
  */
6943
7124
  class NewgroundsPlugin
6944
7125
  {
@@ -6972,7 +7153,7 @@ class NewgroundsPlugin
6972
7153
  // get medals
6973
7154
  const medalsResult = this.call('Medal.getList');
6974
7155
  this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
6975
- debugMedals && console.log(this.medals);
7156
+ debugMedals && LOG(this.medals);
6976
7157
  for (const newgroundsMedal of this.medals)
6977
7158
  {
6978
7159
  const medal = medals[newgroundsMedal['id']];
@@ -6995,7 +7176,7 @@ class NewgroundsPlugin
6995
7176
  // get scoreboards
6996
7177
  const scoreboardResult = this.call('ScoreBoard.getBoards');
6997
7178
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
6998
- debugMedals && console.log(this.scoreboards);
7179
+ debugMedals && LOG(this.scoreboards);
6999
7180
 
7000
7181
  // keep the session alive with a ping every minute
7001
7182
  const keepAliveMS = 60 * 1e3;
@@ -7064,29 +7245,32 @@ class NewgroundsPlugin
7064
7245
  try { xmlHttp.send(formData); }
7065
7246
  catch(e)
7066
7247
  {
7067
- debugMedals && console.log('newgrounds call failed', e);
7248
+ debugMedals && LOG('newgrounds call failed', e);
7068
7249
  return;
7069
7250
  }
7070
- debugMedals && console.log(xmlHttp.responseText);
7251
+ debugMedals && LOG(xmlHttp.responseText);
7071
7252
  return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
7072
7253
  }
7073
7254
  }
7074
7255
  /**
7075
7256
  * LittleJS Post Processing Plugin
7076
7257
  * - Supports shadertoy style post processing shaders
7077
- * - call new new PostProcessPlugin() to setup post processing
7258
+ * - call new PostProcessPlugin() to setup post processing
7078
7259
  * - can be enabled to pass other canvases through a final shader
7260
+ * @namespace PostProcess
7079
7261
  */
7080
7262
 
7081
7263
  ///////////////////////////////////////////////////////////////////////////////
7082
7264
 
7083
7265
  /** Global Post Process plugin object
7084
- * @type {PostProcessPlugin} */
7266
+ * @type {PostProcessPlugin}
7267
+ * @memberof PostProcess */
7085
7268
  let postProcess;
7086
7269
 
7087
7270
  /////////////////////////////////////////////////////////////////////////
7088
7271
  /**
7089
7272
  * UI System Global Object
7273
+ * @memberof PostProcess
7090
7274
  */
7091
7275
  class PostProcessPlugin
7092
7276
  {
@@ -7194,12 +7378,15 @@ class PostProcessPlugin
7194
7378
  }
7195
7379
  /**
7196
7380
  * LittleJS ZzFXM Plugin
7381
+ * @namespace ZzFXM
7197
7382
  */
7198
7383
 
7199
7384
  /**
7200
7385
  * Music Object - Stores a zzfx music track for later use
7201
7386
  *
7202
7387
  * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
7388
+ * @extends Sound
7389
+ * @memberof ZzFXM
7203
7390
  * @example
7204
7391
  * // create some music
7205
7392
  * const music_example = new Music(
@@ -7258,7 +7445,8 @@ class ZzFXMusic extends Sound
7258
7445
  * @param {Array} patterns - Array of pattern data
7259
7446
  * @param {Array} sequence - Array of pattern indexes
7260
7447
  * @param {number} [BPM] - Playback speed of the song in BPM
7261
- * @return {Array} - Left and right channel sample data */
7448
+ * @return {Array} - Left and right channel sample data
7449
+ * @memberof ZzFXM */
7262
7450
  function zzfxM(instruments, patterns, sequence, BPM = 125)
7263
7451
  {
7264
7452
  let i, j, k;
@@ -7361,17 +7549,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
7361
7549
  * - Buttons
7362
7550
  * - Checkboxes
7363
7551
  * - Images
7552
+ * @namespace UISystem
7364
7553
  */
7365
7554
 
7366
7555
  ///////////////////////////////////////////////////////////////////////////////
7367
7556
 
7368
7557
  /** Global UI system plugin object
7369
- * @type {UISystemPlugin} */
7558
+ * @type {UISystemPlugin}
7559
+ * @memberof UISystem */
7370
7560
  let uiSystem;
7371
7561
 
7372
7562
  ///////////////////////////////////////////////////////////////////////////////
7373
7563
  /**
7374
7564
  * UI System Global Object
7565
+ * @memberof UISystem
7375
7566
  */
7376
7567
  class UISystemPlugin
7377
7568
  {
@@ -7416,10 +7607,12 @@ class UISystemPlugin
7416
7607
  this.uiObjects = [];
7417
7608
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
7418
7609
  this.uiContext = context;
7419
- /** @property {UIObject} - Top most object user is over */
7420
- this.hoverObject = undefined;
7421
7610
  /** @property {UIObject} - Object user is currently interacting with */
7422
7611
  this.activeObject = undefined;
7612
+ /** @property {UIObject} - Top most object user is over */
7613
+ this.hoverObject = undefined;
7614
+ /** @property {UIObject} - Hover object at start of update */
7615
+ this.lastHoverObject = undefined;
7423
7616
 
7424
7617
  engineAddPlugin(uiUpdate, uiRender);
7425
7618
 
@@ -7450,6 +7643,7 @@ class UISystemPlugin
7450
7643
  updateInvisibleObject(o);
7451
7644
  }
7452
7645
  // reset hover object at start of update
7646
+ uiSystem.lastHoverObject = uiSystem.hoverObject;
7453
7647
  uiSystem.hoverObject = undefined;
7454
7648
  for (let i = uiSystem.uiObjects.length; i--;)
7455
7649
  {
@@ -7482,6 +7676,13 @@ class UISystemPlugin
7482
7676
  * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
7483
7677
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
7484
7678
  {
7679
+ ASSERT(isVector2(pos), 'pos must be a vec2');
7680
+ ASSERT(isVector2(size), 'size must be a vec2');
7681
+ ASSERT(isColor(color), 'color must be a color');
7682
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
7683
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
7684
+ ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
7685
+
7485
7686
  const context = uiSystem.uiContext;
7486
7687
  context.fillStyle = color.toString();
7487
7688
  context.beginPath();
@@ -7505,6 +7706,11 @@ class UISystemPlugin
7505
7706
  * @param {Color} [lineColor=uiSystem.defaultLineColor] */
7506
7707
  drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
7507
7708
  {
7709
+ ASSERT(isVector2(posA), 'posA must be a vec2');
7710
+ ASSERT(isVector2(posB), 'posB must be a vec2');
7711
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
7712
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
7713
+
7508
7714
  const context = uiSystem.uiContext;
7509
7715
  context.strokeStyle = lineColor.toString();
7510
7716
  context.lineWidth = lineWidth;
@@ -7540,12 +7746,37 @@ class UISystemPlugin
7540
7746
  {
7541
7747
  drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7542
7748
  }
7749
+
7750
+ /**
7751
+ * @callback DragAndDropCallback - Callback for drag and drop events
7752
+ * @param {DragEvent} event - The drag event
7753
+ * @memberof UISystem
7754
+ */
7755
+
7756
+ /** Setup drag and drop event handlers
7757
+ * Automatically prevents defaults and calls the given functions
7758
+ * @param {DragAndDropCallback} [onDrop] - when a file is dropped
7759
+ * @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
7760
+ * @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
7761
+ * @param {DragAndDropCallback} [onDragOver] - continously when dragging over */
7762
+ setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
7763
+ {
7764
+ function setCallback(callback, listenerType)
7765
+ {
7766
+ function listener(e) { e.preventDefault(); callback && callback(e); }
7767
+ document.addEventListener(listenerType, listener);
7768
+ }
7769
+ setCallback(onDrop, 'drop');
7770
+ setCallback(onDragEnter, 'dragenter');
7771
+ setCallback(onDragLeave, 'dragleave');
7772
+ setCallback(onDragOver, 'dragover');
7773
+ }
7543
7774
  }
7544
7775
 
7545
7776
  ///////////////////////////////////////////////////////////////////////////////
7546
7777
  /**
7547
7778
  * UI Object - Base level object for all UI elements
7548
- */
7779
+ * @memberof UISystem */
7549
7780
  class UIObject
7550
7781
  {
7551
7782
  /** Create a UIObject
@@ -7554,6 +7785,9 @@ class UIObject
7554
7785
  */
7555
7786
  constructor(pos=vec2(), size=vec2())
7556
7787
  {
7788
+ ASSERT(isVector2(pos), 'ui object pos must be a vec2');
7789
+ ASSERT(isVector2(size), 'ui object size must be a vec2');
7790
+
7557
7791
  /** @property {Vector2} - Local position of the object */
7558
7792
  this.localPos = pos.copy();
7559
7793
  /** @property {Vector2} - Screen space position of the object */
@@ -7561,21 +7795,21 @@ class UIObject
7561
7795
  /** @property {Vector2} - Screen space size of the object */
7562
7796
  this.size = size.copy();
7563
7797
  /** @property {Color} - Color of the object */
7564
- this.color = uiSystem.defaultColor;
7798
+ this.color = uiSystem.defaultColor.copy();
7565
7799
  /** @property {Color} - Color of the object when active, uses color if undefined */
7566
7800
  this.activeColor = undefined;
7567
7801
  /** @property {string} - Text for this ui object */
7568
7802
  this.text = undefined;
7569
7803
  /** @property {Color} - Color when disabled */
7570
- this.disabledColor = uiSystem.defaultDisabledColor;
7804
+ this.disabledColor = uiSystem.defaultDisabledColor.copy();
7571
7805
  /** @property {boolean} - Is this object disabled? */
7572
7806
  this.disabled = false;
7573
7807
  /** @property {Color} - Color for text */
7574
- this.textColor = uiSystem.defaultTextColor;
7808
+ this.textColor = uiSystem.defaultTextColor.copy()
7575
7809
  /** @property {Color} - Color used when hovering over the object */
7576
- this.hoverColor = uiSystem.defaultHoverColor;
7810
+ this.hoverColor = uiSystem.defaultHoverColor.copy()
7577
7811
  /** @property {Color} - Color for line drawing */
7578
- this.lineColor = uiSystem.defaultLineColor;
7812
+ this.lineColor = uiSystem.defaultLineColor.copy()
7579
7813
  /** @property {number} - Width for line drawing */
7580
7814
  this.lineWidth = uiSystem.defaultLineWidth;
7581
7815
  /** @property {number} - Corner radius for rounded rects */
@@ -7632,7 +7866,7 @@ class UIObject
7632
7866
  /** Update the object, called automatically by plugin once each frame */
7633
7867
  update()
7634
7868
  {
7635
- const wasHover = this.isHoverObject();
7869
+ const wasHover = uiSystem.lastHoverObject === this;
7636
7870
  const isActive = this.isActiveObject();
7637
7871
  const mouseDown = mouseIsDown(0);
7638
7872
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
@@ -7645,15 +7879,14 @@ class UIObject
7645
7879
  }
7646
7880
  if (this.isHoverObject())
7647
7881
  {
7648
- if (mousePress)
7649
- inputClearKey(0,0,0,1,0); // clear mouse was pressed state
7650
7882
  if (!this.disabled)
7651
7883
  {
7652
7884
  if (mousePress)
7653
7885
  {
7654
7886
  if (this.interactive)
7655
7887
  {
7656
- this.onPress();
7888
+ if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
7889
+ this.onPress();
7657
7890
  if (this.soundPress)
7658
7891
  this.soundPress.play();
7659
7892
  if (uiSystem.activeObject && !isActive)
@@ -7661,13 +7894,15 @@ class UIObject
7661
7894
  uiSystem.activeObject = this;
7662
7895
  }
7663
7896
  }
7664
- if (!mouseDown && uiSystem.activeObject === this && this.interactive)
7897
+ if (!mouseDown && this.isActiveObject() && this.interactive)
7665
7898
  {
7666
7899
  this.onClick();
7667
7900
  if (this.soundClick)
7668
7901
  this.soundClick.play();
7669
7902
  }
7670
7903
  }
7904
+ // clear mouse was pressed state even when disabled
7905
+ mousePress && inputClearKey(0,0,0,1,0);
7671
7906
  }
7672
7907
  if (isActive)
7673
7908
  if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
@@ -7678,6 +7913,7 @@ class UIObject
7678
7913
  uiSystem.activeObject = undefined;
7679
7914
  }
7680
7915
 
7916
+ // call enter/leave events
7681
7917
  if (this.isHoverObject() !== wasHover)
7682
7918
  this.isHoverObject() ? this.onEnter() : this.onLeave();
7683
7919
  }
@@ -7688,7 +7924,7 @@ class UIObject
7688
7924
  if (!this.size.x || !this.size.y) return;
7689
7925
 
7690
7926
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
7691
- const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
7927
+ const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
7692
7928
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7693
7929
  }
7694
7930
 
@@ -7739,6 +7975,7 @@ class UIObject
7739
7975
  /**
7740
7976
  * UIText - A UI object that displays text
7741
7977
  * @extends UIObject
7978
+ * @memberof UISystem
7742
7979
  */
7743
7980
  class UIText extends UIObject
7744
7981
  {
@@ -7753,6 +7990,10 @@ class UIText extends UIObject
7753
7990
  {
7754
7991
  super(pos, size);
7755
7992
 
7993
+ ASSERT(isString(text), 'ui text must be a string');
7994
+ ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
7995
+ ASSERT(isString(font), 'ui text font must be a string');
7996
+
7756
7997
  // set properties
7757
7998
  this.text = text;
7758
7999
  this.align = align;
@@ -7772,6 +8013,7 @@ class UIText extends UIObject
7772
8013
  /**
7773
8014
  * UITile - A UI object that displays a tile image
7774
8015
  * @extends UIObject
8016
+ * @memberof UISystem
7775
8017
  */
7776
8018
  class UITile extends UIObject
7777
8019
  {
@@ -7786,15 +8028,19 @@ class UITile extends UIObject
7786
8028
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
7787
8029
  {
7788
8030
  super(pos, size);
8031
+
8032
+ ASSERT(tileInfo instanceof TileInfo, 'ui tile tileInfo must be a TileInfo');
8033
+ ASSERT(isColor(color), 'ui tile color must be a color');
8034
+ ASSERT(isNumber(angle), 'ui tile angle must be a number');
8035
+
7789
8036
  /** @property {TileInfo} - Tile image to use */
7790
8037
  this.tileInfo = tileInfo;
7791
8038
  /** @property {number} - Angle to rotate in radians */
7792
8039
  this.angle = angle;
7793
8040
  /** @property {boolean} - Should it be mirrored? */
7794
8041
  this.mirror = mirror;
7795
-
7796
8042
  // set properties
7797
- this.color = color;
8043
+ this.color = color.copy();
7798
8044
  }
7799
8045
  render()
7800
8046
  {
@@ -7806,6 +8052,7 @@ class UITile extends UIObject
7806
8052
  /**
7807
8053
  * UIButton - A UI object that acts as a button
7808
8054
  * @extends UIObject
8055
+ * @memberof UISystem
7809
8056
  */
7810
8057
  class UIButton extends UIObject
7811
8058
  {
@@ -7819,9 +8066,12 @@ class UIButton extends UIObject
7819
8066
  {
7820
8067
  super(pos, size);
7821
8068
 
8069
+ ASSERT(isString(text), 'ui button must be a string');
8070
+ ASSERT(isColor(color), 'ui button color must be a color');
8071
+
7822
8072
  // set properties
7823
8073
  this.text = text;
7824
- this.color = color;
8074
+ this.color = color.copy()
7825
8075
  this.interactive = true;
7826
8076
  }
7827
8077
  render()
@@ -7839,6 +8089,7 @@ class UIButton extends UIObject
7839
8089
  /**
7840
8090
  * UICheckbox - A UI object that acts as a checkbox
7841
8091
  * @extends UIObject
8092
+ * @memberof UISystem
7842
8093
  */
7843
8094
  class UICheckbox extends UIObject
7844
8095
  {
@@ -7852,12 +8103,15 @@ class UICheckbox extends UIObject
7852
8103
  constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
7853
8104
  {
7854
8105
  super(pos, size);
8106
+
8107
+ ASSERT(isString(text), 'ui checkbox must be a string');
8108
+ ASSERT(isColor(color), 'ui checkbox color must be a color');
8109
+
7855
8110
  /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
7856
8111
  this.checked = checked;
7857
-
7858
8112
  // set properties
7859
8113
  this.text = text;
7860
- this.color = color;
8114
+ this.color = color.copy();
7861
8115
  this.interactive = true;
7862
8116
  }
7863
8117
  onClick()
@@ -7889,6 +8143,7 @@ class UICheckbox extends UIObject
7889
8143
  /**
7890
8144
  * UIScrollbar - A UI object that acts as a scrollbar
7891
8145
  * @extends UIObject
8146
+ * @memberof UISystem
7892
8147
  */
7893
8148
  class UIScrollbar extends UIObject
7894
8149
  {
@@ -7904,14 +8159,19 @@ class UIScrollbar extends UIObject
7904
8159
  {
7905
8160
  super(pos, size);
7906
8161
 
8162
+ ASSERT(isNumber(value), 'ui scrollbar value must be a number');
8163
+ ASSERT(isString(text), 'ui scrollbar must be a string');
8164
+ ASSERT(isColor(color), 'ui scrollbar color must be a color');
8165
+ ASSERT(isColor(handleColor), 'ui scrollbar handleColor must be a color');
8166
+
7907
8167
  /** @property {number} - Current percentage value of this scrollbar 0-1 */
7908
8168
  this.value = value;
7909
8169
  /** @property {Color} - Color for the handle part of the scrollbar */
7910
- this.handleColor = handleColor;
8170
+ this.handleColor = handleColor.copy();
7911
8171
 
7912
8172
  // set properties
7913
8173
  this.text = text;
7914
- this.color = color;
8174
+ this.color = color.copy();
7915
8175
  this.interactive = true;
7916
8176
  }
7917
8177
  update()
@@ -7919,29 +8179,43 @@ class UIScrollbar extends UIObject
7919
8179
  super.update();
7920
8180
  if (this.isActiveObject() && this.interactive)
7921
8181
  {
8182
+ // handle horizontal or vertical scrollbar
8183
+ const isHorizontal = this.size.x > this.size.y;
8184
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8185
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8186
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8187
+
7922
8188
  // check if value changed
7923
- const handleSize = vec2(this.size.y);
7924
- const handleWidth = this.size.x - handleSize.x;
7925
- const p1 = this.pos.x - handleWidth/2;
7926
- const p2 = this.pos.x + handleWidth/2;
8189
+ const handleWidth = barSize - handleSize;
8190
+ const p1 = centerPos - handleWidth/2;
8191
+ const p2 = centerPos + handleWidth/2;
7927
8192
  const oldValue = this.value;
7928
- this.value = percent(mousePosScreen.x, p1, p2);
8193
+ this.value = isHorizontal ?
8194
+ percent(mousePosScreen.x, p1, p2) :
8195
+ percent(mousePosScreen.y, p2, p1);
7929
8196
  this.value === oldValue || this.onChange();
7930
8197
  }
7931
8198
  }
7932
8199
  render()
7933
8200
  {
7934
8201
  super.render();
7935
-
8202
+
8203
+ // handle horizontal or vertical scrollbar
8204
+ const isHorizontal = this.size.x > this.size.y;
8205
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8206
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8207
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8208
+
7936
8209
  // draw the scrollbar handle
7937
- const handleSize = vec2(this.size.y);
7938
- const handleWidth = this.size.x - handleSize.x;
7939
- const p1 = this.pos.x - handleWidth/2;
7940
- const p2 = this.pos.x + handleWidth/2;
7941
- const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
8210
+ const handleWidth = barSize - handleSize;
8211
+ const p1 = centerPos - handleWidth/2;
8212
+ const p2 = centerPos + handleWidth/2;
8213
+ const handlePos = isHorizontal ?
8214
+ vec2(lerp(p1, p2, this.value), this.pos.y) :
8215
+ vec2(this.pos.x, lerp(p2, p1, this.value))
7942
8216
  const handleColor = this.disabled ? this.disabledColor :
7943
8217
  this.interactive && this.isActiveObject() ? this.color : this.handleColor;
7944
- uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8218
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
7945
8219
 
7946
8220
  // draw the text scaled to fit on the scrollbar
7947
8221
  const textSize = this.getTextSize();
@@ -7988,6 +8262,7 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
7988
8262
  * - Each object has a Box2D body which can have multiple fixtures and joints
7989
8263
  * - Provides interface for Box2D body and fixture functions
7990
8264
  * @extends EngineObject
8265
+ * @memberof Box2D
7991
8266
  */
7992
8267
  class Box2dObject extends EngineObject
7993
8268
  {
@@ -7999,7 +8274,7 @@ class Box2dObject extends EngineObject
7999
8274
  * @param {Color} [color]
8000
8275
  * @param {number} [bodyType]
8001
8276
  * @param {number} [renderOrder] */
8002
- constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8277
+ constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8003
8278
  {
8004
8279
  super(pos, size, tileInfo, angle, color, renderOrder);
8005
8280
 
@@ -8013,7 +8288,7 @@ class Box2dObject extends EngineObject
8013
8288
  this.lineColor = BLACK;
8014
8289
  }
8015
8290
 
8016
- /** Destroy this object and it's physics body */
8291
+ /** Destroy this object and its physics body */
8017
8292
  destroy()
8018
8293
  {
8019
8294
  // destroy physics body, fixtures, and joints
@@ -8497,6 +8772,7 @@ class Box2dRaycastResult
8497
8772
  * Box2D Joint
8498
8773
  * - Base class for Box2D joints
8499
8774
  * - A joint is used to connect objects together
8775
+ * @memberof Box2D
8500
8776
  */
8501
8777
  class Box2dJoint
8502
8778
  {
@@ -8552,6 +8828,7 @@ class Box2dJoint
8552
8828
  * - This a soft constraint with a max force
8553
8829
  * - This allows the constraint to stretch and without applying huge forces
8554
8830
  * @extends Box2dJoint
8831
+ * @memberof Box2D
8555
8832
  */
8556
8833
  class Box2dTargetJoint extends Box2dJoint
8557
8834
  {
@@ -8601,6 +8878,7 @@ class Box2dTargetJoint extends Box2dJoint
8601
8878
  * - Constrains two points on two objects to remain at a fixed distance
8602
8879
  * - You can view this as a massless, rigid rod
8603
8880
  * @extends Box2dJoint
8881
+ * @memberof Box2D
8604
8882
  */
8605
8883
  class Box2dDistanceJoint extends Box2dJoint
8606
8884
  {
@@ -8664,6 +8942,7 @@ class Box2dDistanceJoint extends Box2dJoint
8664
8942
  * Box2D Pin Joint
8665
8943
  * - Pins two objects together at a point
8666
8944
  * @extends Box2dDistanceJoint
8945
+ * @memberof Box2D
8667
8946
  */
8668
8947
  class Box2dPinJoint extends Box2dDistanceJoint
8669
8948
  {
@@ -8683,6 +8962,7 @@ class Box2dPinJoint extends Box2dDistanceJoint
8683
8962
  * Box2D Rope Joint
8684
8963
  * - Enforces a maximum distance between two points on two objects
8685
8964
  * @extends Box2dJoint
8965
+ * @memberof Box2D
8686
8966
  */
8687
8967
  class Box2dRopeJoint extends Box2dJoint
8688
8968
  {
@@ -8735,6 +9015,7 @@ class Box2dRopeJoint extends Box2dJoint
8735
9015
  * - You can use a motor to drive the relative rotation about the shared point
8736
9016
  * - A maximum motor torque is provided so that infinite forces are not generated
8737
9017
  * @extends Box2dJoint
9018
+ * @memberof Box2D
8738
9019
  */
8739
9020
  class Box2dRevoluteJoint extends Box2dJoint
8740
9021
  {
@@ -8836,6 +9117,7 @@ class Box2dRevoluteJoint extends Box2dJoint
8836
9117
  * - Either joint can be a revolute or prismatic joint
8837
9118
  * - You specify a gear ratio to bind the motions together
8838
9119
  * @extends Box2dJoint
9120
+ * @memberof Box2D
8839
9121
  */
8840
9122
  class Box2dGearJoint extends Box2dJoint
8841
9123
  {
@@ -8884,6 +9166,7 @@ class Box2dGearJoint extends Box2dJoint
8884
9166
  * - You can use a joint limit to restrict the range of motion
8885
9167
  * - You can use a joint motor to drive the motion or to model joint friction
8886
9168
  * @extends Box2dJoint
9169
+ * @memberof Box2D
8887
9170
  */
8888
9171
  class Box2dPrismaticJoint extends Box2dJoint
8889
9172
  {
@@ -8993,6 +9276,7 @@ class Box2dPrismaticJoint extends Box2dJoint
8993
9276
  * - You can use a joint motor to drive the motion or to model joint friction
8994
9277
  * - This joint is designed for vehicle suspensions
8995
9278
  * @extends Box2dJoint
9279
+ * @memberof Box2D
8996
9280
  */
8997
9281
  class Box2dWheelJoint extends Box2dJoint
8998
9282
  {
@@ -9088,6 +9372,7 @@ class Box2dWheelJoint extends Box2dJoint
9088
9372
  * Box2D Weld Joint
9089
9373
  * - Glues two objects together
9090
9374
  * @extends Box2dJoint
9375
+ * @memberof Box2D
9091
9376
  */
9092
9377
  class Box2dWeldJoint extends Box2dJoint
9093
9378
  {
@@ -9146,6 +9431,7 @@ class Box2dWeldJoint extends Box2dJoint
9146
9431
  * - Used to apply top-down friction
9147
9432
  * - Provides 2D translational friction and angular friction
9148
9433
  * @extends Box2dJoint
9434
+ * @memberof Box2D
9149
9435
  */
9150
9436
  class Box2dFrictionJoint extends Box2dJoint
9151
9437
  {
@@ -9200,6 +9486,7 @@ class Box2dFrictionJoint extends Box2dJoint
9200
9486
  * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
9201
9487
  * - The force transmitted is scaled by the ratio
9202
9488
  * @extends Box2dJoint
9489
+ * @memberof Box2D
9203
9490
  */
9204
9491
  class Box2dPulleyJoint extends Box2dJoint
9205
9492
  {
@@ -9267,6 +9554,7 @@ class Box2dPulleyJoint extends Box2dJoint
9267
9554
  * - Controls the relative motion between two objects
9268
9555
  * - Typical usage is to control the movement of a object with respect to the ground
9269
9556
  * @extends Box2dJoint
9557
+ * @memberof Box2D
9270
9558
  */
9271
9559
  class Box2dMotorJoint extends Box2dJoint
9272
9560
  {
@@ -9330,6 +9618,7 @@ class Box2dMotorJoint extends Box2dJoint
9330
9618
  /**
9331
9619
  * Box2D Global Object
9332
9620
  * - Wraps Box2d world and provides global functions
9621
+ * @memberof Box2D
9333
9622
  */
9334
9623
  class Box2dPlugin
9335
9624
  {
@@ -9664,7 +9953,7 @@ async function box2dInit()
9664
9953
  }
9665
9954
  function box2dRender()
9666
9955
  {
9667
- if (box2dDebug || debugPhysics && debugOverlay)
9956
+ if (box2dDebug || debugPhysics)
9668
9957
  box2d.world.DrawDebugData();
9669
9958
  }
9670
9959