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.
package/dist/littlejs.js CHANGED
@@ -80,6 +80,20 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
80
80
  debugPrimitives.push({pos, size:vec2(size), color, time:new Timer(time), angle, fill});
81
81
  }
82
82
 
83
+ /** Draw a debug poly in world space
84
+ * @param {Vector2} pos
85
+ * @param {Array} points
86
+ * @param {String} [color]
87
+ * @param {Number} [time]
88
+ * @param {Number} [angle]
89
+ * @param {Boolean} [fill]
90
+ * @memberof Debug */
91
+ function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
92
+ {
93
+ ASSERT(typeof color == 'string', 'pass in css color strings');
94
+ debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
95
+ }
96
+
83
97
  /** Draw a debug circle in world space
84
98
  * @param {Vector2} pos
85
99
  * @param {Number} [radius]
@@ -89,7 +103,7 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
89
103
  * @memberof Debug */
90
104
  function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
91
105
  {
92
- ASSERT(typeof color == 'string', 'pass in css color strings');
106
+ ASSERT(typeof color == 'string', 'pass in css color strings');
93
107
  debugPrimitives.push({pos, size:radius, color, time:new Timer(time), angle:0, fill});
94
108
  }
95
109
 
@@ -99,7 +113,11 @@ function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
99
113
  * @param {Number} [time]
100
114
  * @param {Number} [angle]
101
115
  * @memberof Debug */
102
- function debugPoint(pos, color, time, angle) {debugRect(pos, undefined, color, time, angle);}
116
+ function debugPoint(pos, color, time, angle)
117
+ {
118
+ ASSERT(typeof color == 'string', 'pass in css color strings');
119
+ debugRect(pos, undefined, color, time, angle);
120
+ }
103
121
 
104
122
  /** Draw a debug line in world space
105
123
  * @param {Vector2} posA
@@ -110,19 +128,20 @@ function debugPoint(pos, color, time, angle) {debugRect(pos, undefined, color, t
110
128
  * @memberof Debug */
111
129
  function debugLine(posA, posB, color, thickness=.1, time)
112
130
  {
131
+ ASSERT(typeof color == 'string', 'pass in css color strings');
113
132
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
114
133
  const size = vec2(thickness, halfDelta.length()*2);
115
134
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
116
135
  }
117
136
 
118
- /** Draw a debug axis aligned bounding box in world space
137
+ /** Draw a debug combined axis aligned bounding box in world space
119
138
  * @param {Vector2} pA - position A
120
139
  * @param {Vector2} sA - size A
121
140
  * @param {Vector2} pB - position B
122
141
  * @param {Vector2} sB - size B
123
142
  * @param {String} [color]
124
143
  * @memberof Debug */
125
- function debugAABB(pA, sA, pB, sB, color)
144
+ function debugOverlap(pA, sA, pB, sB, color)
126
145
  {
127
146
  const minPos = vec2(min(pA.x - sA.x/2, pB.x - sB.x/2), min(pA.y - sA.y/2, pB.y - sB.y/2));
128
147
  const maxPos = vec2(max(pA.x + sA.x/2, pB.x + sB.x/2), max(pA.y + sA.y/2, pB.y + sB.y/2));
@@ -140,7 +159,7 @@ function debugAABB(pA, sA, pB, sB, color)
140
159
  * @memberof Debug */
141
160
  function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
142
161
  {
143
- ASSERT(typeof color == 'string', 'pass in css color strings');
162
+ ASSERT(typeof color == 'string', 'pass in css color strings');
144
163
  debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
145
164
  }
146
165
 
@@ -239,7 +258,7 @@ function debugRender()
239
258
  const stickScale = 1;
240
259
  const buttonScale = .2;
241
260
  const centerPos = cameraPos;
242
- const sticks = stickData[i];
261
+ const sticks = gamepadStickData[i];
243
262
  for (let j = sticks.length; j--;)
244
263
  {
245
264
  const drawPos = centerPos.add(vec2(j*stickScale*2, i*stickScale*3));
@@ -259,6 +278,7 @@ function debugRender()
259
278
  }
260
279
  }
261
280
 
281
+ let debugObject;
262
282
  if (debugOverlay)
263
283
  {
264
284
  const saveContext = mainContext;
@@ -269,11 +289,13 @@ function debugRender()
269
289
  debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
270
290
 
271
291
  // mouse pick
272
- let bestDistance = Infinity, bestObject;
292
+ let bestDistance = Infinity;
273
293
  for (const o of engineObjects)
274
294
  {
275
295
  if (o.canvas || o.destroyed)
276
296
  continue;
297
+
298
+ o.renderDebugInfo();
277
299
  if (!o.size.x || !o.size.y)
278
300
  continue;
279
301
 
@@ -281,33 +303,15 @@ function debugRender()
281
303
  if (distance < bestDistance)
282
304
  {
283
305
  bestDistance = distance;
284
- bestObject = o;
306
+ debugObject = o;
285
307
  }
286
-
287
- // show object info
288
- const size = vec2(max(o.size.x, .2), max(o.size.y, .2));
289
- const color1 = new Color(o.collideTiles?1:0, o.collideSolidObjects?1:0, o.isSolid?1:0, o.parent?.2:.5);
290
- const color2 = o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8);
291
- drawRect(o.pos, size, color1, o.angle, false);
292
- drawRect(o.pos, size.scale(.8), color2, o.angle, false);
293
- o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5), false);
294
- }
295
-
296
- if (bestObject)
297
- {
298
- const raycastHitPos = tileCollisionRaycast(bestObject.pos, mousePos);
299
- raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), new Color(0,1,1,.3));
300
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5), 0, false);
301
- drawLine(mousePos, bestObject.pos, .1, raycastHitPos ? new Color(1,0,0,.5) : new Color(0,1,0,.5), false);
302
-
303
- const debugText = 'mouse pos = ' + mousePos +
304
- '\nmouse collision = ' + getTileCollisionData(mousePos) +
305
- '\n\n--- object info ---\n' +
306
- bestObject.toString();
307
- drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, undefined, 'center', 'monospace');
308
308
  }
