littlejsengine 1.14.11 → 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 (74) hide show
  1. package/dist/littlejs.d.ts +252 -109
  2. package/dist/littlejs.esm.js +559 -252
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +557 -251
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +482 -206
  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 +10 -7
  15. package/examples/module/game.js +3 -3
  16. package/examples/platformer/gameEffects.js +4 -4
  17. package/examples/platformer/gameLevel.js +1 -1
  18. package/examples/platformer/gameObjects.js +4 -4
  19. package/examples/puzzle/game.js +1 -1
  20. package/examples/shorts/animation.js +1 -1
  21. package/examples/shorts/base.html +1 -1
  22. package/examples/shorts/blending.js +8 -14
  23. package/examples/shorts/box2d.js +7 -3
  24. package/examples/shorts/box2dCar.js +2 -1
  25. package/examples/shorts/empty.js +30 -0
  26. package/examples/shorts/helloWorld.js +1 -1
  27. package/examples/shorts/hillGlideGame.js +10 -4
  28. package/examples/shorts/landerGame.js +11 -8
  29. package/examples/shorts/medals.js +4 -4
  30. package/examples/shorts/music.js +29 -56
  31. package/examples/shorts/musicPlayer.js +134 -0
  32. package/examples/shorts/nineSlice.js +34 -15
  33. package/examples/shorts/parallax.js +4 -3
  34. package/examples/shorts/particles.js +15 -15
  35. package/examples/shorts/piano.js +15 -21
  36. package/examples/shorts/pongGame.js +6 -4
  37. package/examples/shorts/raycasting.js +9 -4
  38. package/examples/shorts/sequencer.js +122 -0
  39. package/examples/shorts/shapes.js +7 -4
  40. package/examples/shorts/slidingPuzzle.js +4 -2
  41. package/examples/shorts/sound.js +18 -9
  42. package/examples/shorts/spaceGame.js +12 -8
  43. package/examples/shorts/spriteAtlas.js +7 -7
  44. package/examples/shorts/starfield.js +1 -1
  45. package/examples/shorts/systemFont.js +2 -2
  46. package/examples/shorts/texture.js +2 -2
  47. package/examples/shorts/tileLayer.js +10 -10
  48. package/examples/shorts/tiltedView.js +14 -3
  49. package/examples/shorts/timers.js +10 -0
  50. package/examples/shorts/topDown.js +4 -1
  51. package/examples/shorts/uiSystem.js +8 -5
  52. package/examples/starter/game.js +3 -3
  53. package/examples/starter/index.html +2 -2
  54. package/examples/typescript/game.js +3 -3
  55. package/examples/typescript/game.ts +3 -3
  56. package/examples/uiSystem/game.js +3 -3
  57. package/package.json +4 -2
  58. package/plugins/box2d.js +17 -2
  59. package/plugins/newgrounds.js +7 -5
  60. package/plugins/postProcess.js +5 -2
  61. package/plugins/uiSystem.js +120 -34
  62. package/plugins/zzfxm.js +5 -1
  63. package/reference.md +1 -1
  64. package/src/engine.js +35 -12
  65. package/src/engineAudio.js +31 -14
  66. package/src/engineDebug.js +75 -45
  67. package/src/engineDraw.js +60 -31
  68. package/src/engineExport.js +2 -1
  69. package/src/engineMedals.js +10 -2
  70. package/src/engineObject.js +14 -10
  71. package/src/engineParticles.js +59 -46
  72. package/src/engineSettings.js +7 -7
  73. package/src/engineTileLayer.js +45 -20
  74. package/src/engineUtilities.js +67 -20
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.11';
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,12 +116,22 @@ 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
@@ -368,6 +383,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
368
383
  const promises = imageSources.map((src, textureIndex)=>
369
384
  new Promise(resolve =>
370
385
  {
386
+ ASSERT(isString(src), 'imageSources must be an array of strings');
387
+
371
388
  const image = new Image;
372
389
  image.onerror = image.onload = ()=>
373
390
  {
@@ -399,7 +416,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
399
416
  promises.push(new Promise(resolve =>
400
417
  {
401
418
  let t = 0;
402
- LOG(`${engineName} Engine v${engineVersion}`);
419
+ console.log(`${engineName} Engine v${engineVersion}`);
403
420
  updateSplash();
404
421
  function updateSplash()
405
422
  {
@@ -483,10 +500,16 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
483
500
  return collectedObjects;
484
501
  }
485
502
 
503
+ /**
504
+ * @callback ObjectCallbackFunction - Function that processes an object
505
+ * @param {EngineObject} uiObjects
506
+ * @memberof Engine
507
+ */
508
+
486
509
  /** Triggers a callback for each object within a given area
487
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
488
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
489
- * @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
490
513
  * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
491
514
  * @memberof Engine */
492
515
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
@@ -727,10 +750,16 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
727
750
  // Debug helper functions
728
751
 
729
752
  /** Asserts if the expression is false, does nothing in release builds
753
+ * Halts execution if the assert fails and throws an error
730
754
  * @param {boolean} assert
731
755
  * @param {...Object} [output] - error message output
732
756
  * @memberof Debug */
733
- function ASSERT(assert, ...output) { console.assert(assert, ...output); }
757
+ function ASSERT(assert, ...output)
758
+ {
759
+ if (assert) return;
760
+ console.assert(assert, ...output)
761
+ throw new Error('Assert failed!'); // halt execution
762
+ }
734
763
 
735
764
  /** Log to console if debug is enabled, does nothing in release builds
736
765
  * @param {...Object} [output] - message output
@@ -740,64 +769,72 @@ function LOG(...output) { console.log(...output); }
740
769
  /** Draw a debug rectangle in world space
741
770
  * @param {Vector2} pos
742
771
  * @param {Vector2} [size=Vector2()]
743
- * @param {string} [color]
744
- * @param {number} [time]
745
- * @param {number} [angle]
772
+ * @param {Color|string} [color]
773
+ * @param {number} [time]
774
+ * @param {number} [angle]
746
775
  * @param {boolean} [fill]
747
776
  * @memberof Debug */
748
- function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
777
+ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
749
778
  {
750
779
  if (typeof size === 'number')
751
780
  size = vec2(size); // allow passing in floats
752
- ASSERT(typeof color === 'string', 'pass in css color strings');
753
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
781
+ if (isColor(color))
782
+ color = color.toString();
783
+ pos = pos.copy();
784
+ size = size.copy();
785
+ const timer = new Timer(time);
786
+ debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill});
754
787
  }
755
788
 
756
789
  /** Draw a debug poly in world space
757
790
  * @param {Vector2} pos
758
791
  * @param {Array<Vector2>} points
759
- * @param {string} [color]
760
- * @param {number} [time]
761
- * @param {number} [angle]
792
+ * @param {Color|string} [color]
793
+ * @param {number} [time]
794
+ * @param {number} [angle]
762
795
  * @param {boolean} [fill]
763
796
  * @memberof Debug */
764
- function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
797
+ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
765
798
  {
766
- ASSERT(typeof color === 'string', 'pass in css color strings');
767
- debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
799
+ if (isColor(color))
800
+ color = color.toString();
801
+ pos = pos.copy();
802
+ points = points.map(p=>p.copy());
803
+ const timer = new Timer(time);
804
+ debugPrimitives.push({pos, points, color, timer, angle, fill});
768
805
  }
769
806
 
770
807
  /** Draw a debug circle in world space
771
808
  * @param {Vector2} pos
772
- * @param {number} [size] - diameter
773
- * @param {string} [color]
774
- * @param {number} [time]
809
+ * @param {number} [size] - diameter
810
+ * @param {Color|string} [color]
811
+ * @param {number} [time]
775
812
  * @param {boolean} [fill]
776
813
  * @memberof Debug */
777
- function debugCircle(pos, size=0, color='#fff', time=0, fill=false)
814
+ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
778
815
  {
779
- ASSERT(typeof color === 'string', 'pass in css color strings');
780
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle:0, fill});
816
+ if (isColor(color))
817
+ color = color.toString();
818
+ pos = pos.copy();
819
+ const timer = new Timer(time);
820
+ debugPrimitives.push({pos, size, color, timer, angle:0, fill});
781
821
  }
782
822
 
783
823
  /** Draw a debug point in world space
784
824
  * @param {Vector2} pos
785
- * @param {string} [color]
786
- * @param {number} [time]
787
- * @param {number} [angle]
825
+ * @param {Color|string} [color]
826
+ * @param {number} [time]
827
+ * @param {number} [angle]
788
828
  * @memberof Debug */
789
829
  function debugPoint(pos, color, time, angle)
790
- {
791
- ASSERT(typeof color === 'string', 'pass in css color strings');
792
- debugRect(pos, undefined, color, time, angle);
793
- }
830
+ { debugRect(pos, undefined, color, time, angle); }
794
831
 
795
832
  /** Draw a debug line in world space
796
833
  * @param {Vector2} posA
797
834
  * @param {Vector2} posB
798
- * @param {string} [color]
799
- * @param {number} [width]
800
- * @param {number} [time]
835
+ * @param {Color|string} [color]
836
+ * @param {number} [width]
837
+ * @param {number} [time]
801
838
  * @memberof Debug */
802
839
  function debugLine(posA, posB, color, width=.1, time)
803
840
  {
@@ -811,7 +848,7 @@ function debugLine(posA, posB, color, width=.1, time)
811
848
  * @param {Vector2} sizeA
812
849
  * @param {Vector2} posB
813
850
  * @param {Vector2} sizeB
814
- * @param {string} [color]
851
+ * @param {Color|string} [color]
815
852
  * @memberof Debug */
816
853
  function debugOverlap(posA, sizeA, posB, sizeB, color)
817
854
  {
@@ -827,18 +864,21 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
827
864
  }
828
865
 
829
866
  /** Draw a debug axis aligned bounding box in world space
830
- * @param {string} text
867
+ * @param {string} text
831
868
  * @param {Vector2} pos
832
- * @param {number} [size]
833
- * @param {string} [color]
834
- * @param {number} [time]
835
- * @param {number} [angle]
836
- * @param {string} [font]
869
+ * @param {number} [size]
870
+ * @param {Color|string} [color]
871
+ * @param {number} [time]
872
+ * @param {number} [angle]
873
+ * @param {string} [font]
837
874
  * @memberof Debug */
838
- function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
875
+ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
839
876
  {
840
- ASSERT(typeof color === 'string', 'pass in css color strings');
841
- debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
877
+ if (isColor(color))
878
+ color = color.toString();
879
+ pos = pos.copy();
880
+ const timer = new Timer(time);
881
+ debugPrimitives.push({text, pos, size, color, timer, angle, font});
842
882
  }
843
883
 
844
884
  /** Clear all debug primitives in the list
@@ -889,17 +929,30 @@ function debugSaveDataURL(dataURL, filename)
889
929
  downloadLink.click();
890
930
  }
891
931
 
892
- /** Show error as full page of red text
932
+ /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
933
+ * This is a good function to call at the start of your game to catch all errors
934
+ * In release builds this function has no effect
893
935
  * @memberof Debug */
894
936
  function debugShowErrors()
895
937
  {
896
938
  const showError = (message)=>
897
939
  {
898
940
  // replace entire page with error message
899
- document.body.style.display = '';
900
- document.body.style.backgroundColor = '#111';
901
- document.body.innerHTML = `<pre style=color:#f00;font-size:50px;white-space:pre-wrap>` + message;
941
+ document.body.style = 'background-color:#111;margin:8px';
942
+ document.body.innerHTML = `<pre style=color:#f00;font-size:28px;white-space:pre-wrap>` + message;
902
943
  }
944
+
945
+ const originalAssert = console.assert;
946
+ console.assert = (assertion, ...output)=>
947
+ {
948
+ originalAssert(assertion, ...output);
949
+ if (!assertion)
950
+ {
951
+ const message = output.join(' ');
952
+ const stack = new Error().stack;
953
+ throw 'Assertion failed!\n' + message + '\n' + stack;
954
+ }
955
+ };
903
956
  onunhandledrejection = (event)=>
904
957
  showError(event.reason.stack || event.reason);
905
958
  onerror = (message, source, lineno, colno)=>
@@ -1067,7 +1120,7 @@ function debugRender()
1067
1120
  p.fill && overlayContext.fill();
1068
1121
  overlayContext.stroke();
1069
1122
  }
1070
- else if (p.size === 0 || p.size.x === 0 && p.size.y === 0)
1123
+ else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
1071
1124
  {
1072
1125
  // point
1073
1126
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
@@ -1094,7 +1147,7 @@ function debugRender()
1094
1147
  });
1095
1148
 
1096
1149
  // remove expired primitives
1097
- debugPrimitives = debugPrimitives.filter(r=>r.time<0);
1150
+ debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
1098
1151
  }
1099
1152
 
1100
1153
  if (debugObject)
@@ -1504,6 +1557,8 @@ function formatTime(t)
1504
1557
  async function fetchJSON(url)
1505
1558
  {
1506
1559
  const response = await fetch(url);
1560
+ if (!response.ok)
1561
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1507
1562
  return response.json();
1508
1563
  }
1509
1564
 
@@ -1514,6 +1569,13 @@ async function fetchJSON(url)
1514
1569
  * @memberof Utilities */
1515
1570
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1516
1571
 
1572
+ /**
1573
+ * Check if object is a valid string or can be converted to one
1574
+ * @param {any} s
1575
+ * @return {boolean}
1576
+ * @memberof Utilities */
1577
+ function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1578
+
1517
1579
  ///////////////////////////////////////////////////////////////////////////////
1518
1580
 
1519
1581
  /** Random global functions
@@ -1576,6 +1638,7 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1576
1638
  /**
1577
1639
  * Seeded random number generator
1578
1640
  * - Can be used to create a deterministic random number sequence
1641
+ * @memberof Engine
1579
1642
  * @example
1580
1643
  * let r = new RandomGenerator(123); // random number generator with seed 123
1581
1644
  * let a = r.float(); // random value between 0 and 1
@@ -1672,6 +1735,7 @@ function ASSERT_VECTOR2_NORMAL(v)
1672
1735
  /**
1673
1736
  * 2D Vector object with vector math library
1674
1737
  * - Functions do not change this so they can be chained together
1738
+ * @memberof Engine
1675
1739
  * @example
1676
1740
  * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
1677
1741
  * let b = new Vector2; // vector with coordinates (0, 0)
@@ -1812,9 +1876,10 @@ class Vector2
1812
1876
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1813
1877
  }
1814
1878
 
1815
- /** Set the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
1879
+ /** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
1816
1880
  * @param {number} [direction]
1817
- * @param {number} [length] */
1881
+ * @param {number} [length]
1882
+ * @return {Vector2} */
1818
1883
  setDirection(direction, length=1)
1819
1884
  {
1820
1885
  ASSERT_NUMBER_VALID(direction);
@@ -1822,8 +1887,10 @@ class Vector2
1822
1887
  direction = mod(direction, 4);
1823
1888
  ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1824
1889
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1825
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1826
- direction%2 ? 0 : direction ? -length : length);
1890
+
1891
+ this.x = direction%2 ? direction-1 ? -length : length : 0;
1892
+ this.y = direction%2 ? 0 : direction ? -length : length;
1893
+ return this;
1827
1894
  }
1828
1895
 
1829
1896
  /** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
@@ -1919,6 +1986,7 @@ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
1919
1986
 
1920
1987
  /**
1921
1988
  * Color object (red, green, blue, alpha) with some helpful functions
1989
+ * @memberof Engine
1922
1990
  * @example
1923
1991
  * let a = new Color; // white
1924
1992
  * let b = new Color(1, 0, 0); // red
@@ -2096,7 +2164,8 @@ class Color
2096
2164
  * @return {Color} */
2097
2165
  setHex(hex)
2098
2166
  {
2099
- ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
2167
+ ASSERT(isString(hex), 'Color hex code must be a string');
2168
+ ASSERT(hex[0] === '#', 'Color hex code must start with #');
2100
2169
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
2101
2170
 
2102
2171
  if (hex.length < 6)
@@ -2138,77 +2207,78 @@ class Color
2138
2207
  }
2139
2208
 
2140
2209
  ///////////////////////////////////////////////////////////////////////////////
2141
- // default colors
2210
+ // Default Colors
2142
2211
 
2143
2212
  /** Color - White #ffffff
2144
2213
  * @type {Color}
2145
2214
  * @memberof Utilities */
2146
- const WHITE = rgb();
2215
+ const WHITE = protectEngineConstant(rgb());
2147
2216
 
2148
- /** Color - Clear White #ffffff with 0 alpha
2217
+ /** Color - Clear White #757474ff with 0 alpha
2149
2218
  * @type {Color}
2150
2219
  * @memberof Utilities */
2151
- const CLEAR_WHITE = rgb(1,1,1,0);
2220
+ const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
2152
2221
 
2153
2222
  /** Color - Black #000000
2154
2223
  * @type {Color}
2155
2224
  * @memberof Utilities */
2156
- const BLACK = rgb(0,0,0);
2225
+ const BLACK = protectEngineConstant(rgb(0,0,0));
2157
2226
 
2158
2227
  /** Color - Clear Black #000000 with 0 alpha
2159
2228
  * @type {Color}
2160
2229
  * @memberof Utilities */
2161
- const CLEAR_BLACK = rgb(0,0,0,0);
2230
+ const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
2162
2231
 
2163
2232
  /** Color - Gray #808080
2164
2233
  * @type {Color}
2165
2234
  * @memberof Utilities */
2166
- const GRAY = rgb(.5,.5,.5);
2235
+ const GRAY = protectEngineConstant(rgb(.5,.5,.5));
2167
2236
 
2168
2237
  /** Color - Red #ff0000
2169
2238
  * @type {Color}
2170
2239
  * @memberof Utilities */
2171
- const RED = rgb(1,0,0);
2240
+ const RED = protectEngineConstant(rgb(1,0,0));
2172
2241
 
2173
2242
  /** Color - Orange #ff8000
2174
2243
  * @type {Color}
2175
2244
  * @memberof Utilities */
2176
- const ORANGE = rgb(1,.5,0);
2245
+ const ORANGE = protectEngineConstant(rgb(1,.5,0));
2177
2246
 
2178
2247
  /** Color - Yellow #ffff00
2179
2248
  * @type {Color}
2180
2249
  * @memberof Utilities */
2181
- const YELLOW = rgb(1,1,0);
2250
+ const YELLOW = protectEngineConstant(rgb(1,1,0));
2182
2251
 
2183
2252
  /** Color - Green #00ff00
2184
2253
  * @type {Color}
2185
2254
  * @memberof Utilities */
2186
- const GREEN = rgb(0,1,0);
2255
+ const GREEN = protectEngineConstant(rgb(0,1,0));
2187
2256
 
2188
2257
  /** Color - Cyan #00ffff
2189
2258
  * @type {Color}
2190
2259
  * @memberof Utilities */
2191
- const CYAN = rgb(0,1,1);
2260
+ const CYAN = protectEngineConstant(rgb(0,1,1));
2192
2261
 
2193
2262
  /** Color - Blue #0000ff
2194
2263
  * @type {Color}
2195
2264
  * @memberof Utilities */
2196
- const BLUE = rgb(0,0,1);
2265
+ const BLUE = protectEngineConstant(rgb(0,0,1));
2197
2266
 
2198
2267
  /** Color - Purple #8000ff
2199
2268
  * @type {Color}
2200
2269
  * @memberof Utilities */
2201
- const PURPLE = rgb(.5,0,1);
2270
+ const PURPLE = protectEngineConstant(rgb(.5,0,1));
2202
2271
 
2203
2272
  /** Color - Magenta #ff00ff
2204
2273
  * @type {Color}
2205
2274
  * @memberof Utilities */
2206
- const MAGENTA = rgb(1,0,1);
2275
+ const MAGENTA = protectEngineConstant(rgb(1,0,1));
2207
2276
 
2208
2277
  ///////////////////////////////////////////////////////////////////////////////
2209
2278
 
2210
2279
  /**
2211
2280
  * Timer object tracks how long has passed since it was set
2281
+ * @memberof Engine
2212
2282
  * @example
2213
2283
  * let a = new Timer; // creates a timer that is not set
2214
2284
  * a.set(3); // sets the timer to 3 seconds
@@ -2270,6 +2340,36 @@ class Timer
2270
2340
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2271
2341
  * @return {number} */
2272
2342
  valueOf() { return this.get(); }
2343
+ }
2344
+
2345
+ ///////////////////////////////////////////////////////////////////////////////
2346
+ // Helper functions used by the engine
2347
+
2348
+ // make color constants immutable with debug assertions
2349
+ function protectEngineConstant(obj)
2350
+ {
2351
+ if (debug)
2352
+ {
2353
+ // get properties and store original values
2354
+ const props = Object.keys(obj), values = {};
2355
+ props.forEach(prop => values[prop] = obj[prop]);
2356
+
2357
+ // replace with getters/setters that assert
2358
+ props.forEach(prop =>
2359
+ {
2360
+ Object.defineProperty(obj, prop, {
2361
+ get: () => values[prop],
2362
+ set: (value) =>
2363
+ {
2364
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
2365
+ },
2366
+ enumerable: true
2367
+ });
2368
+ });
2369
+ }
2370
+
2371
+ // freeze the object to prevent adding new properties
2372
+ return Object.freeze(obj);
2273
2373
  }
