littlejsengine 1.9.6 → 1.9.8

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.
@@ -26,13 +26,16 @@ function debugInit (){}
26
26
  function debugUpdate (){}
27
27
  function debugRender (){}
28
28
  function debugRect (){}
29
+ function debugPoly (){}
29
30
  function debugCircle (){}
30
31
  function debugPoint (){}
31
32
  function debugLine (){}
32
- function debugAABB (){}
33
+ function debugOverlap (){}
33
34
  function debugText (){}
34
35
  function debugClear (){}
35
- function debugSaveCanvas (){}
36
+ function debugSaveCanvas (){}
37
+ function debugSaveText (){}
38
+ function debugSaveDataURL(){}
36
39
  /**
37
40
  * LittleJS Utility Classes and Functions
38
41
  * - General purpose math library
@@ -336,7 +339,7 @@ class RandomGenerator
336
339
  */
337
340
  function vec2(x=0, y)
338
341
  {
339
- return typeof x === 'number' ?
342
+ return typeof x == 'number' ?
340
343
  new Vector2(x, y == undefined? x : y) :
341
344
  new Vector2(x.x, x.y);
342
345
  }
@@ -365,13 +368,19 @@ class Vector2
365
368
  * @param {Number} [y] - Y axis location */
366
369
  constructor(x=0, y=0)
367
370
  {
368
- ASSERT(typeof x === 'number' && typeof y === 'number');
371
+ ASSERT(typeof x == 'number' && typeof y == 'number');
369
372
  /** @property {Number} - X axis location */
370
373
  this.x = x;
371
374
  /** @property {Number} - Y axis location */
372
375
  this.y = y;
373
376
  }
374
377
 
378
+ /** Sets values of this vector and returns self
379
+ * @param {Number} [x] - X axis location
380
+ * @param {Number} [y] - Y axis location
381
+ * @return {Vector2} */
382
+ set(x=0, y=0) { this.x=x; this.y=y; return this; }
383
+
375
384
  /** Returns a new vector that is a copy of this
376
385
  * @return {Vector2} */
377
386
  copy() { return new Vector2(this.x, this.y); }
@@ -623,6 +632,15 @@ class Color
623
632
  this.a = a;
624
633
  }
625
634
 
635
+ /** Sets values of this color and returns self
636
+ * @param {Number} [r] - red
637
+ * @param {Number} [g] - green
638
+ * @param {Number} [b] - blue
639
+ * @param {Number} [a] - alpha
640
+ * @return {Color} */
641
+ set(r=1, g=1, b=1, a=1)
642
+ { this.r=r; this.g=g; this.b=b; this.a=a; return this; }
643
+
626
644
  /** Returns a new color that is a copy of this
627
645
  * @return {Color} */
628
646
  copy() { return new Color(this.r, this.g, this.b, this.a); }
@@ -750,14 +768,15 @@ class Color
750
768
  ).clamp();
751
769
  }
752
770
 
753
- /** Returns this color expressed as a rgb color code
771
+ /** Returns this color expressed as a hex color code
754
772
  * @param {Boolean} [useAlpha] - if alpha should be included in result
755
773
  * @return {String} */
756
- toString(useAlpha = true)
757
- {
758
- return `rgb(${this.r*255},${this.g*255},${this.b*255},${useAlpha ? this.a : 0})`;
774
+ toString(useAlpha = true)
775
+ {
776
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
777
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
759
778
  }
760
-
779
+
761
780
  /** Set this color from a hex code
762
781
  * @param {String} hex - html hex code
763
782
  * @return {Color} */
@@ -783,6 +802,64 @@ class Color
783
802
  }
784
803
  }
785
804
 
805
+ ///////////////////////////////////////////////////////////////////////////////
806
+ // default colors
807
+
808
+ /** Color - White
809
+ * @type {Color}
810
+ * @memberof Utilities */
811
+ const WHITE = rgb();
812
+
813
+ /** Color - Black
814
+ * @type {Color}
815
+ * @memberof Utilities */
816
+ const BLACK = rgb(0,0,0);
817
+
818
+ /** Color - Gray
819
+ * @type {Color}
820
+ * @memberof Utilities */
821
+ const GRAY = rgb(.5,.5,.5);
822
+
823
+ /** Color - Red
824
+ * @type {Color}
825
+ * @memberof Utilities */
826
+ const RED = rgb(1,0,0);
827
+
828
+ /** Color - Orange
829
+ * @type {Color}
830
+ * @memberof Utilities */
831
+ const ORANGE = rgb(1,.5,0);
832
+
833
+ /** Color - Yellow
834
+ * @type {Color}
835
+ * @memberof Utilities */
836
+ const YELLOW = rgb(1,1,0);
837
+
838
+ /** Color - Green
839
+ * @type {Color}
840
+ * @memberof Utilities */
841
+ const GREEN = rgb(0,1,0);
842
+
843
+ /** Color - Cyan
844
+ * @type {Color}
845
+ * @memberof Utilities */
846
+ const CYAN = rgb(0,1,1);
847
+
848
+ /** Color - Blue
849
+ * @type {Color}
850
+ * @memberof Utilities */
851
+ const BLUE = rgb(0,0,1);
852
+
853
+ /** Color - Purple
854
+ * @type {Color}
855
+ * @memberof Utilities */
856
+ const PURPLE = rgb(.5,0,1);
857
+
858
+ /** Color - Magenta
859
+ * @type {Color}
860
+ * @memberof Utilities */
861
+ const MAGENTA = rgb(1,0,1);
862
+
786
863
  ///////////////////////////////////////////////////////////////////////////////