309
309
 
310
- glCopyToContext(mainContext = saveContext);
310
+ if (tileCollisionSize.x > 0 && tileCollisionSize.y > 0)
311
+ drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(0,0,1,.5), 0, false);
312
+ mainContext = saveContext;
313
+
314
+ //glCopyToContext(mainContext = saveContext);
311
315
  }
312
316
 
313
317
  {
@@ -322,6 +326,7 @@ function debugRender()
322
326
  const pos = worldToScreen(p.pos);
323
327
  overlayContext.translate(pos.x|0, pos.y|0);
324
328
  overlayContext.rotate(p.angle);
329
+ overlayContext.scale(1, -1);
325
330
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
326
331
 
327
332
  if (p.text != undefined)
@@ -331,7 +336,20 @@ function debugRender()
331
336
  overlayContext.textBaseline = 'middle';
332
337
  overlayContext.fillText(p.text, 0, 0);
333
338
  }
334
- else if (p.size == 0 || p.size.x === 0 && p.size.y === 0 )
339
+ else if (p.points != undefined)
340
+ {
341
+ // poly
342
+ overlayContext.beginPath();
343
+ for (const point of p.points)
344
+ {
345
+ const p2 = point.scale(cameraScale).floor();
346
+ overlayContext.lineTo(p2.x, p2.y);
347
+ }
348
+ overlayContext.closePath();
349
+ p.fill && overlayContext.fill();
350
+ overlayContext.stroke();
351
+ }
352
+ else if (p.size == 0 || p.size.x === 0 && p.size.y === 0)
335
353
  {
336
354
  // point
337
355
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
@@ -340,7 +358,8 @@ function debugRender()
340
358
  else if (p.size.x != undefined)
341
359
  {
342
360
  // rect
343
- const w = p.size.x*cameraScale|0, h = p.size.y*cameraScale|0;
361
+ const s = p.size.scale(cameraScale).floor();
362
+ const w = s.x, h = s.y;
344
363
  p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
345
364
  overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
346
365
  }
@@ -359,6 +378,22 @@ function debugRender()
359
378
  // remove expired primitives
360
379
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
361
380
  }
381
+
382
+ if (debugObject)
383
+ {
384
+ const saveContext = mainContext;
385
+ mainContext = overlayContext;
386
+ const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
387
+ raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
388
+ drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5), false);
389
+
390
+ const debugText = 'mouse pos = ' + mousePos +
391
+ '\nmouse collision = ' + getTileCollisionData(mousePos) +
392
+ '\n\n--- object info ---\n' +
393
+ debugObject.toString();
394
+ drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
395
+ mainContext = saveContext;
396
+ }
362
397
 
363
398
  {
364
399
  // draw debug overlay
@@ -721,7 +756,7 @@ class RandomGenerator
721
756
  */
722
757
  function vec2(x=0, y)
723
758
  {
724
- return typeof x === 'number' ?
759
+ return typeof x == 'number' ?
725
760
  new Vector2(x, y == undefined? x : y) :
726
761
  new Vector2(x.x, x.y);
727
762
  }
@@ -750,13 +785,19 @@ class Vector2
750
785
  * @param {Number} [y] - Y axis location */
751
786
  constructor(x=0, y=0)
752
787
  {
753
- ASSERT(typeof x === 'number' && typeof y === 'number');
788
+ ASSERT(typeof x == 'number' && typeof y == 'number');
754
789
  /** @property {Number} - X axis location */
755
790
  this.x = x;
756
791
  /** @property {Number} - Y axis location */
757
792
  this.y = y;
758
793
  }
759
794
 
795
+ /** Sets values of this vector and returns self
796
+ * @param {Number} [x] - X axis location
797
+ * @param {Number} [y] - Y axis location
798
+ * @return {Vector2} */
799
+ set(x=0, y=0) { this.x=x; this.y=y; return this; }
800
+
760
801
  /** Returns a new vector that is a copy of this
761
802
  * @return {Vector2} */
762
803
  copy() { return new Vector2(this.x, this.y); }
@@ -1008,6 +1049,15 @@ class Color
1008
1049
  this.a = a;
1009
1050
  }
1010
1051
 
1052
+ /** Sets values of this color and returns self
1053
+ * @param {Number} [r] - red
1054
+ * @param {Number} [g] - green
1055
+ * @param {Number} [b] - blue
1056
+ * @param {Number} [a] - alpha
1057
+ * @return {Color} */
1058
+ set(r=1, g=1, b=1, a=1)
1059
+ { this.r=r; this.g=g; this.b=b; this.a=a; return this; }
1060
+
1011
1061
  /** Returns a new color that is a copy of this
1012
1062
  * @return {Color} */
1013
1063
  copy() { return new Color(this.r, this.g, this.b, this.a); }
@@ -1135,14 +1185,15 @@ class Color
1135
1185
  ).clamp();
1136
1186
  }
1137
1187
 
1138
- /** Returns this color expressed as a rgb color code
1188
+ /** Returns this color expressed as a hex color code
1139
1189
  * @param {Boolean} [useAlpha] - if alpha should be included in result
1140
1190
  * @return {String} */
1141
- toString(useAlpha = true)
1142
- {
1143
- return `rgb(${this.r*255},${this.g*255},${this.b*255},${useAlpha ? this.a : 0})`;
1191
+ toString(useAlpha = true)
1192
+ {
1193
+ const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
1194
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1144
1195
  }
1145
-
1196
+
1146
1197
  /** Set this color from a hex code
1147
1198
  * @param {String} hex - html hex code
1148
1199
  * @return {Color} */
@@ -1168,6 +1219,64 @@ class Color
1168
1219
  }