2274
2374
  /**
2275
2375
  * LittleJS Engine Settings
@@ -2577,7 +2677,7 @@ let medalsPreventUnlock = false;
2577
2677
  /** Set position of camera in world space
2578
2678
  * @param {Vector2} pos
2579
2679
  * @memberof Settings */
2580
- function setCameraPos(pos) { cameraPos = pos; }
2680
+ function setCameraPos(pos) { cameraPos = pos.copy(); }
2581
2681
 
2582
2682
  /** Set angle of camera in world space
2583
2683
  * @param {number} angle
@@ -2600,17 +2700,17 @@ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2600
2700
  /** Set color to clear the canvas to before render
2601
2701
  * @param {Color} color
2602
2702
  * @memberof Settings */
2603
- function setCanvasClearColor(color) { canvasClearColor = color; }
2703
+ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
2604
2704
 
2605
2705
  /** Set max size of the canvas
2606
2706
  * @param {Vector2} size
2607
2707
  * @memberof Settings */
2608
- function setCanvasMaxSize(size) { canvasMaxSize = size; }
2708
+ function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
2609
2709
 
2610
2710
  /** Set fixed size of the canvas
2611
2711
  * @param {Vector2} size
2612
2712
  * @memberof Settings */
2613
- function setCanvasFixedSize(size) { canvasFixedSize = size; }
2713
+ function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
2614
2714
 