787
864
 
788
865
  /**
@@ -863,9 +940,9 @@ let cameraScale = 32;
863
940
 
864
941
  /** The max size of the canvas, centered if window is larger
865
942
  * @type {Vector2}
866
- * @default Vector2(1920,1200)
943
+ * @default Vector2(1920,1080)
867
944
  * @memberof Settings */
868
- let canvasMaxSize = vec2(1920, 1200);
945
+ let canvasMaxSize = vec2(1920, 1080);
869
946
 
870
947
  /** Fixed size of the canvas, if enabled canvas size never changes
871
948
  * - you may also need to set mainCanvasSize if using screen space coords in startup
@@ -1254,7 +1331,12 @@ function setSoundEnable(enable) { soundEnable = enable; }
1254
1331
  /** Set volume scale to apply to all sound, music and speech
1255
1332
  * @param {Number} volume
1256
1333
  * @memberof Settings */
1257
- function setSoundVolume(volume) { soundVolume = volume; }
1334
+ function setSoundVolume(volume)
1335
+ {
1336
+ soundVolume = volume;
1337
+ if (soundEnable && !headlessMode && audioGainNode)
1338
+ audioGainNode.gain.value = volume; // update gain immediatly
1339
+ }
1258
1340
 
1259
1341
  /** Set default range where sound no longer plays
1260
1342
  * @param {Number} range
@@ -1387,6 +1469,8 @@ class EngineObject
1387
1469
  this.spawnTime = time;
1388
1470
  /** @property {Array} - List of children of this object */
1389
1471
  this.children = [];
1472
+ /** @property {Boolean} - Limit object speed using linear or circular math */
1473
+ this.clampSpeedLinear = true;
1390
1474
 
1391
1475
  // parent child system
1392
1476
  /** @property {EngineObject} - Parent of object if in local space */
@@ -1435,8 +1519,21 @@ class EngineObject
1435
1519
  return;
1436
1520
 
1437
1521
  // limit max speed to prevent missing collisions
1438
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1439
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1522
+ if (this.clampSpeedLinear)
1523
+ {
1524
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1525
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1526
+ }
1527
+ else
1528
+ {
1529
+ const length2 = this.velocity.lengthSquared();
1530
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
1531
+ {
1532
+ const s = objectMaxSpeed / length2**.5;
1533
+ this.velocity.x *= s;
1534
+ this.velocity.y *= s;
1535
+ }
1536
+ }
1440
1537
 
1441
1538
  // apply physics
1442
1539
  const oldPos = this.pos.copy();
@@ -1492,7 +1589,7 @@ class EngineObject
1492
1589
  if (o.mass) // push away if not fixed
1493
1590
  o.velocity = o.velocity.subtract(velocity);
1494
1591
 
1495
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f00');
1592
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
1496
1593
  continue;
1497
1594
  }
1498
1595
 
@@ -1554,7 +1651,7 @@ class EngineObject
1554
1651
  else // bounce if other object is fixed
1555
1652
  this.velocity.x *= -elasticity;
1556
1653
  }
1557
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
1654
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
1558
1655
  }
1559
1656
  }
1560
1657
  if (this.collideTiles)
@@ -1615,6 +1712,22 @@ class EngineObject
1615
1712
  for (const child of this.children)
1616
1713
  child.destroy(child.parent = 0);
1617
1714
  }
1715
+
1716
+ /** Convert from local space to world space
1717
+ * @param {Vector2} pos - local space point */
1718
+ localToWorld(pos) { return this.pos.add(pos.rotate(-this.angle)); }
1719
+
1720
+ /** Convert from world space to local space
1721
+ * @param {Vector2} pos - world space point */
1722
+ worldToLocal(pos) { return pos.subtract(this.pos).rotate(this.angle); }
1723
+
1724
+ /** Convert from local space to world space for a vector (rotation only)
1725
+ * @param {Vector2} vec - local space vector */
1726
+ localToWorldVector(vec) { return vec.rotate(this.angle); }
1727
+
1728
+ /** Convert from world space to local space for a vector (rotation only)
1729
+ * @param {Vector2} vec - world space vector */
1730
+ worldToLocalVector(vec) { return vec.rotate(-this.angle); }
1618
1731
 