1169
1220
  }
1170
1221
 
1222
+ ///////////////////////////////////////////////////////////////////////////////
1223
+ // default colors
1224
+
1225
+ /** Color - White
1226
+ * @type {Color}
1227
+ * @memberof Utilities */
1228
+ const WHITE = rgb();
1229
+
1230
+ /** Color - Black
1231
+ * @type {Color}
1232
+ * @memberof Utilities */
1233
+ const BLACK = rgb(0,0,0);
1234
+
1235
+ /** Color - Gray
1236
+ * @type {Color}
1237
+ * @memberof Utilities */
1238
+ const GRAY = rgb(.5,.5,.5);
1239
+
1240
+ /** Color - Red
1241
+ * @type {Color}
1242
+ * @memberof Utilities */
1243
+ const RED = rgb(1,0,0);
1244
+
1245
+ /** Color - Orange
1246
+ * @type {Color}
1247
+ * @memberof Utilities */
1248
+ const ORANGE = rgb(1,.5,0);
1249
+
1250
+ /** Color - Yellow
1251
+ * @type {Color}
1252
+ * @memberof Utilities */
1253
+ const YELLOW = rgb(1,1,0);
1254
+
1255
+ /** Color - Green
1256
+ * @type {Color}
1257
+ * @memberof Utilities */
1258
+ const GREEN = rgb(0,1,0);
1259
+
1260
+ /** Color - Cyan
1261
+ * @type {Color}
1262
+ * @memberof Utilities */
1263
+ const CYAN = rgb(0,1,1);
1264
+
1265
+ /** Color - Blue
1266
+ * @type {Color}
1267
+ * @memberof Utilities */
1268
+ const BLUE = rgb(0,0,1);
1269
+
1270
+ /** Color - Purple
1271
+ * @type {Color}
1272
+ * @memberof Utilities */
1273
+ const PURPLE = rgb(.5,0,1);
1274
+
1275
+ /** Color - Magenta
1276
+ * @type {Color}
1277
+ * @memberof Utilities */
1278
+ const MAGENTA = rgb(1,0,1);
1279
+
1171
1280
  ///////////////////////////////////////////////////////////////////////////////
1172
1281
 
1173
1282
  /**
@@ -1248,9 +1357,9 @@ let cameraScale = 32;
1248
1357
 
1249
1358
  /** The max size of the canvas, centered if window is larger
1250
1359
  * @type {Vector2}
1251
- * @default Vector2(1920,1200)
1360
+ * @default Vector2(1920,1080)
1252
1361
  * @memberof Settings */
1253
- let canvasMaxSize = vec2(1920, 1200);
1362
+ let canvasMaxSize = vec2(1920, 1080);
1254
1363
 
1255
1364
  /** Fixed size of the canvas, if enabled canvas size never changes
1256
1365
  * - you may also need to set mainCanvasSize if using screen space coords in startup
@@ -1639,7 +1748,12 @@ function setSoundEnable(enable) { soundEnable = enable; }
1639
1748
  /** Set volume scale to apply to all sound, music and speech
1640
1749
  * @param {Number} volume
1641
1750
  * @memberof Settings */
1642
- function setSoundVolume(volume) { soundVolume = volume; }
1751
+ function setSoundVolume(volume)
1752
+ {
1753
+ soundVolume = volume;
1754
+ if (soundEnable && !headlessMode && audioGainNode)
1755
+ audioGainNode.gain.value = volume; // update gain immediatly
1756
+ }
1643
1757
 
1644
1758
  /** Set default range where sound no longer plays
1645
1759
  * @param {Number} range
@@ -1772,6 +1886,8 @@ class EngineObject
1772
1886
  this.spawnTime = time;
1773
1887
  /** @property {Array} - List of children of this object */
1774
1888
  this.children = [];
1889
+ /** @property {Boolean} - Limit object speed using linear or circular math */
1890
+ this.clampSpeedLinear = true;
1775
1891
 
1776
1892
  // parent child system
1777
1893
  /** @property {EngineObject} - Parent of object if in local space */
@@ -1820,8 +1936,21 @@ class EngineObject
1820
1936
  return;
1821
1937
 
1822
1938
  // limit max speed to prevent missing collisions
1823
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1824
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1939
+ if (this.clampSpeedLinear)
1940
+ {
1941
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1942
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1943
+ }
1944
+ else
1945
+ {
1946
+ const length2 = this.velocity.lengthSquared();
1947
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
1948
+ {
1949
+ const s = objectMaxSpeed / length2**.5;
1950
+ this.velocity.x *= s;
1951
+ this.velocity.y *= s;
1952
+ }
1953
+ }
1825
1954
 
1826
1955
  // apply physics
1827
1956
  const oldPos = this.pos.copy();
@@ -1877,7 +2006,7 @@ class EngineObject
1877
2006
  if (o.mass) // push away if not fixed
1878
2007
  o.velocity = o.velocity.subtract(velocity);
1879
2008
 
1880
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f00');
2009
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
1881
2010
  continue;
1882
2011
  }
1883
2012
 
@@ -1939,7 +2068,7 @@ class EngineObject
1939
2068
  else // bounce if other object is fixed
1940
2069
  this.velocity.x *= -elasticity;
1941
2070
  }
1942
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
2071
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
1943
2072
  }
1944
2073
  }
1945
2074
  if (this.collideTiles)
@@ -2000,6 +2129,22 @@ class EngineObject
2000
2129
  for (const child of this.children)
2001
2130
  child.destroy(child.parent = 0);
2002
2131
  }