2615
2715
  /** Use nearest scaling algorithm for canvas for more pixelated look
2616
2716
  * - If enabled sets css image-rendering:pixelated
@@ -2675,7 +2775,7 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
2675
2775
  /** Set default size of tiles in pixels
2676
2776
  * @param {Vector2} size
2677
2777
  * @memberof Settings */
2678
- function setTileSizeDefault(size) { tileSizeDefault = size; }
2778
+ function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
2679
2779
 
2680
2780
  /** Set to prevent tile bleeding from neighbors in pixels
2681
2781
  * @param {number} scale
@@ -2720,7 +2820,7 @@ function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
2720
2820
  /** Set how much gravity to apply to objects
2721
2821
  * @param {Vector2} newGravity
2722
2822
  * @memberof Settings */
2723
- function setGravity(newGravity) { gravity = newGravity; }
2823
+ function setGravity(newGravity) { gravity = newGravity.copy(); }
2724
2824
 
2725
2825
  /** Set to scales emit rate of particles
2726
2826
  * @param {number} scale
@@ -2810,7 +2910,7 @@ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
2810
2910
  /** Set size of medal display
2811
2911
  * @param {Vector2} size
2812
2912
  * @memberof Settings */
2813
- function setMedalDisplaySize(size) { medalDisplaySize = size; }
2913
+ function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
2814
2914
 
2815
2915
  /** Set to stop medals from being unlockable
2816
2916
  * @param {boolean} preventUnlock
@@ -2850,6 +2950,7 @@ function setDebugKey(key) { debugKey = key; }
2850
2950
  * - Collision for objects can be set to be solid to block other objects
2851
2951
  * - Objects may get pushed into overlapping other solid objects, if so they will push away
2852
2952
  * - Solid objects are more performance intensive and should be used sparingly
2953
+ * @memberof Engine
2853
2954
  * @example
2854
2955
  * // create an engine object, normally you would first extend the class with your own
2855
2956
  * const pos = vec2(2,3);
@@ -2858,18 +2959,18 @@ function setDebugKey(key) { debugKey = key; }
2858
2959
  class EngineObject
2859
2960
  {
2860
2961
  /** Create an engine object and adds it to the list of objects
2861
- * @param {Vector2} [pos=(0,0)] - World space position of the object
2862
- * @param {Vector2} [size=(1,1)] - World space size of the object
2863
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2864
- * @param {number} [angle] - Angle the object is rotated by
2865
- * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
2866
- * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2962
+ * @param {Vector2} [pos=(0,0)] - World space position of the object
2963
+ * @param {Vector2} [size=(1,1)] - World space size of the object
2964
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2965
+ * @param {number} [angle] - Angle the object is rotated by
2966
+ * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2967
+ * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2867
2968
  */
2868
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
2969
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
2869
2970
  {
2870
2971
  // check passed in params
2871
- ASSERT(isVector2(pos), 'object pos should be a vec2');
2872
- ASSERT(isVector2(size), 'object size should be a vec2');
2972
+ ASSERT(isVector2(pos), 'object pos must be a vec2');
2973
+ ASSERT(isVector2(size), 'object size must be a vec2');
2873
2974
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2874
2975
  ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2875
2976
  ASSERT(isColor(color), 'object color should be a valid rgba color');
@@ -3154,7 +3255,7 @@ class EngineObject
3154
3255
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3155
3256
  }
3156
3257
 
3157
- /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
3258
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
3158
3259
  destroy()