1619
1732
  /** Called to check if a tile collision should be resolved
1620
1733
  * @param {Number} tileData - the value of the tile at the position
@@ -1701,6 +1814,18 @@ class EngineObject
1701
1814
  return text;
1702
1815
  }
1703
1816
  }
1817
+
1818
+ /** Render debug info for this object */
1819
+ renderDebugInfo()
1820
+ {
1821
+ // show object info for debugging
1822
+ const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
1823
+ const color1 = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, this.parent?.2:.5);
1824
+ const color2 = this.parent ? rgb(1,1,1,.5) : rgb(0,0,0,.8);
1825
+ drawRect(this.pos, size, color1, this.angle, false);
1826
+ drawRect(this.pos, size.scale(.8), color2, this.angle, false);
1827
+ this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(0,0,1,.5), false);
1828
+ }
1704
1829
  }
1705
1830
  /**
1706
1831
  * LittleJS Drawing System
@@ -2024,7 +2149,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2024
2149
  context.save();
2025
2150
  context.translate(pos.x+.5, pos.y+.5);
2026
2151
  context.rotate(angle);
2027
- context.scale(mirror ? -size.x : size.x, size.y);
2152
+ context.scale(mirror ? -size.x : size.x, -size.y);
2028
2153
  drawFunction(context);
2029
2154
  context.restore();
2030
2155
  }
@@ -2719,6 +2844,32 @@ function touchGamepadRender()
2719
2844
 
2720
2845
 
2721
2846
 
2847
+ /** Audio context used by the engine
2848
+ * @type {AudioContext}
2849
+ * @memberof Audio */
2850
+ let audioContext;
2851
+
2852
+ /** Master gain node for all audio to pass through
2853
+ * @type {GainNode}
2854
+ * @memberof Audio */
2855
+ let audioGainNode;
2856
+
2857
+ function audioInit()
2858
+ {
2859
+ if (!soundEnable || headlessMode) return;
2860
+
2861
+ // create audio context
2862
+ audioContext = new AudioContext;
2863
+
2864
+ // create and connect gain node
2865
+ // (createGain is more widely spported then GainNode construtor)
2866
+ audioGainNode = audioContext.createGain();
2867
+ audioGainNode.connect(audioContext.destination);
2868
+ setSoundVolume(soundVolume); // update gain volume
2869
+ }
2870
+
2871
+ ///////////////////////////////////////////////////////////////////////////////
2872
+
2722
2873
  /**
2723
2874
  * Sound Object - Stores a sound for later use and can be played positionally
2724
2875
  *
@@ -2770,7 +2921,8 @@ class Sound
2770
2921
  */
2771
2922
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
2772
2923
  {
2773
- if (!soundEnable || !this.sampleChannels || headlessMode) return;
2924
+ if (!soundEnable || headlessMode) return;
2925
+ if (!this.sampleChannels) return;
2774
2926
 
2775
2927
  let pan;
2776
2928
  if (pos)
@@ -2793,7 +2945,17 @@ class Sound
2793
2945
 
2794
2946
  // play the sound
2795
2947
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
2796
- return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate);
2948
+ this.gainNode = audioContext.createGain();
2949
+ return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
2950
+ }
2951
+
2952
+ /** Set the sound volume
2953
+ * @param {Number} [volume] - How much to scale volume by
2954
+ */
2955
+ setVolume(volume=1)
2956
+ {
2957
+ if (this.gainNode)
2958
+ this.gainNode.gain.value = volume;
2797
2959
  }
2798
2960
 
2799
2961
  /** Stop the last instance of this sound that was played */
@@ -2847,16 +3009,14 @@ class SoundWave extends Sound
2847
3009
  * @param {Number} [randomness] - How much to randomize frequency each time sound plays
2848
3010
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2849
3011
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
3012
+ * @param {Function} [onloadCallback] - callback function to call when sound is loaded
2850
3013
  */
2851
- constructor(filename, randomness=0, range, taper)
3014
+ constructor(filename, randomness=0, range, taper, onloadCallback)
2852
3015
  {
2853
3016
  super(undefined, range, taper);
2854
- this.randomness = randomness;
2855
-
2856
3017
  if (!soundEnable || headlessMode) return;
2857
- if (!audioContext)
2858
- audioContext = new AudioContext; // create audio context
2859
3018
 
3019
+ this.randomness = randomness;
2860
3020
  fetch(filename)
2861
3021
  .then(response => response.arrayBuffer())
2862
3022
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
@@ -2866,10 +3026,23 @@ class SoundWave extends Sound
2866
3026
  for (let i = audioBuffer.numberOfChannels; i--;)
2867
3027
  this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
2868
3028
  this.sampleRate = audioBuffer.sampleRate;
2869
- });
3029
+ }).then(() => onloadCallback && onloadCallback(this));
2870
3030
  }