2132
+
2133
+ /** Convert from local space to world space
2134
+ * @param {Vector2} pos - local space point */
2135
+ localToWorld(pos) { return this.pos.add(pos.rotate(-this.angle)); }
2136
+
2137
+ /** Convert from world space to local space
2138
+ * @param {Vector2} pos - world space point */
2139
+ worldToLocal(pos) { return pos.subtract(this.pos).rotate(this.angle); }
2140
+
2141
+ /** Convert from local space to world space for a vector (rotation only)
2142
+ * @param {Vector2} vec - local space vector */
2143
+ localToWorldVector(vec) { return vec.rotate(this.angle); }
2144
+
2145
+ /** Convert from world space to local space for a vector (rotation only)
2146
+ * @param {Vector2} vec - world space vector */
2147
+ worldToLocalVector(vec) { return vec.rotate(-this.angle); }
2003
2148
 
2004
2149
  /** Called to check if a tile collision should be resolved
2005
2150
  * @param {Number} tileData - the value of the tile at the position
@@ -2086,6 +2231,18 @@ class EngineObject
2086
2231
  return text;
2087
2232
  }
2088
2233
  }
2234
+
2235
+ /** Render debug info for this object */
2236
+ renderDebugInfo()
2237
+ {
2238
+ // show object info for debugging
2239
+ const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
2240
+ const color1 = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, this.parent?.2:.5);
2241
+ const color2 = this.parent ? rgb(1,1,1,.5) : rgb(0,0,0,.8);
2242
+ drawRect(this.pos, size, color1, this.angle, false);
2243
+ drawRect(this.pos, size.scale(.8), color2, this.angle, false);
2244
+ this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(0,0,1,.5), false);
2245
+ }
2089
2246
  }
2090
2247
  /**
2091
2248
  * LittleJS Drawing System
@@ -2409,7 +2566,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2409
2566
  context.save();
2410
2567
  context.translate(pos.x+.5, pos.y+.5);
2411
2568
  context.rotate(angle);
2412
- context.scale(mirror ? -size.x : size.x, size.y);
2569
+ context.scale(mirror ? -size.x : size.x, -size.y);
2413
2570
  drawFunction(context);
2414
2571
  context.restore();
2415
2572
  }
@@ -3104,6 +3261,32 @@ function touchGamepadRender()
3104
3261
 
3105
3262
 
3106
3263
 
3264
+ /** Audio context used by the engine
3265
+ * @type {AudioContext}
3266
+ * @memberof Audio */
3267
+ let audioContext;
3268
+
3269
+ /** Master gain node for all audio to pass through
3270
+ * @type {GainNode}
3271
+ * @memberof Audio */
3272
+ let audioGainNode;
3273
+
3274
+ function audioInit()
3275
+ {
3276
+ if (!soundEnable || headlessMode) return;
3277
+
3278
+ // create audio context
3279
+ audioContext = new AudioContext;
3280
+
3281
+ // create and connect gain node
3282
+ // (createGain is more widely spported then GainNode construtor)
3283
+ audioGainNode = audioContext.createGain();
3284
+ audioGainNode.connect(audioContext.destination);
3285
+ setSoundVolume(soundVolume); // update gain volume
3286
+ }
3287
+
3288
+ ///////////////////////////////////////////////////////////////////////////////
3289
+
3107
3290
  /**
3108
3291
  * Sound Object - Stores a sound for later use and can be played positionally
3109
3292
  *
@@ -3155,7 +3338,8 @@ class Sound
3155
3338
  */
3156
3339
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
3157
3340
  {
3158
- if (!soundEnable || !this.sampleChannels || headlessMode) return;
3341
+ if (!soundEnable || headlessMode) return;
3342
+ if (!this.sampleChannels) return;
3159
3343
 
3160
3344
  let pan;
3161
3345
  if (pos)
@@ -3178,7 +3362,17 @@ class Sound
3178
3362
 
3179
3363
  // play the sound
3180
3364
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
3181
- return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate);
3365
+ this.gainNode = audioContext.createGain();
3366
+ return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
3367
+ }
3368
+
3369
+ /** Set the sound volume
3370
+ * @param {Number} [volume] - How much to scale volume by
3371
+ */
3372
+ setVolume(volume=1)
3373
+ {
3374
+ if (this.gainNode)
3375
+ this.gainNode.gain.value = volume;
3182
3376
  }
3183
3377
 
3184
3378
  /** Stop the last instance of this sound that was played */
@@ -3232,16 +3426,14 @@ class SoundWave extends Sound
3232
3426
  * @param {Number} [randomness] - How much to randomize frequency each time sound plays
3233
3427
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
3234
3428
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
3429
+ * @param {Function} [onloadCallback] - callback function to call when sound is loaded
3235
3430
  */
3236
- constructor(filename, randomness=0, range, taper)
3431
+ constructor(filename, randomness=0, range, taper, onloadCallback)
3237
3432
  {
3238
3433
  super(undefined, range, taper);
3239
- this.randomness = randomness;
3240
-
3241
3434
  if (!soundEnable || headlessMode) return;
3242
- if (!audioContext)
3243
- audioContext = new AudioContext; // create audio context
3244
3435
 
3436
+ this.randomness = randomness;
3245
3437
  fetch(filename)
3246
3438
  .then(response => response.arrayBuffer())
3247
3439
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
@@ -3251,10 +3443,23 @@ class SoundWave extends Sound
3251
3443
  for (let i = audioBuffer.numberOfChannels; i--;)
3252
3444
  this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
3253
3445
  this.sampleRate = audioBuffer.sampleRate;
3254
- });
3446
+ }).then(() => onloadCallback && onloadCallback(this));
3255
3447
  }
3256
3448
  }
3257
3449
 