3159
3260
  {
3160
3261
  if (this.destroyed)
@@ -3228,6 +3329,8 @@ class EngineObject
3228
3329
  addChild(child, localPos=vec2(), localAngle=0)
3229
3330
  {
3230
3331
  ASSERT(!child.parent && !this.children.includes(child));
3332
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
3333
+ ASSERT(child !== this, 'cannot add self as child');
3231
3334
  this.children.push(child);
3232
3335
  child.parent = this;
3233
3336
  child.localPos = localPos.copy();
@@ -3239,6 +3342,7 @@ class EngineObject
3239
3342
  removeChild(child)
3240
3343
  {
3241
3344
  ASSERT(child.parent === this && this.children.includes(child));
3345
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
3242
3346
  this.children.splice(this.children.indexOf(child), 1);
3243
3347
  child.parent = 0;
3244
3348
  }
@@ -3377,7 +3481,7 @@ let drawCount;
3377
3481
  * Create a tile info object using a grid based system
3378
3482
  * - This can take vecs or floats for easier use and conversion
3379
3483
  * - If an index is passed in, the tile size and index will determine the position
3380
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
3484
+ * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3381
3485
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3382
3486
  * @param {number} [textureIndex] - Texture index to use
3383
3487
  * @param {number} [padding] - How many pixels padding around tiles
@@ -3422,6 +3526,7 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3422
3526
 
3423
3527
  /**
3424
3528
  * Tile Info - Stores info about how to draw a tile
3529
+ * @memberof Draw
3425
3530
  */
3426
3531
  class TileInfo
3427
3532
  {
@@ -3482,7 +3587,10 @@ class TileInfo
3482
3587
  }
3483
3588
  }
3484
3589
 
3485
- /** Texture Info - Stores info about each texture */
3590
+ /**
3591
+ * Tile Info - Stores info about each texture
3592
+ * @memberof Draw
3593
+ */
3486
3594
  class TextureInfo
3487
3595
  {
3488
3596
  /**
@@ -3528,10 +3636,11 @@ class TextureInfo
3528
3636
  function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3529
3637
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3530
3638
  {
3531
- ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
3532
- ASSERT(isVector2(size), 'drawTile size should be a vec2');
3533
- ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
3534
- ASSERT(isNumber(angle), 'drawTile angle should be a number');
3639
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3640
+ ASSERT(isVector2(size), 'size must be a vec2');
3641
+ ASSERT(isColor(color), 'color is invalid');
3642
+ ASSERT(isNumber(angle), 'angle must be a number');
3643
+ ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
3535
3644
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3536
3645
 
3537
3646
  const textureInfo = tileInfo && tileInfo.textureInfo;
@@ -3626,10 +3735,10 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3626
3735
  * @memberof Draw */
3627
3736
  function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3628
3737
  {
3629
- ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
3630
- ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
3631
- ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
3632
- ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
3738
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3739
+ ASSERT(isVector2(size), 'size must be a vec2');
3740
+ ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
3741
+ ASSERT(isNumber(angle), 'angle must be a number');
3633
3742
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3634
3743
  if (useWebGL)
3635
3744
  {
@@ -3687,11 +3796,11 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3687
3796
  * @memberof Draw */
3688
3797
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3689
3798
  {
3690
- ASSERT(Array.isArray(points), 'drawLineList points should be an array');
3691
- ASSERT(isNumber(width), 'drawLineList width should be a number');
3692
- ASSERT(isColor(color), 'drawLineList color is invalid');
3693
- ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
3694
- ASSERT(isNumber(angle), 'drawLineList angle should be a number');
3799
+ ASSERT(Array.isArray(points), 'points must be an array');
3800
+ ASSERT(isNumber(width), 'width must be a number');
3801
+ ASSERT(isColor(color), 'color is invalid');
3802
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3803
+ ASSERT(isNumber(angle), 'angle must be a number');
3695
3804
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3696
3805
  if (useWebGL)
3697
3806
  {
@@ -3762,8 +3871,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3762
3871
  * @memberof Draw */
3763
3872
  function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3764
3873
  {
3765
- ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
3766
- ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
3874
+ ASSERT(isVector2(size), 'size must be a vec2');
3875
+ ASSERT(isNumber(sides), 'sides must be a number');
3767
3876
 
3768
3877
  // build regular polygon points
3769
3878
  const points = [];
@@ -3789,12 +3898,13 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3789
3898
  * @memberof Draw */
3790
3899
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3791
3900
  {
3792
- ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
3793
- ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3794
- ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3795
- ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3796
- ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3901
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3902
+ ASSERT(Array.isArray(points), 'points must be an array');
3903
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3904
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3905
+ ASSERT(isNumber(angle), 'angle must be a number');
3797
3906
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3907
+
3798
3908
  if (useWebGL)
3799
3909
  {
3800
3910
  let scale = 1;
@@ -3841,13 +3951,14 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3841
3951
  * @memberof Draw */
3842
3952
  function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3843
3953
  {
3844
- ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
3845
- ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
3846
- ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3847
- ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3848
- ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3849
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3954
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3955
+ ASSERT(isVector2(size), 'size must be a vec2');
3956
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3957
+ ASSERT(isNumber(angle), 'angle must be a number');
3958
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3959
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3850
3960
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3961
+
3851
3962
  if (useWebGL)
3852
3963
  {
3853
3964
  // draw as a regular polygon
@@ -3884,21 +3995,32 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3884
3995
  * @memberof Draw */
3885
3996
  function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3886
3997
  {
3887
- ASSERT(isNumber(size), 'drawCircle size should be a number');
3998
+ ASSERT(isNumber(size), 'size must be a number');
3888
3999
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3889
4000
  }
3890
4001
 
4002
+ /**
4003
+ * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
4004
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4005
+ * @memberof Draw
4006
+ */
4007
+
3891
4008
  /** Draw directly to a 2d canvas context in world space
3892
4009
  * @param {Vector2} pos
3893
4010
  * @param {Vector2} size
3894
4011
  * @param {number} angle
3895
4012
  * @param {boolean} [mirror]
3896
- * @param {Function} [drawFunction]
4013
+ * @param {Canvas2DDrawFunction} [drawFunction]
3897
4014
  * @param {boolean} [screenSpace=false]
3898
4015
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3899
4016
  * @memberof Draw */
3900
4017
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
3901
4018
  {
4019
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4020
+ ASSERT(isVector2(size), 'size must be a vec2');
4021
+ ASSERT(isNumber(angle), 'angle must be a number');
4022
+ ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
4023
+
3902
4024
  if (!screenSpace)
3903
4025
  {
3904
4026
  // transform from world space to screen space
@@ -3966,6 +4088,16 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3966
4088
  * @memberof Draw */
3967
4089
  function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3968
4090
  {
4091
+ ASSERT(isString(text), 'text must be a string');
4092
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4093
+ ASSERT(isNumber(size), 'size must be a number');
4094
+ ASSERT(isColor(color), 'color must be a color');
4095
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4096
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
4097
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
4098
+ ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4099
+ ASSERT(isString(font), 'font must be a string');
4100
+
3969
4101
  context.fillStyle = color.toString();
3970
4102
  context.strokeStyle = lineColor.toString();
3971
4103
  context.lineWidth = lineWidth;
@@ -4167,6 +4299,7 @@ let engineFontImage;
4167
4299
  * - 96 characters (from space to tilde) are stored in an image
4168
4300
  * - Uses a default 8x8 font if none is supplied
4169
4301
  * - You can also use fonts from the main tile sheet
4302
+ * @memberof Draw
4170
4303
  * @example
4171
4304
  * // use built in font
4172
4305
  * const font = new FontImage;
@@ -4223,7 +4356,7 @@ class FontImage
4223
4356
  * @param {boolean} [center]
4224
4357
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4225
4358
  */
4226
- drawTextScreen(text, pos, scale=4, center, context=overlayContext)
4359
+ drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
4227
4360
  {
4228
4361
  context.save();
4229
4362
  const size = this.tileSize;
@@ -4917,6 +5050,7 @@ function audioInit()
4917
5050
  * Sound Object - Stores a sound for later use and can be played positionally
4918
5051
  *
4919
5052
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5053
+ * @memberof Audio
4920
5054
  * @example
4921
5055
  * // create a sound
4922
5056
  * const sound_example = new Sound([.5,.5]);
@@ -5011,12 +5145,12 @@ class Sound
5011
5145
 
5012
5146
  /** Play the sound as a musical note with a semitone offset
5013
5147
  * This can be used to play music with chromatic scales
5014
- * @param {number} semitoneOffset - How many semitones to offset pitch
5148
+ * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5015
5149
  * @param {Vector2} [pos] - World space position to play the sound if any
5016
5150
  * @param {number} [volume=1] - How much to scale volume by
5017
5151
  * @return {SoundInstance} - The audio source node
5018
5152
  */
5019
- playNote(semitoneOffset, pos, volume)
5153
+ playNote(semitoneOffset=0, pos, volume)
5020
5154
  {
5021
5155
  const pitch = getNoteFrequency(semitoneOffset, 1);
5022
5156
  return this.play(pos, volume, pitch, 0);
@@ -5039,6 +5173,8 @@ class Sound
5039
5173
  /**
5040
5174
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
5041
5175
  * - this can be used to play wave, mp3, and ogg files
5176
+ * @extends Sound
5177
+ * @memberof Audio
5042
5178
  * @example
5043
5179
  * // create a sound
5044
5180
  * const sound_example = new SoundWave('sound.mp3');
@@ -5048,22 +5184,29 @@ class Sound
5048
5184
  */
5049
5185
  class SoundWave extends Sound
5050
5186
  {
5187
+ /**
5188
+ * @callback SoundLoadCallback - Function called when sound is loaded
5189
+ * @param {SoundWave} sound
5190
+ * @memberof Audio
5191
+ */
5192
+
5051
5193
  /** Create a sound object and cache the wave file for later use
5052
5194
  * @param {string} filename - Filename of audio file to load
5053
5195
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
5054
5196
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5055
5197
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5056
- * @param {Function} [onloadCallback] - callback function to call when sound is loaded
5198
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5057
5199
  */
5058
5200
  constructor(filename, randomness=0, range, taper, onloadCallback)
5059
5201
  {
5060
5202
  super(undefined, range, taper);
5061
5203
  if (!soundEnable || headlessMode) return;
5204
+ ASSERT(!filename || isString(filename), 'filename must be a string');
5062
5205
 
5063
- /** @property {Function} - callback function to call when sound is loaded */
5206
+ /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5064
5207
  this.onloadCallback = onloadCallback;
5065
5208
  this.randomness = randomness;
5066
- this.loadSound(filename);
5209
+ filename && this.loadSound(filename);
5067
5210
  }
5068
5211
 
5069
5212
  /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
@@ -5072,6 +5215,8 @@ class SoundWave extends Sound
5072
5215
  async loadSound(filename)
5073
5216
  {
5074
5217
  const response = await fetch(filename);
5218
+ if (!response.ok)
5219
+ throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
5075
5220
  const arrayBuffer = await response.arrayBuffer();
5076
5221
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
5077
5222
 
@@ -5107,7 +5252,7 @@ class SoundWave extends Sound
5107
5252
  this.sampleChannels = sampleChannels;
5108
5253
  this.loadedPercent = 1;
5109
5254
  if (this.onloadCallback)
5110
- this.onloadCallback();
5255
+ this.onloadCallback(this);
5111
5256
  }
5112
5257
  }
5113
5258
 
@@ -5116,6 +5261,7 @@ class SoundWave extends Sound
5116
5261
  /**
5117
5262
  * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
5118
5263
  * Represents a single playing instance of a sound with pause/resume capabilities
5264
+ * @memberof Audio
5119
5265
  * @example
5120
5266
  * // Play a sound and get an instance for control
5121
5267
  * const jumpSound = new Sound([.5,.5,220]);
@@ -5315,6 +5461,12 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5315
5461
 
5316
5462
  ///////////////////////////////////////////////////////////////////////////////
5317
5463
 
5464
+ /**
5465
+ * @callback AudioEndedCallback - Function called when a sound ends
5466
+ * @param {AudioBufferSourceNode} source
5467
+ * @memberof Audio
5468
+ */
5469
+
5318
5470
  /** Play cached audio samples with given settings
5319
5471
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
5320
5472
  * @param {number} [volume] - How much to scale volume by
@@ -5324,7 +5476,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5324
5476
  * @param {number} [sampleRate=44100] - Sample rate for the sound
5325
5477
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
5326
5478
  * @param {number} [offset] - Offset in seconds to start playback from
5327
- * @param {Function} [onended] - Callback for when the sound ends
5479
+ * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
5328
5480
  * @return {AudioBufferSourceNode} - The audio node of the sound played
5329
5481
  * @memberof Audio */
5330
5482
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
@@ -5356,16 +5508,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
5356
5508
  if (onended)
5357
5509
  source.addEventListener('ended', ()=> onended(source));
5358
5510
 
5511
+ const startOffset = offset * rate;
5359
5512
  if (!audioIsRunning())
5360
5513
  {
5361
- // fix stalled audio, this sound won't be able to play
5362
- audioContext.resume();
5363
- return;
5514
+ // fix stalled audio and start
5515
+ audioContext.resume().then(()=>source.start(0, startOffset));
5364
5516
  }
5365
-
5366
- // play and return sound
5367
- const startOffset = offset * rate;
5368
- source.start(0, startOffset);
5517
+ else
5518
+ source.start(0, startOffset);
5369
5519
  return source;
5370
5520
  }
5371
5521
 
@@ -5530,7 +5680,7 @@ function zzfxG
5530
5680
  * - Unlimited numbers of layers, allocates canvases as needed
5531
5681
  * - Tile layers can be drawn to using their context with canvas2d
5532
5682
  * - Tile layers can also have collision with EngineObjects
5533
- * @namespace TileCollision
5683
+ * @namespace TileLayers
5534
5684
  */
5535
5685
 
5536
5686
  ///////////////////////////////////////////////////////////////////////////////
@@ -5538,13 +5688,13 @@ function zzfxG
5538
5688
 
5539
5689
  /** Keep track of all tile layers with collision
5540
5690
  * @type {Array<TileCollisionLayer>}
5541
- * @memberof TileCollision */
5691
+ * @memberof TileLayers */
5542
5692
  const tileCollisionLayers = [];
5543
5693
 
5544
5694
  /** Get tile collision data for a given cell in the grid
5545
5695
  * @param {Vector2} pos
5546
5696
  * @return {number}
5547
- * @memberof TileCollision */
5697
+ * @memberof TileLayers */
5548
5698
  function tileCollisionGetData(pos)
5549
5699
  {
5550
5700
  // check all tile collision layers
@@ -5560,7 +5710,7 @@ function tileCollisionGetData(pos)
5560
5710
  * @param {EngineObject} [object] - An object or undefined for generic test
5561
5711
  * @param {boolean} [solidOnly] - Only check solid layers if true
5562
5712
  * @return {TileCollisionLayer}
5563
- * @memberof TileCollision */
5713
+ * @memberof TileLayers */
5564
5714
  function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5565
5715
  {
5566
5716
  for (const layer of tileCollisionLayers)
@@ -5578,7 +5728,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5578
5728
  * @param {EngineObject} [object] - An object or undefined for generic test
5579
5729
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5580
5730
  * @return {Vector2}
5581
- * @memberof TileCollision */
5731
+ * @memberof TileLayers */
5582
5732
  function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5583
5733
  {
5584
5734
  for (const layer of tileCollisionLayers)
@@ -5601,8 +5751,8 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5601
5751
  * @param {number} [collisionLayer] - Layer to use for collision if any
5602
5752
  * @param {boolean} [draw] - Should the layer be drawn automatically
5603
5753
  * @return {Array<TileCollisionLayer>}
5604
- * @memberof TileCollision */
5605
- function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5754
+ * @memberof TileLayers */
5755
+ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5606
5756
  {
5607
5757
  if (!tileMapData)
5608
5758
  {
@@ -5633,9 +5783,9 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5633
5783
  tileLayers[layerIndex] = tileLayer;
5634
5784
 
5635
5785
  // apply layer color
5636
- const layerColor = dataLayer.color || WHITE;
5637
- if (dataLayer.tintcolor)
5638
- layerColor.setHex(dataLayer.tintcolor);
5786
+ const layerColor = dataLayer.tintcolor ?
5787
+ new Color().setHex(dataLayer.tintcolor) :
5788
+ dataLayer.color || WHITE;
5639
5789
  ASSERT(isColor(layerColor), 'layer color is not a color');
5640
5790
 
5641
5791
  for (let x=levelSize.x; x--;)
@@ -5662,6 +5812,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5662
5812
  ///////////////////////////////////////////////////////////////////////////////
5663
5813
  /**
5664
5814
  * Tile layer data object stores info about how to draw a tile
5815
+ * @memberof TileLayers
5665
5816
  * @example
5666
5817
  * // create tile layer data with tile index 0 and random orientation and color
5667
5818
  * const tileIndex = 0;
@@ -5699,6 +5850,7 @@ class TileLayerData
5699
5850
  * - Contains an offscreen canvas that can be rendered to
5700
5851
  * - WebGL rendering is optional, call useWebGL to enable
5701
5852
  * @extends EngineObject
5853
+ * @memberof TileLayers
5702
5854
  * @example
5703
5855
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
5704
5856
  */
@@ -5713,6 +5865,7 @@ class CanvasLayer extends EngineObject
5713
5865
  */
5714
5866
  constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5715
5867
  {
5868
+ ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5716
5869
  super(position, size, undefined, angle, WHITE, renderOrder);
5717
5870
 
5718
5871
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
@@ -5760,12 +5913,18 @@ class CanvasLayer extends EngineObject
5760
5913
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5761
5914
  }
5762
5915
 
5916
+ /**
5917
+ * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
5918
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
5919
+ * @memberof TileLayers
5920
+ */
5921
+
5763
5922
  /** Draw onto the layer canvas in world space (bypass WebGL)
5764
5923
  * @param {Vector2} pos
5765
5924
  * @param {Vector2} size
5766
5925
  * @param {number} angle
5767
5926
  * @param {boolean} mirror
5768
- * @param {Function} drawFunction */
5927
+ * @param {Canvas2DDrawCallback} drawFunction */
5769
5928
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5770
5929
  {
5771
5930
  const context = this.context;
@@ -5802,7 +5961,7 @@ class CanvasLayer extends EngineObject
5802
5961
  else
5803
5962
  {
5804
5963
  // untextured
5805
- context.fillStyle = color;
5964
+ context.fillStyle = color.toString();
5806
5965
  context.fillRect(-.5, -.5, 1, 1);
5807
5966
  }
5808
5967
  });
@@ -5839,7 +5998,9 @@ class CanvasLayer extends EngineObject
5839
5998
  * - To allow dynamic modifications, layers are rendered using canvas 2d
5840
5999
  * - Some devices like mobile phones are limited to 4k texture resolution
5841
6000
  * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
6001
+ * - Tile layers are centered on their corner, so normal levels are at (0,0)
5842
6002
  * @extends CanvasLayer
6003
+ * @memberof TileLayers
5843
6004
  * @example
5844
6005
  * const tileLayer = new TileLayer(vec2(), vec2(200,100));
5845
6006
  */
@@ -5849,15 +6010,14 @@ class TileLayer extends CanvasLayer
5849
6010
  * @param {Vector2} position - World space position
5850
6011
  * @param {Vector2} size - World space size
5851
6012
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
5852
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
5853
6013
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5854
6014
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5855
6015
  */
5856
- constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0, useWebGL=glEnable)
6016
+ constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5857
6017
  {
5858
6018
  super(position, size, 0, renderOrder, size);
6019
+
5859
6020
  this.tileInfo = tileInfo;
5860
-
5861
6021
  const canvasSize = size.multiply(tileInfo.size);
5862
6022
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
5863
6023
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
@@ -5894,6 +6054,8 @@ class TileLayer extends CanvasLayer
5894
6054
  * @param {boolean} [redraw] - Force the tile to redraw if true */
5895
6055
  setData(layerPos, data, redraw=false)
5896
6056
  {
6057
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6058
+ ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
5897
6059
  if (layerPos.arrayCheck(this.size))
5898
6060
  {
5899
6061
  this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
@@ -5905,7 +6067,10 @@ class TileLayer extends CanvasLayer
5905
6067
  * @param {Vector2} layerPos - Local position in array
5906
6068
  * @return {TileLayerData} */
5907
6069
  getData(layerPos)
5908
- { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
6070
+ {
6071
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6072
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6073
+ }
5909
6074
 
5910
6075
  // Render the tile layer, called automatically by the engine
5911
6076
  render()
@@ -5914,9 +6079,10 @@ class TileLayer extends CanvasLayer
5914
6079
 
5915
6080
  // draw the tile layer as a single tile
5916
6081
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5917
- const pos = this.pos.add(this.size.scale(.5));
6082
+ const size = this.drawSize || this.size;
6083
+ const pos = this.pos.add(size.scale(.5));
5918
6084
  const useWebGL = glEnable && this.glTexture !== undefined;
5919
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6085
+ drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5920
6086
  }
5921
6087
 
5922
6088
  /** Draw all the tile data to an offscreen canvas
@@ -6008,6 +6174,7 @@ class TileLayer extends CanvasLayer
6008
6174
  * - there can be multiple tile collision layers
6009
6175
  * - tile collision layers should not overlap each other
6010
6176
  * @extends TileLayer
6177
+ * @memberof TileLayers
6011
6178
  */
6012
6179
  class TileCollisionLayer extends TileLayer
6013
6180
  {
@@ -6020,8 +6187,7 @@ class TileCollisionLayer extends TileLayer
6020
6187
  */
6021
6188
  constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
6022
6189
  {
6023
- const scale = vec2(1); // collision layers are not scaled
6024
- super(position, size.floor(), tileInfo, scale, renderOrder, useWebGL);
6190
+ super(position, size.floor(), tileInfo, renderOrder, useWebGL);
6025
6191
 
6026
6192
  /** @property {Array<number>} - The tile collision grid */
6027
6193
  this.collisionData = [];
@@ -6051,6 +6217,7 @@ class TileCollisionLayer extends TileLayer
6051
6217
  * @param {Vector2} size - width and height of tile collision 2d grid */
6052
6218
  initCollision(size)
6053
6219
  {
6220
+ ASSERT(isVector2(size), 'size must be a Vector2');
6054
6221
  this.size = size.floor();
6055
6222
  this.collisionData = [];
6056
6223
  this.collisionData.length = size.area();
@@ -6062,6 +6229,7 @@ class TileCollisionLayer extends TileLayer
6062
6229
  * @param {number} [data] */
6063
6230
  setCollisionData(gridPos, data=1)
6064
6231
  {
6232
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6065
6233
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6066
6234
  gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6067
6235
  }
@@ -6071,6 +6239,7 @@ class TileCollisionLayer extends TileLayer
6071
6239
  * @return {number} */
6072
6240
  getCollisionData(gridPos)
6073
6241
  {
6242
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6074
6243
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6075
6244
  return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6076
6245
  }
@@ -6082,6 +6251,9 @@ class TileCollisionLayer extends TileLayer
6082
6251
  * @return {boolean} */
6083
6252
  collisionTest(pos, size=new Vector2, object)
6084
6253
  {
6254
+ ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
6255
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6256
+
6085
6257
  // transform to local layer space
6086
6258
  const posX = pos.x - this.pos.x;
6087
6259
  const posY = pos.y - this.pos.y;
@@ -6112,6 +6284,9 @@ class TileCollisionLayer extends TileLayer
6112
6284
  * @return {Vector2} */
6113
6285
  collisionRaycast(posStart, posEnd, object)
6114
6286
  {
6287
+ ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6288
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6289
+
6115
6290
  // transform to local layer space
6116
6291
  const posStartX = posStart.x - this.pos.x;
6117
6292
  const posStartY = posStart.y - this.pos.y;
@@ -6161,9 +6336,16 @@ class TileCollisionLayer extends TileLayer
6161
6336
  * LittleJS Particle System
6162
6337
  */
6163
6338
 
6339
+ /**
6340
+ * @callback ParticleCallbackFunction - Function that processes a particle
6341
+ * @param {Particle} particle
6342
+ * @memberof Engine
6343
+ */
6344
+
6164
6345
  /**
6165
6346
  * Particle Emitter - Spawns particles with the given settings
6166
6347
  * @extends EngineObject
6348
+ * @memberof Engine
6167
6349
  * @example
6168
6350
  * // create a particle emitter
6169
6351
  * let pos = vec2(2,3);
@@ -6188,10 +6370,10 @@ class ParticleEmitter extends EngineObject
6188
6370
  * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
6189
6371
  * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
6190
6372
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
6191
- * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
6192
- * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
6193
- * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
6194
- * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
6373
+ * @param {Color} [colorStartA=WHITE] - Color at start of life 1, randomized between start colors
6374
+ * @param {Color} [colorStartB=WHITE] - Color at start of life 2, randomized between start colors
6375
+ * @param {Color} [colorEndA=CLEAR_WHITE] - Color at end of life 1, randomized between end colors
6376
+ * @param {Color} [colorEndB=CLEAR_WHITE] - Color at end of life 2, randomized between end colors
6195
6377
  * @param {number} [particleTime] - How long particles live
6196
6378
  * @param {number} [sizeStart] - How big are particles at start
6197
6379
  * @param {number} [sizeEnd] - How big are particles at end
@@ -6218,10 +6400,10 @@ class ParticleEmitter extends EngineObject
6218
6400
  emitRate = 100,
6219
6401
  emitConeAngle = PI,
6220
6402
  tileInfo,
6221
- colorStartA = new Color,
6222
- colorStartB = new Color,
6223
- colorEndA = new Color(1,1,1,0),
6224
- colorEndB = new Color(1,1,1,0),
6403
+ colorStartA = WHITE,
6404
+ colorStartB = WHITE,
6405
+ colorEndA = CLEAR_WHITE,
6406
+ colorEndB = CLEAR_WHITE,
6225
6407
  particleTime = .5,
6226
6408
  sizeStart = .1,
6227
6409
  sizeEnd = 1,
@@ -6244,7 +6426,8 @@ class ParticleEmitter extends EngineObject
6244
6426
 
6245
6427
  // emitter settings
6246
6428
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
6247
- this.emitSize = emitSize
6429
+ this.emitSize = emitSize instanceof Vector2 ?
6430
+ emitSize.copy() : emitSize;
6248
6431
  /** @property {number} - How long to stay alive (0 is forever) */
6249
6432
  this.emitTime = emitTime;
6250
6433
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -6254,13 +6437,13 @@ class ParticleEmitter extends EngineObject
6254
6437
 
6255
6438
  // color settings
6256
6439
  /** @property {Color} - Color at start of life 1, randomized between start colors */
6257
- this.colorStartA = colorStartA;
6440
+ this.colorStartA = colorStartA.copy();
6258
6441
  /** @property {Color} - Color at start of life 2, randomized between start colors */
6259
- this.colorStartB = colorStartB;
6442
+ this.colorStartB = colorStartB.copy();
6260
6443
  /** @property {Color} - Color at end of life 1, randomized between end colors */
6261
- this.colorEndA = colorEndA;
6444
+ this.colorEndA = colorEndA.copy();
6262
6445
  /** @property {Color} - Color at end of life 2, randomized between end colors */
6263
- this.colorEndB = colorEndB;
6446
+ this.colorEndB = colorEndB.copy();
6264
6447
  /** @property {boolean} - Should color be randomized linearly or across each component */
6265
6448
  this.randomColorLinear = randomColorLinear;
6266
6449
 
@@ -6295,9 +6478,9 @@ class ParticleEmitter extends EngineObject
6295
6478
  this.localSpace = localSpace;
6296
6479
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
6297
6480
  this.trailScale = 0;
6298
- /** @property {Function} - Callback when particle is destroyed */
6481
+ /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
6299
6482
  this.particleDestroyCallback = undefined;
6300
- /** @property {Function} - Callback when particle is created */
6483
+ /** @property {ParticleCallbackFunction} - Callback when particle is created */
6301
6484
  this.particleCreateCallback = undefined;
6302
6485
  /** @property {number} - Track particle emit time */
6303
6486
  this.emitTimeBuffer = 0;
@@ -6398,6 +6581,7 @@ class ParticleEmitter extends EngineObject
6398
6581
  /**
6399
6582
  * Particle Object - Created automatically by Particle Emitters
6400
6583
  * @extends EngineObject
6584
+ * @memberof Engine
6401
6585
  */
6402
6586
  class Particle extends EngineObject
6403
6587
  {
@@ -6416,7 +6600,7 @@ class Particle extends EngineObject
6416
6600
  * @param {boolean} additive - Does it use additive blend mode
6417
6601
  * @param {number} trailScale - If a trail, how long to make it
6418
6602
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
6419
- * @param {Function} [destroyCallback] - Callback when particle dies
6603
+ * @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
6420
6604
  */
6421
6605
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
6422
6606
  )
@@ -6425,14 +6609,14 @@ class Particle extends EngineObject
6425
6609
 
6426
6610
  /** @property {Color} - Color at start of life */
6427
6611
  this.colorStart = colorStart;
6428
- /** @property {Color} - Calculated change in color */
6429
- this.colorEndDelta = colorEnd.subtract(colorStart);
6612
+ /** @property {Color} - Color at end of life */
6613
+ this.colorEnd = colorEnd;
6430
6614
  /** @property {number} - How long to live for */
6431
6615
  this.lifeTime = lifeTime;
6432
6616
  /** @property {number} - Size at start of life */
6433
6617
  this.sizeStart = sizeStart;
6434
- /** @property {number} - Calculated change in size */
6435
- this.sizeEndDelta = sizeEnd - sizeStart;
6618
+ /** @property {number} - Size at end of life */
6619
+ this.sizeEnd = sizeEnd;
6436
6620
  /** @property {number} - How quick to fade in/out */
6437
6621
  this.fadeRate = fadeRate;
6438
6622
  /** @property {boolean} - Is it additive */
@@ -6441,7 +6625,7 @@ class Particle extends EngineObject
6441
6625
  this.trailScale = trailScale;
6442
6626
  /** @property {ParticleEmitter} - Parent emitter if local space */
6443
6627
  this.localSpaceEmitter = localSpaceEmitter;
6444
- /** @property {Function} - Called when particle dies */
6628
+ /** @property {ParticleCallbackFunction} - Called when particle dies */
6445
6629
  this.destroyCallback = destroyCallback;
6446
6630
  // particles do not clamp speed by default
6447
6631
  this.clampSpeed = false;
@@ -6468,54 +6652,58 @@ class Particle extends EngineObject
6468
6652
  /** Render the particle, automatically called each frame, sorted by renderOrder */
6469
6653
  render()
6470
6654
  {
6471
- // modulate size and color
6472
- const p = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1;
6473
- const radius = this.sizeStart + p * this.sizeEndDelta;
6474
- const size = vec2(radius);
6655
+ // lerp color and size
6656
+ const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6657
+ const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6658
+ this.size.x = this.size.y = radius;
6659
+ this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6660
+ this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6661
+ this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
6662
+ this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
6663
+
6664
+ // fade alpha
6475
6665
  const fadeRate = this.fadeRate/2;
6476
- const color = new Color(
6477
- this.colorStart.r + p * this.colorEndDelta.r,
6478
- this.colorStart.g + p * this.colorEndDelta.g,
6479
- this.colorStart.b + p * this.colorEndDelta.b,
6480
- (this.colorStart.a + p * this.colorEndDelta.a) *
6481
- (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
6666
+ this.color.a *= p1 < fadeRate ? p1/fadeRate :
6667
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
6482
6668
 
6483
6669
  // draw the particle
6484
6670
  this.additive && setBlendMode(true);
6485
6671
 
6672
+ // update the position and angle for drawing
6486
6673
  let pos = this.pos, angle = this.angle;
6487
6674
  if (this.localSpaceEmitter)
6488
6675
  {
6489
6676
  // in local space of emitter
6490
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6677
+ const a = this.localSpaceEmitter.angle;
6678
+ const c = Math.cos(a), s = Math.sin(a);
6679
+ pos = this.localSpaceEmitter.pos.add(
6680
+ new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6491
6681
  angle += this.localSpaceEmitter.angle;
6492
6682
  }
6493
6683
  if (this.trailScale)
6494
6684
  {
6495
6685
  // trail style particles
6496
- let velocity = this.velocity;
6497
- if (this.localSpaceEmitter)
6498
- velocity = velocity.rotate(-this.localSpaceEmitter.angle);
6499
- const speed = velocity.length();
6686
+ const direction = this.localSpaceEmitter ?
6687
+ this.velocity.rotate(-this.localSpaceEmitter.angle) :
6688
+ this.velocity;
6689
+ const speed = direction.length();
6500
6690
  if (speed)
6501
6691
  {
6502
- const direction = velocity.scale(1/speed);
6692
+ // stretch in direction of motion
6503
6693
  const trailLength = speed * this.trailScale;
6504
- size.y = max(size.x, trailLength);
6505
- angle = direction.angle();
6506
- drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileInfo, color, angle, this.mirror);
6694
+ this.size.y = max(this.size.x, trailLength);
6695
+ angle = Math.atan2(direction.x, direction.y);
6696
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6507
6697
  }
6508
6698
  }
6509
6699
  else
6510
- drawTile(pos, size, this.tileInfo, color, angle, this.mirror);
6700
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6511
6701
  this.additive && setBlendMode();
6512
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
6702
+ debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6513
6703
 
6514
- if (p === 1)
6704
+ if (p1 === 1)
6515
6705
  {
6516
- // destroy particle when it's time runs out
6517
- this.color = color;
6518
- this.size = size;
6706
+ // destroy particle when its time runs out
6519
6707
  this.destroyCallback && this.destroyCallback(this);
6520
6708
  this.destroyed = 1;
6521
6709
  }
@@ -6580,8 +6768,14 @@ function medalsInit(saveName)
6580
6768
  }
6581
6769
  }
6582
6770
 
6771
+ /**
6772
+ * @callback MedalCallbackFunction - Function that processes a medal
6773
+ * @param {Medal} medal
6774
+ * @memberof Medals
6775
+ */
6776
+
6583
6777
  /** Calls a function for each medal
6584
- * @param {Function} callback
6778
+ * @param {MedalCallbackFunction} callback
6585
6779
  * @memberof Medals */
6586
6780
  function medalsForEach(callback)
6587
6781
  { Object.values(medals).forEach(medal=>callback(medal)); }
@@ -6590,6 +6784,7 @@ function medalsForEach(callback)
6590
6784
 
6591
6785
  /**
6592
6786
  * Medal - Tracks an unlockable medal
6787
+ * @memberof Medals
6593
6788
  * @example
6594
6789
  * // create a medal
6595
6790
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
@@ -6658,11 +6853,12 @@ class Medal
6658
6853
  const height = medalDisplaySize.y;
6659
6854
  const x = overlayCanvas.width - width;
6660
6855
  const y = -height*hidePercent;
6856
+ const backgroundColor = hsl(0,0,.9);
6661
6857
 
6662
6858
  // draw containing rect and clip to that region
6663
6859
  context.save();
6664
6860
  context.beginPath();
6665
- context.fillStyle = new Color(.9,.9,.9).toString();
6861
+ context.fillStyle = backgroundColor.toString();
6666
6862
  context.strokeStyle = BLACK.toString();
6667
6863
  context.lineWidth = 3;
6668
6864
  context.rect(x, y, width, height);
@@ -7464,24 +7660,25 @@ function glPolyStrip(points)
7464
7660
  return strip;
7465
7661
  }
7466
7662
  /**
7467
- * LittleJS Newgrounds API
7663
+ * LittleJS Newgrounds Plugin
7468
7664
  * - NewgroundsMedal extends Medal with Newgrounds API functionality
7469
- * - Call new NewgroundsPlugin() to setup Newgrounds
7665
+ * - Call new NewgroundsPlugin(app_id) to setup Newgrounds
7470
7666
  * - Uses CryptoJS for encryption if optional cipher is provided
7667
+ * - provides functions to interact with medals scoreboards
7471
7668
  * - Keeps connection alive and logs views
7472
- * - Functions to interact with scoreboards
7473
- * - Functions to unlock medals
7669
+ * @namespace Newgrounds
7474
7670
  */
7475
7671
 
7476
7672
  /** Global Newgrounds object
7477
7673
  * @type {NewgroundsPlugin}
7478
- * @memberof Medal */
7674
+ * @memberof Newgrounds */
7479
7675
  let newgrounds;
7480
7676
 
7481
7677
  ///////////////////////////////////////////////////////////////////////////////
7482
7678
  /**
7483
7679
  * Newgrounds medal auto unlocks in newgrounds API
7484
7680
  * @extends Medal
7681
+ * @memberof Newgrounds
7485
7682
  */
7486
7683
  class NewgroundsMedal extends Medal
7487
7684
  {
@@ -7506,6 +7703,7 @@ class NewgroundsMedal extends Medal
7506
7703
  ///////////////////////////////////////////////////////////////////////////////
7507
7704
  /**
7508
7705
  * Newgrounds API object
7706
+ * @memberof Newgrounds
7509
7707
  */
7510
7708
  class NewgroundsPlugin
7511
7709
  {
@@ -7641,19 +7839,22 @@ class NewgroundsPlugin
7641
7839
  /**
7642
7840
  * LittleJS Post Processing Plugin
7643
7841
  * - Supports shadertoy style post processing shaders
7644
- * - call new new PostProcessPlugin() to setup post processing
7842
+ * - call new PostProcessPlugin() to setup post processing
7645
7843
  * - can be enabled to pass other canvases through a final shader
7844
+ * @namespace PostProcess
7646
7845
  */
7647
7846
 
7648
7847
  ///////////////////////////////////////////////////////////////////////////////
7649
7848
 
7650
7849
  /** Global Post Process plugin object
7651
- * @type {PostProcessPlugin} */
7850
+ * @type {PostProcessPlugin}
7851
+ * @memberof PostProcess */
7652
7852
  let postProcess;
7653
7853
 
7654
7854
  /////////////////////////////////////////////////////////////////////////
7655
7855
  /**
7656
7856
  * UI System Global Object
7857
+ * @memberof PostProcess
7657
7858
  */
7658
7859
  class PostProcessPlugin
7659
7860
  {
@@ -7761,12 +7962,15 @@ class PostProcessPlugin
7761
7962
  }
7762
7963
  /**
7763
7964
  * LittleJS ZzFXM Plugin
7965
+ * @namespace ZzFXM
7764
7966
  */
7765
7967
 
7766
7968
  /**
7767
7969
  * Music Object - Stores a zzfx music track for later use
7768
7970
  *
7769
7971
  * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
7972
+ * @extends Sound
7973
+ * @memberof ZzFXM
7770
7974
  * @example
7771
7975
  * // create some music
7772
7976
  * const music_example = new Music(
@@ -7825,7 +8029,8 @@ class ZzFXMusic extends Sound
7825
8029
  * @param {Array} patterns - Array of pattern data
7826
8030
  * @param {Array} sequence - Array of pattern indexes
7827
8031
  * @param {number} [BPM] - Playback speed of the song in BPM
7828
- * @return {Array} - Left and right channel sample data */
8032
+ * @return {Array} - Left and right channel sample data
8033
+ * @memberof ZzFXM */
7829
8034
  function zzfxM(instruments, patterns, sequence, BPM = 125)
7830
8035
  {
7831
8036
  let i, j, k;
@@ -7928,17 +8133,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
7928
8133
  * - Buttons
7929
8134
  * - Checkboxes
7930
8135
  * - Images
8136
+ * @namespace UISystem
7931
8137
  */
7932
8138
 
7933
8139
  ///////////////////////////////////////////////////////////////////////////////
7934
8140
 
7935
8141
  /** Global UI system plugin object
7936
- * @type {UISystemPlugin} */
8142
+ * @type {UISystemPlugin}
8143
+ * @memberof UISystem */
7937
8144
  let uiSystem;
7938
8145
 
7939
8146
  ///////////////////////////////////////////////////////////////////////////////
7940
8147
  /**
7941
8148
  * UI System Global Object
8149
+ * @memberof UISystem
7942
8150
  */
7943
8151
  class UISystemPlugin
7944
8152
  {
@@ -7983,10 +8191,12 @@ class UISystemPlugin
7983
8191
  this.uiObjects = [];
7984
8192
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
7985
8193
  this.uiContext = context;
7986
- /** @property {UIObject} - Top most object user is over */
7987
- this.hoverObject = undefined;
7988
8194
  /** @property {UIObject} - Object user is currently interacting with */
7989
8195
  this.activeObject = undefined;
8196
+ /** @property {UIObject} - Top most object user is over */
8197
+ this.hoverObject = undefined;
8198
+ /** @property {UIObject} - Hover object at start of update */
8199
+ this.lastHoverObject = undefined;
7990
8200
 
7991
8201
  engineAddPlugin(uiUpdate, uiRender);
7992
8202
 
@@ -8017,6 +8227,7 @@ class UISystemPlugin
8017
8227
  updateInvisibleObject(o);
8018
8228
  }
8019
8229
  // reset hover object at start of update
8230
+ uiSystem.lastHoverObject = uiSystem.hoverObject;
8020
8231
  uiSystem.hoverObject = undefined;
8021
8232
  for (let i = uiSystem.uiObjects.length; i--;)
8022
8233
  {
@@ -8049,6 +8260,13 @@ class UISystemPlugin
8049
8260
  * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
8050
8261
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
8051
8262
  {
8263
+ ASSERT(isVector2(pos), 'pos must be a vec2');
8264
+ ASSERT(isVector2(size), 'size must be a vec2');
8265
+ ASSERT(isColor(color), 'color must be a color');
8266
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
8267
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
8268
+ ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
8269
+
8052
8270
  const context = uiSystem.uiContext;
8053
8271
  context.fillStyle = color.toString();
8054
8272
  context.beginPath();
@@ -8072,6 +8290,11 @@ class UISystemPlugin
8072
8290
  * @param {Color} [lineColor=uiSystem.defaultLineColor] */
8073
8291
  drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
8074
8292
  {
8293
+ ASSERT(isVector2(posA), 'posA must be a vec2');
8294
+ ASSERT(isVector2(posB), 'posB must be a vec2');
8295
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
8296
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
8297
+
8075
8298
  const context = uiSystem.uiContext;
8076
8299
  context.strokeStyle = lineColor.toString();
8077
8300
  context.lineWidth = lineWidth;
@@ -8107,12 +8330,37 @@ class UISystemPlugin
8107
8330
  {
8108
8331
  drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8109
8332
  }
8333
+
8334
+ /**
8335
+ * @callback DragAndDropCallback - Callback for drag and drop events
8336
+ * @param {DragEvent} event - The drag event
8337
+ * @memberof UISystem
8338
+ */
8339
+
8340
+ /** Setup drag and drop event handlers
8341
+ * Automatically prevents defaults and calls the given functions
8342
+ * @param {DragAndDropCallback} [onDrop] - when a file is dropped
8343
+ * @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
8344
+ * @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
8345
+ * @param {DragAndDropCallback} [onDragOver] - continously when dragging over */
8346
+ setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
8347
+ {
8348
+ function setCallback(callback, listenerType)
8349
+ {
8350
+ function listener(e) { e.preventDefault(); callback && callback(e); }
8351
+ document.addEventListener(listenerType, listener);
8352
+ }
8353
+ setCallback(onDrop, 'drop');
8354
+ setCallback(onDragEnter, 'dragenter');
8355
+ setCallback(onDragLeave, 'dragleave');
8356
+ setCallback(onDragOver, 'dragover');
8357
+ }
8110
8358
  }
8111
8359
 
8112
8360
  ///////////////////////////////////////////////////////////////////////////////
8113
8361
  /**
8114
8362
  * UI Object - Base level object for all UI elements
8115
- */
8363
+ * @memberof UISystem */
8116
8364
  class UIObject
8117
8365
  {
8118
8366
  /** Create a UIObject
@@ -8121,6 +8369,9 @@ class UIObject
8121
8369
  */
8122
8370
  constructor(pos=vec2(), size=vec2())
8123
8371
  {
8372
+ ASSERT(isVector2(pos), 'ui object pos must be a vec2');
8373
+ ASSERT(isVector2(size), 'ui object size must be a vec2');
8374
+
8124
8375
  /** @property {Vector2} - Local position of the object */
8125
8376
  this.localPos = pos.copy();
8126
8377
  /** @property {Vector2} - Screen space position of the object */
@@ -8128,21 +8379,21 @@ class UIObject
8128
8379
  /** @property {Vector2} - Screen space size of the object */
8129
8380
  this.size = size.copy();
8130
8381
  /** @property {Color} - Color of the object */
8131
- this.color = uiSystem.defaultColor;
8382
+ this.color = uiSystem.defaultColor.copy();
8132
8383
  /** @property {Color} - Color of the object when active, uses color if undefined */
8133
8384
  this.activeColor = undefined;
8134
8385
  /** @property {string} - Text for this ui object */
8135
8386
  this.text = undefined;
8136
8387
  /** @property {Color} - Color when disabled */
8137
- this.disabledColor = uiSystem.defaultDisabledColor;
8388
+ this.disabledColor = uiSystem.defaultDisabledColor.copy();
8138
8389
  /** @property {boolean} - Is this object disabled? */
8139
8390
  this.disabled = false;
8140
8391
  /** @property {Color} - Color for text */
8141
- this.textColor = uiSystem.defaultTextColor;
8392
+ this.textColor = uiSystem.defaultTextColor.copy()
8142
8393
  /** @property {Color} - Color used when hovering over the object */
8143
- this.hoverColor = uiSystem.defaultHoverColor;
8394
+ this.hoverColor = uiSystem.defaultHoverColor.copy()
8144
8395
  /** @property {Color} - Color for line drawing */
8145
- this.lineColor = uiSystem.defaultLineColor;
8396
+ this.lineColor = uiSystem.defaultLineColor.copy()
8146
8397
  /** @property {number} - Width for line drawing */
8147
8398
  this.lineWidth = uiSystem.defaultLineWidth;
8148
8399
  /** @property {number} - Corner radius for rounded rects */
@@ -8199,7 +8450,7 @@ class UIObject
8199
8450
  /** Update the object, called automatically by plugin once each frame */
8200
8451
  update()
8201
8452
  {
8202
- const wasHover = this.isHoverObject();
8453
+ const wasHover = uiSystem.lastHoverObject === this;
8203
8454
  const isActive = this.isActiveObject();
8204
8455
  const mouseDown = mouseIsDown(0);
8205
8456
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
@@ -8212,15 +8463,14 @@ class UIObject
8212
8463
  }
8213
8464
  if (this.isHoverObject())
8214
8465
  {
8215
- if (mousePress)
8216
- inputClearKey(0,0,0,1,0); // clear mouse was pressed state
8217
8466
  if (!this.disabled)
8218
8467
  {
8219
8468
  if (mousePress)
8220
8469
  {
8221
8470
  if (this.interactive)
8222
8471
  {
8223
- this.onPress();
8472
+ if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
8473
+ this.onPress();
8224
8474
  if (this.soundPress)
8225
8475
  this.soundPress.play();
8226
8476
  if (uiSystem.activeObject && !isActive)
@@ -8228,13 +8478,15 @@ class UIObject
8228
8478
  uiSystem.activeObject = this;
8229
8479
  }
8230
8480
  }
8231
- if (!mouseDown && uiSystem.activeObject === this && this.interactive)
8481
+ if (!mouseDown && this.isActiveObject() && this.interactive)
8232
8482
  {
8233
8483
  this.onClick();
8234
8484
  if (this.soundClick)
8235
8485
  this.soundClick.play();
8236
8486
  }
8237
8487
  }
8488
+ // clear mouse was pressed state even when disabled
8489
+ mousePress && inputClearKey(0,0,0,1,0);
8238
8490
  }
8239
8491
  if (isActive)
8240
8492
  if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
@@ -8245,6 +8497,7 @@ class UIObject
8245
8497
  uiSystem.activeObject = undefined;
8246
8498
  }
8247
8499
 
8500
+ // call enter/leave events
8248
8501
  if (this.isHoverObject() !== wasHover)
8249
8502
  this.isHoverObject() ? this.onEnter() : this.onLeave();
8250
8503
  }
@@ -8255,7 +8508,7 @@ class UIObject
8255
8508
  if (!this.size.x || !this.size.y) return;
8256
8509
 
8257
8510
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8258
- const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8511
+ const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8259
8512
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8260
8513
  }
8261
8514
 
@@ -8306,6 +8559,7 @@ class UIObject
8306
8559
  /**
8307
8560
  * UIText - A UI object that displays text
8308
8561
  * @extends UIObject
8562
+ * @memberof UISystem
8309
8563
  */
8310
8564
  class UIText extends UIObject
8311
8565
  {
@@ -8320,6 +8574,10 @@ class UIText extends UIObject
8320
8574
  {
8321
8575
  super(pos, size);
8322
8576
 
8577
+ ASSERT(isString(text), 'ui text must be a string');
8578
+ ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
8579
+ ASSERT(isString(font), 'ui text font must be a string');
8580
+
8323
8581
  // set properties
8324
8582
  this.text = text;
8325
8583
  this.align = align;
@@ -8339,6 +8597,7 @@ class UIText extends UIObject
8339
8597
  /**
8340
8598
  * UITile - A UI object that displays a tile image
8341
8599
  * @extends UIObject
8600
+ * @memberof UISystem
8342
8601
  */
8343
8602
  class UITile extends UIObject
8344
8603
  {
@@ -8353,15 +8612,19 @@ class UITile extends UIObject
8353
8612
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
8354
8613
  {
8355
8614
  super(pos, size);
8615
+
8616
+ ASSERT(tileInfo instanceof TileInfo, 'ui tile tileInfo must be a TileInfo');
8617
+ ASSERT(isColor(color), 'ui tile color must be a color');
8618
+ ASSERT(isNumber(angle), 'ui tile angle must be a number');
8619
+
8356
8620
  /** @property {TileInfo} - Tile image to use */
8357
8621
  this.tileInfo = tileInfo;
8358
8622
  /** @property {number} - Angle to rotate in radians */
8359
8623
  this.angle = angle;
8360
8624
  /** @property {boolean} - Should it be mirrored? */
8361
8625
  this.mirror = mirror;
8362
-
8363
8626
  // set properties
8364
- this.color = color;
8627
+ this.color = color.copy();
8365
8628
  }
8366
8629
  render()
8367
8630
  {
@@ -8373,6 +8636,7 @@ class UITile extends UIObject
8373
8636
  /**
8374
8637
  * UIButton - A UI object that acts as a button
8375
8638
  * @extends UIObject
8639
+ * @memberof UISystem
8376
8640
  */
8377
8641
  class UIButton extends UIObject
8378
8642
  {
@@ -8386,9 +8650,12 @@ class UIButton extends UIObject
8386
8650
  {
8387
8651
  super(pos, size);
8388
8652
 
8653
+ ASSERT(isString(text), 'ui button must be a string');
8654
+ ASSERT(isColor(color), 'ui button color must be a color');
8655
+
8389
8656
  // set properties
8390
8657
  this.text = text;
8391
- this.color = color;
8658
+ this.color = color.copy()
8392
8659
  this.interactive = true;
8393
8660
  }
8394
8661
  render()
@@ -8406,6 +8673,7 @@ class UIButton extends UIObject
8406
8673
  /**
8407
8674
  * UICheckbox - A UI object that acts as a checkbox
8408
8675
  * @extends UIObject
8676
+ * @memberof UISystem
8409
8677
  */
8410
8678
  class UICheckbox extends UIObject
8411
8679
  {
@@ -8419,12 +8687,15 @@ class UICheckbox extends UIObject
8419
8687
  constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
8420
8688
  {
8421
8689
  super(pos, size);
8690
+
8691
+ ASSERT(isString(text), 'ui checkbox must be a string');
8692
+ ASSERT(isColor(color), 'ui checkbox color must be a color');
8693
+
8422
8694
  /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
8423
8695
  this.checked = checked;
8424
-
8425
8696
  // set properties
8426
8697
  this.text = text;
8427
- this.color = color;
8698
+ this.color = color.copy();
8428
8699
  this.interactive = true;
8429
8700
  }
8430
8701
  onClick()
@@ -8456,6 +8727,7 @@ class UICheckbox extends UIObject
8456
8727
  /**
8457
8728
  * UIScrollbar - A UI object that acts as a scrollbar
8458
8729
  * @extends UIObject
8730
+ * @memberof UISystem
8459
8731
  */
8460
8732
  class UIScrollbar extends UIObject
8461
8733
  {
@@ -8471,14 +8743,19 @@ class UIScrollbar extends UIObject
8471
8743
  {
8472
8744
  super(pos, size);
8473
8745
 
8746
+ ASSERT(isNumber(value), 'ui scrollbar value must be a number');
8747
+ ASSERT(isString(text), 'ui scrollbar must be a string');
8748
+ ASSERT(isColor(color), 'ui scrollbar color must be a color');
8749
+ ASSERT(isColor(handleColor), 'ui scrollbar handleColor must be a color');
8750
+
8474
8751
  /** @property {number} - Current percentage value of this scrollbar 0-1 */
8475
8752
  this.value = value;
8476
8753
  /** @property {Color} - Color for the handle part of the scrollbar */
8477
- this.handleColor = handleColor;
8754
+ this.handleColor = handleColor.copy();
8478
8755
 
8479
8756
  // set properties
8480
8757
  this.text = text;
8481
- this.color = color;
8758
+ this.color = color.copy();
8482
8759
  this.interactive = true;
8483
8760
  }
8484
8761
  update()
@@ -8486,29 +8763,43 @@ class UIScrollbar extends UIObject
8486
8763
  super.update();
8487
8764
  if (this.isActiveObject() && this.interactive)
8488
8765
  {
8766
+ // handle horizontal or vertical scrollbar
8767
+ const isHorizontal = this.size.x > this.size.y;
8768
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8769
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8770
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8771
+
8489
8772
  // check if value changed
8490
- const handleSize = vec2(this.size.y);
8491
- const handleWidth = this.size.x - handleSize.x;
8492
- const p1 = this.pos.x - handleWidth/2;
8493
- const p2 = this.pos.x + handleWidth/2;
8773
+ const handleWidth = barSize - handleSize;
8774
+ const p1 = centerPos - handleWidth/2;
8775
+ const p2 = centerPos + handleWidth/2;
8494
8776
  const oldValue = this.value;
8495
- this.value = percent(mousePosScreen.x, p1, p2);
8777
+ this.value = isHorizontal ?
8778
+ percent(mousePosScreen.x, p1, p2) :
8779
+ percent(mousePosScreen.y, p2, p1);
8496
8780
  this.value === oldValue || this.onChange();
8497
8781
  }
8498
8782
  }
8499
8783
  render()
8500
8784
  {
8501
8785
  super.render();
8502
-
8786
+
8787
+ // handle horizontal or vertical scrollbar
8788
+ const isHorizontal = this.size.x > this.size.y;
8789
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8790
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8791
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8792
+
8503
8793
  // draw the scrollbar handle
8504
- const handleSize = vec2(this.size.y);
8505
- const handleWidth = this.size.x - handleSize.x;
8506
- const p1 = this.pos.x - handleWidth/2;
8507
- const p2 = this.pos.x + handleWidth/2;
8508
- const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
8794
+ const handleWidth = barSize - handleSize;
8795
+ const p1 = centerPos - handleWidth/2;
8796
+ const p2 = centerPos + handleWidth/2;
8797
+ const handlePos = isHorizontal ?
8798
+ vec2(lerp(p1, p2, this.value), this.pos.y) :
8799
+ vec2(this.pos.x, lerp(p2, p1, this.value))
8509
8800
  const handleColor = this.disabled ? this.disabledColor :
8510
8801
  this.interactive && this.isActiveObject() ? this.color : this.handleColor;
8511
- uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8802
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8512
8803
 
8513
8804
  // draw the text scaled to fit on the scrollbar
8514
8805
  const textSize = this.getTextSize();
@@ -8555,6 +8846,7 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
8555
8846
  * - Each object has a Box2D body which can have multiple fixtures and joints
8556
8847
  * - Provides interface for Box2D body and fixture functions
8557
8848
  * @extends EngineObject
8849
+ * @memberof Box2D
8558
8850
  */
8559
8851
  class Box2dObject extends EngineObject
8560
8852
  {
@@ -8566,7 +8858,7 @@ class Box2dObject extends EngineObject
8566
8858
  * @param {Color} [color]
8567
8859
  * @param {number} [bodyType]
8568
8860
  * @param {number} [renderOrder] */
8569
- constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8861
+ constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8570
8862
  {
8571
8863
  super(pos, size, tileInfo, angle, color, renderOrder);
8572
8864
 
@@ -8580,7 +8872,7 @@ class Box2dObject extends EngineObject
8580
8872
  this.lineColor = BLACK;
8581
8873
  }
8582
8874
 
8583
- /** Destroy this object and it's physics body */
8875
+ /** Destroy this object and its physics body */
8584
8876
  destroy()
8585
8877
  {
8586
8878
  // destroy physics body, fixtures, and joints
@@ -9064,6 +9356,7 @@ class Box2dRaycastResult
9064
9356
  * Box2D Joint
9065
9357
  * - Base class for Box2D joints
9066
9358
  * - A joint is used to connect objects together
9359
+ * @memberof Box2D
9067
9360
  */
9068
9361
  class Box2dJoint
9069
9362
  {
@@ -9119,6 +9412,7 @@ class Box2dJoint
9119
9412
  * - This a soft constraint with a max force
9120
9413
  * - This allows the constraint to stretch and without applying huge forces
9121
9414
  * @extends Box2dJoint
9415
+ * @memberof Box2D
9122
9416
  */
9123
9417
  class Box2dTargetJoint extends Box2dJoint
9124
9418
  {
@@ -9168,6 +9462,7 @@ class Box2dTargetJoint extends Box2dJoint
9168
9462
  * - Constrains two points on two objects to remain at a fixed distance
9169
9463
  * - You can view this as a massless, rigid rod
9170
9464
  * @extends Box2dJoint
9465
+ * @memberof Box2D
9171
9466
  */
9172
9467
  class Box2dDistanceJoint extends Box2dJoint
9173
9468
  {
@@ -9231,6 +9526,7 @@ class Box2dDistanceJoint extends Box2dJoint
9231
9526
  * Box2D Pin Joint
9232
9527
  * - Pins two objects together at a point
9233
9528
  * @extends Box2dDistanceJoint
9529
+ * @memberof Box2D
9234
9530
  */
9235
9531
  class Box2dPinJoint extends Box2dDistanceJoint
9236
9532
  {
@@ -9250,6 +9546,7 @@ class Box2dPinJoint extends Box2dDistanceJoint
9250
9546
  * Box2D Rope Joint
9251
9547
  * - Enforces a maximum distance between two points on two objects
9252
9548
  * @extends Box2dJoint
9549
+ * @memberof Box2D
9253
9550
  */
9254
9551
  class Box2dRopeJoint extends Box2dJoint
9255
9552
  {
@@ -9302,6 +9599,7 @@ class Box2dRopeJoint extends Box2dJoint
9302
9599
  * - You can use a motor to drive the relative rotation about the shared point
9303
9600
  * - A maximum motor torque is provided so that infinite forces are not generated
9304
9601
  * @extends Box2dJoint
9602
+ * @memberof Box2D
9305
9603
  */
9306
9604
  class Box2dRevoluteJoint extends Box2dJoint
9307
9605
  {
@@ -9403,6 +9701,7 @@ class Box2dRevoluteJoint extends Box2dJoint
9403
9701
  * - Either joint can be a revolute or prismatic joint
9404
9702
  * - You specify a gear ratio to bind the motions together
9405
9703
  * @extends Box2dJoint
9704
+ * @memberof Box2D
9406
9705
  */
9407
9706
  class Box2dGearJoint extends Box2dJoint
9408
9707
  {
@@ -9451,6 +9750,7 @@ class Box2dGearJoint extends Box2dJoint
9451
9750
  * - You can use a joint limit to restrict the range of motion
9452
9751
  * - You can use a joint motor to drive the motion or to model joint friction
9453
9752
  * @extends Box2dJoint
9753
+ * @memberof Box2D
9454
9754
  */
9455
9755
  class Box2dPrismaticJoint extends Box2dJoint
9456
9756
  {
@@ -9560,6 +9860,7 @@ class Box2dPrismaticJoint extends Box2dJoint
9560
9860
  * - You can use a joint motor to drive the motion or to model joint friction
9561
9861
  * - This joint is designed for vehicle suspensions
9562
9862
  * @extends Box2dJoint
9863
+ * @memberof Box2D
9563
9864
  */
9564
9865
  class Box2dWheelJoint extends Box2dJoint
9565
9866
  {
@@ -9655,6 +9956,7 @@ class Box2dWheelJoint extends Box2dJoint
9655
9956
  * Box2D Weld Joint
9656
9957
  * - Glues two objects together
9657
9958
  * @extends Box2dJoint
9959
+ * @memberof Box2D
9658
9960
  */
9659
9961
  class Box2dWeldJoint extends Box2dJoint
9660
9962
  {
@@ -9713,6 +10015,7 @@ class Box2dWeldJoint extends Box2dJoint
9713
10015
  * - Used to apply top-down friction
9714
10016
  * - Provides 2D translational friction and angular friction
9715
10017
  * @extends Box2dJoint
10018
+ * @memberof Box2D
9716
10019
  */
9717
10020
  class Box2dFrictionJoint extends Box2dJoint
9718
10021
  {
@@ -9767,6 +10070,7 @@ class Box2dFrictionJoint extends Box2dJoint
9767
10070
  * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
9768
10071
  * - The force transmitted is scaled by the ratio
9769
10072
  * @extends Box2dJoint
10073
+ * @memberof Box2D
9770
10074
  */
9771
10075
  class Box2dPulleyJoint extends Box2dJoint
9772
10076
  {
@@ -9834,6 +10138,7 @@ class Box2dPulleyJoint extends Box2dJoint
9834
10138
  * - Controls the relative motion between two objects
9835
10139
  * - Typical usage is to control the movement of a object with respect to the ground
9836
10140
  * @extends Box2dJoint
10141
+ * @memberof Box2D
9837
10142
  */
9838
10143
  class Box2dMotorJoint extends Box2dJoint
9839
10144
  {
@@ -9897,6 +10202,7 @@ class Box2dMotorJoint extends Box2dJoint
9897
10202
  /**
9898
10203
  * Box2D Global Object
9899
10204
  * - Wraps Box2d world and provides global functions
10205
+ * @memberof Box2D
9900
10206
  */
9901
10207
  class Box2dPlugin
9902
10208
  {
@@ -10615,6 +10921,7 @@ export
10615
10921
  isColor,
10616
10922
  isVector2,
10617
10923
  isNumber,
10924
+ isString,
10618
10925
 
10619
10926
  // Default Colors
10620
10927
  WHITE,
@@ -10750,7 +11057,7 @@ export
10750
11057
  tileCollisionGetData,
10751
11058
  tileCollisionTest,
10752
11059
  tileCollisionRaycast,
10753
- tileCollisionLoad,
11060
+ tileLayersLoad,
10754
11061
  TileLayerData,
10755
11062
  CanvasLayer,
10756
11063
  TileLayer,