2871
3031
  }
2872
3032
 
3033
+ /** Play an mp3, ogg, or wav audio from a local file or url
3034
+ * @param {String} filename - Location of sound file to play
3035
+ * @param {Number} [volume] - How much to scale volume by
3036
+ * @param {Boolean} [loop] - True if the music should loop
3037
+ * @return {SoundWave} - The sound object for this file
3038
+ * @memberof Audio */
3039
+ function playAudioFile(filename, volume=1, loop=false)
3040
+ {
3041
+ if (!soundEnable || headlessMode) return;
3042
+
3043
+ return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3044
+ }
3045
+
2873
3046
  /**
2874
3047
  * Music Object - Stores a zzfx music track for later use
2875
3048
  *
@@ -2924,23 +3097,6 @@ class Music extends Sound
2924
3097
  { return super.play(undefined, volume, 1, 1, loop); }
2925
3098
  }
2926
3099
 
2927
- /** Play an mp3, ogg, or wav audio from a local file or url
2928
- * @param {String} filename - Location of sound file to play
2929
- * @param {Number} [volume] - How much to scale volume by
2930
- * @param {Boolean} [loop] - True if the music should loop
2931
- * @return {HTMLAudioElement} - The audio element for this sound
2932
- * @memberof Audio */
2933
- function playAudioFile(filename, volume=1, loop=false)
2934
- {
2935
- if (!soundEnable || headlessMode) return;
2936
-
2937
- const audio = new Audio(filename);
2938
- audio.volume = soundVolume * volume;
2939
- audio.loop = loop;
2940
- audio.play();
2941
- return audio;
2942
- }
2943
-
2944
3100
  /** Speak text with passed in settings
2945
3101
  * @param {String} text - The text to speak
2946
3102
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
@@ -2951,7 +3107,8 @@ function playAudioFile(filename, volume=1, loop=false)
2951
3107
  * @memberof Audio */
2952
3108
  function speak(text, language='', volume=1, rate=1, pitch=1)