3450
+ /** Play an mp3, ogg, or wav audio from a local file or url
3451
+ * @param {String} filename - Location of sound file to play
3452
+ * @param {Number} [volume] - How much to scale volume by
3453
+ * @param {Boolean} [loop] - True if the music should loop
3454
+ * @return {SoundWave} - The sound object for this file
3455
+ * @memberof Audio */
3456
+ function playAudioFile(filename, volume=1, loop=false)
3457
+ {
3458
+ if (!soundEnable || headlessMode) return;
3459
+
3460
+ return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3461
+ }
3462
+
3258
3463
  /**
3259
3464
  * Music Object - Stores a zzfx music track for later use
3260
3465
  *
@@ -3309,23 +3514,6 @@ class Music extends Sound
3309
3514
  { return super.play(undefined, volume, 1, 1, loop); }
3310
3515
  }
3311
3516
 
3312
- /** Play an mp3, ogg, or wav audio from a local file or url
3313
- * @param {String} filename - Location of sound file to play
3314
- * @param {Number} [volume] - How much to scale volume by
3315
- * @param {Boolean} [loop] - True if the music should loop
3316
- * @return {HTMLAudioElement} - The audio element for this sound
3317
- * @memberof Audio */
3318
- function playAudioFile(filename, volume=1, loop=false)
3319
- {
3320
- if (!soundEnable || headlessMode) return;
3321
-
3322
- const audio = new Audio(filename);
3323
- audio.volume = soundVolume * volume;
3324
- audio.loop = loop;
3325
- audio.play();
3326
- return audio;
3327
- }
3328
-
3329
3517
  /** Speak text with passed in settings
3330
3518
  * @param {String} text - The text to speak
3331
3519
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
@@ -3336,7 +3524,8 @@ function playAudioFile(filename, volume=1, loop=false)
3336
3524
  * @memberof Audio */
3337
3525
  function speak(text, language='', volume=1, rate=1, pitch=1)