2953
3109
  {
2954
- if (!soundEnable || !speechSynthesis || headlessMode) return;
3110
+ if (!soundEnable || headlessMode) return;
3111
+ if (!speechSynthesis) return;
2955
3112
 
2956
3113
  // common languages (not supported by all browsers)
2957
3114
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -2981,30 +3138,23 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
2981
3138
 
2982
3139
  ///////////////////////////////////////////////////////////////////////////////
2983
3140
 
2984
- /** Audio context used by the engine
2985
- * @type {AudioContext}
2986
- * @memberof Audio */
2987
- let audioContext;
2988
-
2989
- /** Keep track if audio was suspended when last sound was played
2990
- * @type {Boolean}
2991
- * @memberof Audio */
3141
+ // internal tracking if audio was suspended when last sound was played
3142
+ // allows first suspended sound to play when audio is resumed
2992
3143
  let audioSuspended = false;
2993
3144
 
2994
3145
  /** Play cached audio samples with given settings
2995
- * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
2996
- * @param {Number} [volume] - How much to scale volume by
2997
- * @param {Number} [rate] - The playback rate to use
2998
- * @param {Number} [pan] - How much to apply stereo panning
2999
- * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
3000
- * @param {Number} [sampleRate=44100] - Sample rate for the sound
3146
+ * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3147
+ * @param {Number} [volume] - How much to scale volume by
3148
+ * @param {Number} [rate] - The playback rate to use
3149
+ * @param {Number} [pan] - How much to apply stereo panning
3150
+ * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
3151
+ * @param {Number} [sampleRate=44100] - Sample rate for the sound
3152
+ * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
3001
3153
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3002
3154
  * @memberof Audio */
3003
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
3155
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
3004
3156
  {
3005
3157
  if (!soundEnable || headlessMode) return;
3006
- if (!audioContext)
3007
- audioContext = new AudioContext; // create audio context
3008
3158
 
3009
3159
  // prevent sounds from building up if they can't be played
3010
3160
  const audioWasSuspended = audioSuspended;
@@ -3028,10 +3178,10 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3028
3178
  source.playbackRate.value = rate;
3029
3179
  source.loop = loop;
3030
3180
 
3031
- // create and connect gain node (createGain is more widely spported then GainNode construtor)
3032
- const gainNode = audioContext.createGain();
3033
- gainNode.gain.value = soundVolume*volume;
3034
- gainNode.connect(audioContext.destination);
3181
+ // create and connect gain node
3182
+ gainNode = gainNode || audioContext.createGain();
3183
+ gainNode.gain.value = volume;
3184
+ gainNode.connect(audioGainNode);
3035
3185
 
3036
3186
  // connect source to stereo panner and gain
3037
3187
  source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
@@ -3938,6 +4088,9 @@ class Particle extends EngineObject
3938
4088
  this.localSpaceEmitter = localSpaceEmitter;
3939
4089
  /** @property {Function} - Called when particle dies */
3940
4090
  this.destroyCallback = destroyCallback;
4091
+
4092
+ // particles use circular clamped speed
4093
+ this.clampSpeedLinear = false;
3941
4094
  }
3942
4095
 
3943
4096
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -4007,9 +4160,9 @@ class Particle extends EngineObject
4007
4160
 
4008
4161
 
4009
4162
  /** List of all medals
4010
- * @type {Array}
4163
+ * @type {Object}
4011
4164
  * @memberof Medals */
4012
- const medals = [];
4165
+ const medals = {};
4013
4166
 
4014
4167
  // Engine internal variables not exposed to documentation
4015
4168
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
@@ -4025,9 +4178,45 @@ function medalsInit(saveName)
4025
4178
  {
4026
4179
  // check if medals are unlocked
4027
4180
  medalsSaveName = saveName;
4028
- debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4181
+ if (!debugMedals)
4182
+ medalsForEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4183
+
4184
+ // engine automatically renders medals
4185
+ addPluginRender(function()
4186
+ {
4187
+ if (!medalsDisplayQueue.length)
4188
+ return;
4189
+
4190
+ // update first medal in queue
4191
+ const medal = medalsDisplayQueue[0];
4192
+ const time = timeReal - medalsDisplayTimeLast;
4193
+ if (!medalsDisplayTimeLast)
4194
+ medalsDisplayTimeLast = timeReal;
4195
+ else if (time > medalDisplayTime)
4196
+ {
4197
+ medalsDisplayTimeLast = 0;
4198
+ medalsDisplayQueue.shift();
4199
+ }
4200
+ else
4201
+ {
4202
+ // slide on/off medals
4203
+ const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
4204
+ const hidePercent =
4205
+ time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
4206
+ time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
4207
+ medal.render(hidePercent);
4208
+ }
4209
+ });
4029
4210
  }
4030
4211
 
4212
+ /** Calls a function for each medal
4213
+ * @param {Function} callback
4214
+ * @memberof Medals */
4215
+ function medalsForEach(callback)
4216
+ { Object.values(medals).forEach(medal=>callback(medal)); }
4217
+
4218
+ ///////////////////////////////////////////////////////////////////////////////
4219
+
4031
4220
  /**
4032
4221
  * Medal - Tracks an unlockable medal
4033
4222
  * @example
@@ -4072,7 +4261,6 @@ class Medal
4072
4261
  ASSERT(medalsSaveName, 'save name must be set');
4073
4262
  localStorage[this.storageKey()] = this.unlocked = 1;
4074
4263
  medalsDisplayQueue.push(this);
4075
- newgrounds && newgrounds.unlockMedal(this.id);
4076
4264
  }
4077
4265
 
4078
4266
  /** Render a medal
@@ -4120,173 +4308,6 @@ class Medal
4120
4308
 
4121
4309
  // Get local storage key used by the medal
4122
4310
  storageKey() { return medalsSaveName + '_' + this.id; }
4123
- }
4124
-
4125
- // engine automatically renders medals
4126
- function medalsRender()
4127
- {
4128
- if (!medalsDisplayQueue.length)
4129
- return;
4130
-
4131
- // update first medal in queue
4132
- const medal = medalsDisplayQueue[0];
4133
- const time = timeReal - medalsDisplayTimeLast;
4134
- if (!medalsDisplayTimeLast)
4135
- medalsDisplayTimeLast = timeReal;
4136
- else if (time > medalDisplayTime)
4137
- {
4138
- medalsDisplayTimeLast = 0;
4139
- medalsDisplayQueue.shift();
4140
- }
4141
- else
4142
- {
4143
- // slide on/off medals
4144
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
4145
- const hidePercent =
4146
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
4147
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
4148
- medal.render(hidePercent);
4149
- }
4150
- }
4151
-
4152
- ///////////////////////////////////////////////////////////////////////////////
4153
-
4154
- // global Newgrounds object
4155
- let newgrounds;
4156
-
4157
- /** This can used to enable Newgrounds functionality
4158
- * @param {Number} app_id - The newgrounds App ID
4159
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4160
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
4161
- * @memberof Medals */
4162
- function newgroundsInit(app_id, cipher, cryptoJS)
4163
- { newgrounds = new Newgrounds(app_id, cipher, cryptoJS); }
4164
-
4165
- /**
4166
- * Newgrounds API wrapper object
4167
- * @example
4168
- * // create a newgrounds object, replace the app id with your own
4169
- * const app_id = '53123:1ZuSTQ9l';
4170
- * newgrounds = new Newgrounds(app_id);
4171
- */
4172
- class Newgrounds
4173
- {
4174
- /** Create a newgrounds object
4175
- * @param {Number} app_id - The newgrounds App ID
4176
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4177
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */
4178
- constructor(app_id, cipher, cryptoJS)
4179
- {
4180
- ASSERT(!newgrounds && app_id>0, 'there can only be one newgrounds object');
4181
- ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
4182
-
4183
- this.app_id = app_id;
4184
- this.cipher = cipher;
4185
- this.cryptoJS = cryptoJS;
4186
- this.host = location ? location.hostname : '';
4187
-
4188
- // get session id from url search params
4189
- const url = new URL(location.href);
4190
- this.session_id = url.searchParams.get('ngio_session_id');
4191
-
4192
- if (!this.session_id)
4193
- return; // only use newgrounds when logged in
4194
-
4195
- // get medals
4196
- const medalsResult = this.call('Medal.getList');
4197
- this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
4198
- debugMedals && console.log(this.medals);
4199
- for (const newgroundsMedal of this.medals)
4200
- {
4201
- const medal = medals[newgroundsMedal['id']];
4202
- if (medal)
4203
- {
4204
- // copy newgrounds medal data
4205
- medal.image = new Image;
4206
- medal.image.src = newgroundsMedal['icon'];
4207
- medal.name = newgroundsMedal['name'];
4208
- medal.description = newgroundsMedal['description'];
4209
- medal.unlocked = newgroundsMedal['unlocked'];
4210
- medal.difficulty = newgroundsMedal['difficulty'];
4211
- medal.value = newgroundsMedal['value'];
4212
-
4213
- if (medal.value)
4214
- medal.description = medal.description + ' (' + medal.value + ')';
4215
- }
4216
- }
4217
-
4218
- // get scoreboards
4219
- const scoreboardResult = this.call('ScoreBoard.getBoards');
4220
- this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
4221
- debugMedals && console.log(this.scoreboards);
4222
-
4223
- const keepAliveMS = 5 * 60 * 1e3;
4224
- setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
4225
- }
4226
-
4227
- /** Send message to unlock a medal by id
4228
- * @param {Number} id - The medal id */
4229
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
4230
-
4231
- /** Send message to post score
4232
- * @param {Number} id - The scoreboard id
4233
- * @param {Number} value - The score value */
4234
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
4235
-
4236
- /** Get scores from a scoreboard
4237
- * @param {Number} id - The scoreboard id
4238
- * @param {String} [user] - A user's id or name
4239
- * @param {Number} [social] - If true, only social scores will be loaded
4240
- * @param {Number} [skip] - Number of scores to skip before start
4241
- * @param {Number} [limit] - Number of scores to include in the list
4242
- * @return {Object} - The response JSON object
4243
- */
4244
- getScores(id, user, social=0, skip=0, limit=10)
4245
- { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
4246
-
4247
- /** Send message to log a view */
4248
- logView() { return this.call('App.logView', {'host':this.host}, true); }
4249
-
4250
- /** Send a message to call a component of the Newgrounds API
4251
- * @param {String} component - Name of the component
4252
- * @param {Object} [parameters] - Parameters to use for call
4253
- * @param {Boolean} [async] - If true, don't wait for response before continuing
4254
- * @return {Object} - The response JSON object
4255
- */
4256
- call(component, parameters, async=false)
4257
- {
4258
- const call = {'component':component, 'parameters':parameters};
4259
- if (this.cipher)
4260
- {
4261
- // encrypt using AES-128 Base64 with cryptoJS
4262
- const cryptoJS = this.cryptoJS;
4263
- const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
4264
- const iv = cryptoJS['lib']['WordArray']['random'](16);
4265
- const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
4266
- call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
4267
- call['parameters'] = 0;
4268
- }
4269
-
4270
- // build the input object
4271
- const input =
4272
- {
4273
- 'app_id': this.app_id,
4274
- 'session_id': this.session_id,
4275
- 'call': call
4276
- };
4277
-
4278
- // build post data
4279
- const formData = new FormData();
4280
- formData.append('input', JSON.stringify(input));
4281
-
4282
- // send post data
4283
- const xmlHttp = new XMLHttpRequest();
4284
- const url = 'https://newgrounds.io/gateway_v3.php';
4285
- xmlHttp.open('POST', url, !debugMedals && async);
4286
- xmlHttp.send(formData);
4287
- debugMedals && console.log(xmlHttp.responseText);
4288
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
4289
- }
4290
4311
  }
4291
4312
  /**
4292
4313
  * LittleJS WebGL Interface
@@ -4377,7 +4398,7 @@ function glPreRender()
4377
4398
 
4378
4399
  // clear and set to same size as main canvas
4379
4400
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4380
- //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4401
+ glContext.clear(gl_COLOR_BUFFER_BIT);
4381
4402
 
4382
4403
  // set up the shader
4383
4404
  glContext.useProgram(glShader);
@@ -4560,99 +4581,6 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdd
4560
4581
  glInstanceCount++;
4561
4582
  }
4562
4583
 
4563
- ///////////////////////////////////////////////////////////////////////////////
4564
- // post processing - can be enabled to pass other canvases through a final shader
4565
-
4566
- let glPostShader, glPostTexture, glPostIncludeOverlay;
4567
-
4568
- /** Set up a post processing shader
4569
- * @param {String} shaderCode
4570
- * @param {Boolean} includeOverlay
4571
- * @memberof WebGL */
4572
- function glInitPostProcess(shaderCode, includeOverlay=false)
4573
- {
4574
- ASSERT(!glPostShader, 'can only have 1 post effects shader');
4575
- if (headlessMode) return;
4576
- if (!shaderCode) // default shader pass through
4577
- shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4578
-
4579
- // create the shader
4580
- glPostShader = glCreateProgram(
4581
- '#version 300 es\n' + // specify GLSL ES version
4582
- 'precision highp float;'+ // use highp for better accuracy
4583
- 'in vec2 p;'+ // position
4584
- 'void main(){'+ // shader entry point
4585
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
4586
- '}' // end of shader
4587
- ,
4588
- '#version 300 es\n' + // specify GLSL ES version
4589
- 'precision highp float;'+ // use highp for better accuracy
4590
- 'uniform sampler2D iChannel0;'+ // input texture
4591
- 'uniform vec3 iResolution;'+ // size of output texture
4592
- 'uniform float iTime;'+ // time
4593
- 'out vec4 c;'+ // out color
4594
- '\n' + shaderCode + '\n'+ // insert custom shader code
4595
- 'void main(){'+ // shader entry point
4596
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
4597
- 'c.a=1.;'+ // always use full alpha
4598
- '}' // end of shader
4599
- );
4600
-
4601
- // create buffer and texture
4602
- glPostTexture = glCreateTexture(undefined);
4603
- glPostIncludeOverlay = includeOverlay;
4604
-
4605
- // hide the original 2d canvas
4606
- mainCanvas.style.visibility = 'hidden';
4607
- if (glPostIncludeOverlay)
4608
- overlayCanvas.style.visibility = 'hidden';
4609
- }
4610
-
4611
- // Render the post processing shader, called automatically by the engine
4612
- function glRenderPostProcess()
4613
- {
4614
- if (!glPostShader || headlessMode) return;
4615
-
4616
- // prepare to render post process shader
4617
- if (glEnable)
4618
- {
4619
- glFlush(); // clear out the buffer
4620
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
4621
- }
4622
- else
4623
- {
4624
- // set the viewport
4625
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4626
- }
4627
-
4628
- // copy overlay canvas so it will be included in post processing
4629
- glPostIncludeOverlay && mainContext.drawImage(overlayCanvas, 0, 0);
4630
-
4631
- // setup shader program to draw one triangle
4632
- glContext.useProgram(glPostShader);
4633
- glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
4634
- glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, 1);
4635
- glContext.disable(gl_BLEND);
4636
-
4637
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
4638
- glContext.activeTexture(gl_TEXTURE0);
4639
- glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
4640
- glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
4641
-
4642
- // set vertex position attribute
4643
- const vertexByteStride = 8;
4644
- const pLocation = glContext.getAttribLocation(glPostShader, 'p');
4645
- glContext.enableVertexAttribArray(pLocation);
4646
- glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, false, vertexByteStride, 0);
4647
-
4648
- // set uniforms and draw
4649
- const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
4650
- glContext.uniform1i(uniformLocation('iChannel0'), 0);
4651
- glContext.uniform1f(uniformLocation('iTime'), time);
4652
- glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
4653
- glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 4);
4654
- }
4655
-
4656
4584
  ///////////////////////////////////////////////////////////////////////////////
4657
4585
  // store gl constants as integers so their name doesn't use space in minifed
4658
4586
  const
@@ -4717,7 +4645,7 @@ const engineName = 'LittleJS';
4717
4645
  * @type {String}
4718
4646
  * @default
4719
4647
  * @memberof Engine */
4720
- const engineVersion = '1.9.6';
4648
+ const engineVersion = '1.9.8';
4721
4649
 
4722
4650
  /** Frames per second to update
4723
4651
  * @type {Number}
@@ -4771,6 +4699,22 @@ function setPaused(isPaused) { paused = isPaused; }
4771
4699
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
4772
4700
 
4773
4701
  ///////////////////////////////////////////////////////////////////////////////
4702
+ // plugin hooks
4703
+
4704
+ const pluginUpdateList = [], pluginRenderList = [];
4705
+
4706
+ /** Add a new update function for a plugin
4707
+ * @param {Function} updateFunction
4708
+ * @memberof Engine */
4709
+ function addPluginUpdate(updateFunction) { pluginUpdateList.push(updateFunction); }
4710
+
4711
+ /** Add a new render function for a plugin
4712
+ * @param {Function} renderFunction
4713
+ * @memberof Engine */
4714
+ function addPluginRender(renderFunction) { pluginRenderList.push(renderFunction); }
4715
+
4716
+ ///////////////////////////////////////////////////////////////////////////////
4717
+ // Main engine functions
4774
4718
 