3338
3526
  {
3339
- if (!soundEnable || !speechSynthesis || headlessMode) return;
3527
+ if (!soundEnable || headlessMode) return;
3528
+ if (!speechSynthesis) return;
3340
3529
 
3341
3530
  // common languages (not supported by all browsers)
3342
3531
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -3366,30 +3555,23 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3366
3555
 
3367
3556
  ///////////////////////////////////////////////////////////////////////////////
3368
3557
 
3369
- /** Audio context used by the engine
3370
- * @type {AudioContext}
3371
- * @memberof Audio */
3372
- let audioContext;
3373
-
3374
- /** Keep track if audio was suspended when last sound was played
3375
- * @type {Boolean}
3376
- * @memberof Audio */
3558
+ // internal tracking if audio was suspended when last sound was played
3559
+ // allows first suspended sound to play when audio is resumed
3377
3560
  let audioSuspended = false;
3378
3561
 
3379
3562
  /** Play cached audio samples with given settings
3380
- * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3381
- * @param {Number} [volume] - How much to scale volume by
3382
- * @param {Number} [rate] - The playback rate to use
3383
- * @param {Number} [pan] - How much to apply stereo panning
3384
- * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
3385
- * @param {Number} [sampleRate=44100] - Sample rate for the sound
3563
+ * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3564
+ * @param {Number} [volume] - How much to scale volume by
3565
+ * @param {Number} [rate] - The playback rate to use
3566
+ * @param {Number} [pan] - How much to apply stereo panning
3567
+ * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
3568
+ * @param {Number} [sampleRate=44100] - Sample rate for the sound
3569
+ * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
3386
3570
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3387
3571
  * @memberof Audio */
3388
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
3572
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
3389
3573
  {
3390
3574
  if (!soundEnable || headlessMode) return;
3391
- if (!audioContext)
3392
- audioContext = new AudioContext; // create audio context
3393
3575
 
3394
3576
  // prevent sounds from building up if they can't be played
3395
3577
  const audioWasSuspended = audioSuspended;
@@ -3413,10 +3595,10 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3413
3595
  source.playbackRate.value = rate;
3414
3596
  source.loop = loop;
3415
3597
 
3416
- // create and connect gain node (createGain is more widely spported then GainNode construtor)
3417
- const gainNode = audioContext.createGain();
3418
- gainNode.gain.value = soundVolume*volume;
3419
- gainNode.connect(audioContext.destination);
3598
+ // create and connect gain node
3599
+ gainNode = gainNode || audioContext.createGain();
3600
+ gainNode.gain.value = volume;
3601
+ gainNode.connect(audioGainNode);
3420
3602
 
3421
3603
  // connect source to stereo panner and gain
3422
3604
  source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
@@ -4323,6 +4505,9 @@ class Particle extends EngineObject
4323
4505
  this.localSpaceEmitter = localSpaceEmitter;
4324
4506
  /** @property {Function} - Called when particle dies */
4325
4507
  this.destroyCallback = destroyCallback;
4508
+
4509
+ // particles use circular clamped speed
4510
+ this.clampSpeedLinear = false;
4326
4511
  }
4327
4512
 
4328
4513
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -4392,9 +4577,9 @@ class Particle extends EngineObject
4392
4577
 
4393
4578
 
4394
4579
  /** List of all medals
4395
- * @type {Array}
4580
+ * @type {Object}
4396
4581
  * @memberof Medals */
4397
- const medals = [];
4582
+ const medals = {};
4398
4583
 
4399
4584
  // Engine internal variables not exposed to documentation
4400
4585
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
@@ -4410,9 +4595,45 @@ function medalsInit(saveName)
4410
4595
  {
4411
4596
  // check if medals are unlocked
4412
4597
  medalsSaveName = saveName;
4413
- debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4598
+ if (!debugMedals)
4599
+ medalsForEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4600
+
4601
+ // engine automatically renders medals
4602
+ addPluginRender(function()
4603
+ {
4604
+ if (!medalsDisplayQueue.length)
4605
+ return;
4606
+
4607
+ // update first medal in queue
4608
+ const medal = medalsDisplayQueue[0];
4609
+ const time = timeReal - medalsDisplayTimeLast;
4610
+ if (!medalsDisplayTimeLast)
4611
+ medalsDisplayTimeLast = timeReal;
4612
+ else if (time > medalDisplayTime)
4613
+ {
4614
+ medalsDisplayTimeLast = 0;
4615
+ medalsDisplayQueue.shift();
4616
+ }
4617
+ else
4618
+ {
4619
+ // slide on/off medals
4620
+ const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
4621
+ const hidePercent =
4622
+ time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
4623
+ time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
4624
+ medal.render(hidePercent);
4625
+ }
4626
+ });
4414
4627
  }
4415
4628
 
4629
+ /** Calls a function for each medal
4630
+ * @param {Function} callback
4631
+ * @memberof Medals */
4632
+ function medalsForEach(callback)
4633
+ { Object.values(medals).forEach(medal=>callback(medal)); }
4634
+
4635
+ ///////////////////////////////////////////////////////////////////////////////
4636
+
4416
4637
  /**
4417
4638
  * Medal - Tracks an unlockable medal
4418
4639
  * @example
@@ -4457,7 +4678,6 @@ class Medal
4457
4678
  ASSERT(medalsSaveName, 'save name must be set');
4458
4679
  localStorage[this.storageKey()] = this.unlocked = 1;
4459
4680
  medalsDisplayQueue.push(this);
4460
- newgrounds && newgrounds.unlockMedal(this.id);
4461
4681
  }
4462
4682
 
4463
4683
  /** Render a medal
@@ -4505,173 +4725,6 @@ class Medal
4505
4725
 
4506
4726
  // Get local storage key used by the medal
4507
4727
  storageKey() { return medalsSaveName + '_' + this.id; }
4508
- }
4509
-
4510
- // engine automatically renders medals
4511
- function medalsRender()
4512
- {
4513
- if (!medalsDisplayQueue.length)
4514
- return;
4515
-
4516
- // update first medal in queue
4517
- const medal = medalsDisplayQueue[0];
4518
- const time = timeReal - medalsDisplayTimeLast;
4519
- if (!medalsDisplayTimeLast)
4520
- medalsDisplayTimeLast = timeReal;
4521
- else if (time > medalDisplayTime)
4522
- {
4523
- medalsDisplayTimeLast = 0;
4524
- medalsDisplayQueue.shift();
4525
- }
4526
- else
4527
- {
4528
- // slide on/off medals
4529
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
4530
- const hidePercent =
4531
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
4532
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
4533
- medal.render(hidePercent);
4534
- }
4535
- }
4536
-
4537
- ///////////////////////////////////////////////////////////////////////////////
4538
-
4539
- // global Newgrounds object
4540
- let newgrounds;
4541
-
4542
- /** This can used to enable Newgrounds functionality
4543
- * @param {Number} app_id - The newgrounds App ID
4544
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4545
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
4546
- * @memberof Medals */
4547
- function newgroundsInit(app_id, cipher, cryptoJS)
4548
- { newgrounds = new Newgrounds(app_id, cipher, cryptoJS); }
4549
-
4550
- /**
4551
- * Newgrounds API wrapper object
4552
- * @example
4553
- * // create a newgrounds object, replace the app id with your own
4554
- * const app_id = '53123:1ZuSTQ9l';
4555
- * newgrounds = new Newgrounds(app_id);
4556
- */
4557
- class Newgrounds
4558
- {
4559
- /** Create a newgrounds object
4560
- * @param {Number} app_id - The newgrounds App ID
4561
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4562
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */
4563
- constructor(app_id, cipher, cryptoJS)
4564
- {
4565
- ASSERT(!newgrounds && app_id>0, 'there can only be one newgrounds object');
4566
- ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
4567
-
4568
- this.app_id = app_id;
4569
- this.cipher = cipher;
4570
- this.cryptoJS = cryptoJS;
4571
- this.host = location ? location.hostname : '';
4572
-
4573
- // get session id from url search params
4574
- const url = new URL(location.href);
4575
- this.session_id = url.searchParams.get('ngio_session_id');
4576
-
4577
- if (!this.session_id)
4578
- return; // only use newgrounds when logged in
4579
-
4580
- // get medals
4581
- const medalsResult = this.call('Medal.getList');
4582
- this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
4583
- debugMedals && console.log(this.medals);
4584
- for (const newgroundsMedal of this.medals)
4585
- {
4586
- const medal = medals[newgroundsMedal['id']];
4587
- if (medal)
4588
- {
4589
- // copy newgrounds medal data
4590
- medal.image = new Image;
4591
- medal.image.src = newgroundsMedal['icon'];
4592
- medal.name = newgroundsMedal['name'];
4593
- medal.description = newgroundsMedal['description'];
4594
- medal.unlocked = newgroundsMedal['unlocked'];
4595
- medal.difficulty = newgroundsMedal['difficulty'];
4596
- medal.value = newgroundsMedal['value'];
4597
-
4598
- if (medal.value)
4599
- medal.description = medal.description + ' (' + medal.value + ')';
4600
- }
4601
- }
4602
-
4603
- // get scoreboards
4604
- const scoreboardResult = this.call('ScoreBoard.getBoards');
4605
- this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
4606
- debugMedals && console.log(this.scoreboards);
4607
-
4608
- const keepAliveMS = 5 * 60 * 1e3;
4609
- setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
4610
- }
4611
-
4612
- /** Send message to unlock a medal by id
4613
- * @param {Number} id - The medal id */
4614
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
4615
-
4616
- /** Send message to post score
4617
- * @param {Number} id - The scoreboard id
4618
- * @param {Number} value - The score value */
4619
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
4620
-
4621
- /** Get scores from a scoreboard
4622
- * @param {Number} id - The scoreboard id
4623
- * @param {String} [user] - A user's id or name
4624
- * @param {Number} [social] - If true, only social scores will be loaded
4625
- * @param {Number} [skip] - Number of scores to skip before start
4626
- * @param {Number} [limit] - Number of scores to include in the list
4627
- * @return {Object} - The response JSON object
4628
- */
4629
- getScores(id, user, social=0, skip=0, limit=10)
4630
- { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
4631
-
4632
- /** Send message to log a view */
4633
- logView() { return this.call('App.logView', {'host':this.host}, true); }
4634
-
4635
- /** Send a message to call a component of the Newgrounds API
4636
- * @param {String} component - Name of the component
4637
- * @param {Object} [parameters] - Parameters to use for call
4638
- * @param {Boolean} [async] - If true, don't wait for response before continuing
4639
- * @return {Object} - The response JSON object
4640
- */
4641
- call(component, parameters, async=false)
4642
- {
4643
- const call = {'component':component, 'parameters':parameters};
4644
- if (this.cipher)
4645
- {
4646
- // encrypt using AES-128 Base64 with cryptoJS
4647
- const cryptoJS = this.cryptoJS;
4648
- const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
4649
- const iv = cryptoJS['lib']['WordArray']['random'](16);
4650
- const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
4651
- call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
4652
- call['parameters'] = 0;
4653
- }
4654
-
4655
- // build the input object
4656
- const input =
4657
- {
4658
- 'app_id': this.app_id,
4659
- 'session_id': this.session_id,
4660
- 'call': call
4661
- };
4662
-
4663
- // build post data
4664
- const formData = new FormData();
4665
- formData.append('input', JSON.stringify(input));
4666
-
4667
- // send post data
4668
- const xmlHttp = new XMLHttpRequest();
4669
- const url = 'https://newgrounds.io/gateway_v3.php';
4670
- xmlHttp.open('POST', url, !debugMedals && async);
4671
- xmlHttp.send(formData);
4672
- debugMedals && console.log(xmlHttp.responseText);
4673
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
4674
- }
4675
4728
  }
4676
4729
  /**
4677
4730
  * LittleJS WebGL Interface
@@ -4762,7 +4815,7 @@ function glPreRender()
4762
4815
 
4763
4816
  // clear and set to same size as main canvas
4764
4817
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4765
- //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4818
+ glContext.clear(gl_COLOR_BUFFER_BIT);
4766
4819
 
4767
4820
  // set up the shader
4768
4821
  glContext.useProgram(glShader);
@@ -4945,99 +4998,6 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdd
4945
4998
  glInstanceCount++;
4946
4999
  }
4947
5000
 
4948
- ///////////////////////////////////////////////////////////////////////////////
4949
- // post processing - can be enabled to pass other canvases through a final shader
4950
-
4951
- let glPostShader, glPostTexture, glPostIncludeOverlay;
4952
-
4953
- /** Set up a post processing shader
4954
- * @param {String} shaderCode
4955
- * @param {Boolean} includeOverlay
4956
- * @memberof WebGL */
4957
- function glInitPostProcess(shaderCode, includeOverlay=false)
4958
- {
4959
- ASSERT(!glPostShader, 'can only have 1 post effects shader');
4960
- if (headlessMode) return;
4961
- if (!shaderCode) // default shader pass through
4962
- shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4963
-
4964
- // create the shader
4965
- glPostShader = glCreateProgram(
4966
- '#version 300 es\n' + // specify GLSL ES version
4967
- 'precision highp float;'+ // use highp for better accuracy
4968
- 'in vec2 p;'+ // position
4969
- 'void main(){'+ // shader entry point
4970
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
4971
- '}' // end of shader
4972
- ,
4973
- '#version 300 es\n' + // specify GLSL ES version
4974
- 'precision highp float;'+ // use highp for better accuracy
4975
- 'uniform sampler2D iChannel0;'+ // input texture
4976
- 'uniform vec3 iResolution;'+ // size of output texture
4977
- 'uniform float iTime;'+ // time
4978
- 'out vec4 c;'+ // out color
4979
- '\n' + shaderCode + '\n'+ // insert custom shader code
4980
- 'void main(){'+ // shader entry point
4981
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
4982
- 'c.a=1.;'+ // always use full alpha
4983
- '}' // end of shader
4984
- );
4985
-
4986
- // create buffer and texture
4987
- glPostTexture = glCreateTexture(undefined);
4988
- glPostIncludeOverlay = includeOverlay;
4989
-
4990
- // hide the original 2d canvas
4991
- mainCanvas.style.visibility = 'hidden';
4992
- if (glPostIncludeOverlay)
4993
- overlayCanvas.style.visibility = 'hidden';
4994
- }
4995
-
4996
- // Render the post processing shader, called automatically by the engine
4997
- function glRenderPostProcess()
4998
- {
4999
- if (!glPostShader || headlessMode) return;
5000
-
5001
- // prepare to render post process shader
5002
- if (glEnable)
5003
- {
5004
- glFlush(); // clear out the buffer
5005
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
5006
- }
5007
- else
5008
- {
5009
- // set the viewport
5010
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
5011
- }
5012
-
5013
- // copy overlay canvas so it will be included in post processing
5014
- glPostIncludeOverlay && mainContext.drawImage(overlayCanvas, 0, 0);
5015
-
5016
- // setup shader program to draw one triangle
5017
- glContext.useProgram(glPostShader);
5018
- glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
5019
- glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, 1);
5020
- glContext.disable(gl_BLEND);
5021
-
5022
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
5023
- glContext.activeTexture(gl_TEXTURE0);
5024
- glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
5025
- glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
5026
-
5027
- // set vertex position attribute
5028
- const vertexByteStride = 8;
5029
- const pLocation = glContext.getAttribLocation(glPostShader, 'p');
5030
- glContext.enableVertexAttribArray(pLocation);
5031
- glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, false, vertexByteStride, 0);
5032
-
5033
- // set uniforms and draw
5034
- const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
5035
- glContext.uniform1i(uniformLocation('iChannel0'), 0);
5036
- glContext.uniform1f(uniformLocation('iTime'), time);
5037
- glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
5038
- glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 4);
5039
- }
5040
-
5041
5001
  ///////////////////////////////////////////////////////////////////////////////
5042
5002
  // store gl constants as integers so their name doesn't use space in minifed
5043
5003
  const
@@ -5102,7 +5062,7 @@ const engineName = 'LittleJS';
5102
5062
  * @type {String}
5103
5063
  * @default
5104
5064
  * @memberof Engine */
5105
- const engineVersion = '1.9.6';
5065
+ const engineVersion = '1.9.8';
5106
5066
 
5107
5067
  /** Frames per second to update
5108
5068
  * @type {Number}
@@ -5156,6 +5116,22 @@ function setPaused(isPaused) { paused = isPaused; }
5156
5116
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
5157
5117
 
5158
5118
  ///////////////////////////////////////////////////////////////////////////////
5119
+ // plugin hooks
5120
+
5121
+ const pluginUpdateList = [], pluginRenderList = [];
5122
+
5123
+ /** Add a new update function for a plugin
5124
+ * @param {Function} updateFunction
5125
+ * @memberof Engine */
5126
+ function addPluginUpdate(updateFunction) { pluginUpdateList.push(updateFunction); }
5127
+
5128
+ /** Add a new render function for a plugin
5129
+ * @param {Function} renderFunction
5130
+ * @memberof Engine */
5131
+ function addPluginRender(renderFunction) { pluginRenderList.push(renderFunction); }
5132
+
5133
+ ///////////////////////////////////////////////////////////////////////////////
5134
+ // Main engine functions
5159
5135
 