4775
4719
  /** Startup LittleJS engine with your callback functions
4776
4720
  * @param {Function} gameInit - Called once after the engine starts up, setup the game
@@ -4846,6 +4790,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4846
4790
  // update game and objects
4847
4791
  inputUpdate();
4848
4792
  gameUpdate();
4793
+ pluginUpdateList.forEach(f=>f());
4849
4794
  engineObjectsUpdate();
4850
4795
 
4851
4796
  // do post update
@@ -4867,8 +4812,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4867
4812
  for (const o of engineObjects)
4868
4813
  o.destroyed || o.render();
4869
4814
  gameRenderPost();
4870
- glRenderPostProcess();
4871
- medalsRender();
4815
+ pluginRenderList.forEach(f=>f());
4872
4816
  touchGamepadRender();
4873
4817
  debugRender();
4874
4818
  glCopyToContext(mainContext);
@@ -4950,6 +4894,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4950
4894
 
4951
4895
  // init stuff and start engine
4952
4896
  inputInit();
4897
+ audioInit();
4953
4898
  debugInit();
4954
4899
  glInit();
4955
4900
 
@@ -5157,21 +5102,23 @@ function drawEngineSplashScreen(t)
5157
5102
  x.setLineDash([99*p2,99]);
5158
5103
 
5159
5104
  // cab top
5160
- rect(7,17,18,-8,color(2,2));
5161
- rect(7,9,18,4,color(2,3));
5162
- rect(25,9,8,8,color(2,1));
5163
- rect(25,9,-18,8);
5164
- rect(25,9,8,8);
5105
+ rect(7,16,18,-8,color(2,2));
5106
+ rect(7,8,18,4,color(2,3));
5107
+ rect(25,8,8,8,color(2,1));
5108
+ rect(25,8,-18,8);
5109
+ rect(25,8,8,8);
5165
5110
 
5166
5111
  // cab
5167
- rect(25,17,7,22,color());
5168
- rect(11,40,14,-23,color(1,1));
5169
- rect(11,17,14,17,color(1,2));
5170
- rect(11,17,14,9,color(1,3));
5171
- rect(15,31,6,-9,color(2,2));
5172
- circle(15,23,5,0,PI/2,color(2,4),1);
5173
- rect(25,17,-14,23);
5174
- rect(21,22,-6,9);
5112
+ rect(25,16,7,23,color());
5113
+ rect(11,39,14,-23,color(1,1));
5114
+ rect(11,16,14,18,color(1,2));
5115
+ rect(11,16,14,8,color(1,3));
5116
+ rect(25,16,-14,24);
5117
+
5118
+ // cab window
5119
+ rect(15,29,6,-9,color(2,2));
5120
+ circle(15,21,5,0,PI/2,color(2,4),1);
5121
+ rect(21,21,-6,9);
5175
5122
 
5176
5123
  // little stack
5177
5124
  rect(37,14,9,6,color(3,2));
@@ -5179,16 +5126,16 @@ function drawEngineSplashScreen(t)
5179
5126
  rect(37,14,9,6);
5180
5127
 
5181
5128
  // big stack
5182
- rect(50,20,10,-8,color(0,1))
5183
- rect(50,20,6.5,-8,color(0,2))
5184
- rect(50,20,3.5,-8,color(0,3))
5185
- rect(50,20,10,-8)
5186
- circle(55,2,11.4,.5,PI-.5,color(3,3))
5187
- circle(55,2,11.4,.5,PI/2,color(3,2),1)
5188
- circle(55,2,11.4,.5,PI-.5)
5189
- rect(45,7,20,-7,color(0,2))
5190
- rect(45,-1,20,4,color(0,3))
5191
- rect(45,-1,20,8)
5129
+ rect(50,20,10,-8,color(0,1));
5130
+ rect(50,20,6.5,-8,color(0,2));
5131
+ rect(50,20,3.5,-8,color(0,3));
5132
+ rect(50,20,10,-8);
5133
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
5134
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
5135
+ circle(55,2,11.4,.5,PI-.5);
5136
+ rect(45,7,20,-7,color(0,2));
5137
+ rect(45,-1,20,4,color(0,3));
5138
+ rect(45,-1,20,8);
5192
5139
 
5193
5140
  // engine
5194
5141
  for (let i=5; i--;)