5160
5136
  /** Startup LittleJS engine with your callback functions
5161
5137
  * @param {Function} gameInit - Called once after the engine starts up, setup the game
@@ -5231,6 +5207,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5231
5207
  // update game and objects
5232
5208
  inputUpdate();
5233
5209
  gameUpdate();
5210
+ pluginUpdateList.forEach(f=>f());
5234
5211
  engineObjectsUpdate();
5235
5212
 
5236
5213
  // do post update
@@ -5252,8 +5229,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5252
5229
  for (const o of engineObjects)
5253
5230
  o.destroyed || o.render();
5254
5231
  gameRenderPost();
5255
- glRenderPostProcess();
5256
- medalsRender();
5232
+ pluginRenderList.forEach(f=>f());
5257
5233
  touchGamepadRender();
5258
5234
  debugRender();
5259
5235
  glCopyToContext(mainContext);
@@ -5335,6 +5311,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5335
5311
 
5336
5312
  // init stuff and start engine
5337
5313
  inputInit();
5314
+ audioInit();
5338
5315
  debugInit();
5339
5316
  glInit();
5340
5317
 
@@ -5542,21 +5519,23 @@ function drawEngineSplashScreen(t)
5542
5519
  x.setLineDash([99*p2,99]);
5543
5520
 
5544
5521
  // cab top
5545
- rect(7,17,18,-8,color(2,2));
5546
- rect(7,9,18,4,color(2,3));
5547
- rect(25,9,8,8,color(2,1));
5548
- rect(25,9,-18,8);
5549
- rect(25,9,8,8);
5522
+ rect(7,16,18,-8,color(2,2));
5523
+ rect(7,8,18,4,color(2,3));
5524
+ rect(25,8,8,8,color(2,1));
5525
+ rect(25,8,-18,8);
5526
+ rect(25,8,8,8);
5550
5527
 
5551
5528
  // cab
5552
- rect(25,17,7,22,color());
5553
- rect(11,40,14,-23,color(1,1));
5554
- rect(11,17,14,17,color(1,2));
5555
- rect(11,17,14,9,color(1,3));
5556
- rect(15,31,6,-9,color(2,2));
5557
- circle(15,23,5,0,PI/2,color(2,4),1);
5558
- rect(25,17,-14,23);
5559
- rect(21,22,-6,9);
5529
+ rect(25,16,7,23,color());
5530
+ rect(11,39,14,-23,color(1,1));
5531
+ rect(11,16,14,18,color(1,2));
5532
+ rect(11,16,14,8,color(1,3));
5533
+ rect(25,16,-14,24);
5534
+
5535
+ // cab window
5536
+ rect(15,29,6,-9,color(2,2));
5537
+ circle(15,21,5,0,PI/2,color(2,4),1);
5538
+ rect(21,21,-6,9);
5560
5539
 
5561
5540
  // little stack
5562
5541
  rect(37,14,9,6,color(3,2));
@@ -5564,16 +5543,16 @@ function drawEngineSplashScreen(t)
5564
5543
  rect(37,14,9,6);
5565
5544
 
5566
5545
  // big stack
5567
- rect(50,20,10,-8,color(0,1))
5568
- rect(50,20,6.5,-8,color(0,2))
5569
- rect(50,20,3.5,-8,color(0,3))
5570
- rect(50,20,10,-8)
5571
- circle(55,2,11.4,.5,PI-.5,color(3,3))
5572
- circle(55,2,11.4,.5,PI/2,color(3,2),1)
5573
- circle(55,2,11.4,.5,PI-.5)
5574
- rect(45,7,20,-7,color(0,2))
5575
- rect(45,-1,20,4,color(0,3))
5576
- rect(45,-1,20,8)
5546
+ rect(50,20,10,-8,color(0,1));
5547
+ rect(50,20,6.5,-8,color(0,2));
5548
+ rect(50,20,3.5,-8,color(0,3));
5549
+ rect(50,20,10,-8);
5550
+ circle(55,2,11.4,.5,PI-.5,color(3,3));
5551
+ circle(55,2,11.4,.5,PI/2,color(3,2),1);
5552
+ circle(55,2,11.4,.5,PI-.5);
5553
+ rect(45,7,20,-7,color(0,2));
5554
+ rect(45,-1,20,4,color(0,3));
5555
+ rect(45,-1,20,8);
5577
5556
 
5578
5557
  // engine
5579
5558
  for (let i=5; i--;)