littlejsengine 1.9.7 → 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); }
@@ -1169,6 +1219,64 @@ class Color
1169
1219
  }
1170
1220
  }
1171
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
+
1172
1280
  ///////////////////////////////////////////////////////////////////////////////
1173
1281
 
1174
1282
  /**
@@ -1249,9 +1357,9 @@ let cameraScale = 32;
1249
1357
 
1250
1358
  /** The max size of the canvas, centered if window is larger
1251
1359
  * @type {Vector2}
1252
- * @default Vector2(1920,1200)
1360
+ * @default Vector2(1920,1080)
1253
1361
  * @memberof Settings */
1254
- let canvasMaxSize = vec2(1920, 1200);
1362
+ let canvasMaxSize = vec2(1920, 1080);
1255
1363
 
1256
1364
  /** Fixed size of the canvas, if enabled canvas size never changes
1257
1365
  * - you may also need to set mainCanvasSize if using screen space coords in startup
@@ -1898,7 +2006,7 @@ class EngineObject
1898
2006
  if (o.mass) // push away if not fixed
1899
2007
  o.velocity = o.velocity.subtract(velocity);
1900
2008
 
1901
- 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');
1902
2010
  continue;
1903
2011
  }
1904
2012
 
@@ -1960,7 +2068,7 @@ class EngineObject
1960
2068
  else // bounce if other object is fixed
1961
2069
  this.velocity.x *= -elasticity;
1962
2070
  }
1963
- 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');
1964
2072
  }
1965
2073
  }
1966
2074
  if (this.collideTiles)
@@ -2021,6 +2129,22 @@ class EngineObject
2021
2129
  for (const child of this.children)
2022
2130
  child.destroy(child.parent = 0);
2023
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); }
2024
2148
 
2025
2149
  /** Called to check if a tile collision should be resolved
2026
2150
  * @param {Number} tileData - the value of the tile at the position
@@ -2107,6 +2231,18 @@ class EngineObject
2107
2231
  return text;
2108
2232
  }
2109
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
+ }
2110
2246
  }
2111
2247
  /**
2112
2248
  * LittleJS Drawing System
@@ -2430,7 +2566,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2430
2566
  context.save();
2431
2567
  context.translate(pos.x+.5, pos.y+.5);
2432
2568
  context.rotate(angle);
2433
- context.scale(mirror ? -size.x : size.x, size.y);
2569
+ context.scale(mirror ? -size.x : size.x, -size.y);
2434
2570
  drawFunction(context);
2435
2571
  context.restore();
2436
2572
  }
@@ -3226,7 +3362,17 @@ class Sound
3226
3362
 
3227
3363
  // play the sound
3228
3364
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
3229
- 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;
3230
3376
  }
3231
3377
 
3232
3378
  /** Stop the last instance of this sound that was played */
@@ -3414,15 +3560,16 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3414
3560
  let audioSuspended = false;
3415
3561
 
3416
3562
  /** Play cached audio samples with given settings
3417
- * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3418
- * @param {Number} [volume] - How much to scale volume by
3419
- * @param {Number} [rate] - The playback rate to use
3420
- * @param {Number} [pan] - How much to apply stereo panning
3421
- * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
3422
- * @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
3423
3570
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3424
3571
  * @memberof Audio */
3425
- 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)
3426
3573
  {
3427
3574
  if (!soundEnable || headlessMode) return;
3428
3575
 
@@ -3448,11 +3595,8 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3448
3595
  source.playbackRate.value = rate;
3449
3596
  source.loop = loop;
3450
3597
 
3451
- // set master gain volume
3452
- setSoundVolume(soundVolume);
3453
-
3454
3598
  // create and connect gain node
3455
- const gainNode = audioContext.createGain();
3599
+ gainNode = gainNode || audioContext.createGain();
3456
3600
  gainNode.gain.value = volume;
3457
3601
  gainNode.connect(audioGainNode);
3458
3602
 
@@ -4433,9 +4577,9 @@ class Particle extends EngineObject
4433
4577
 
4434
4578
 
4435
4579
  /** List of all medals
4436
- * @type {Array}
4580
+ * @type {Object}
4437
4581
  * @memberof Medals */
4438
- const medals = [];
4582
+ const medals = {};
4439
4583
 
4440
4584
  // Engine internal variables not exposed to documentation
4441
4585
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
@@ -4451,9 +4595,45 @@ function medalsInit(saveName)
4451
4595
  {
4452
4596
  // check if medals are unlocked
4453
4597
  medalsSaveName = saveName;
4454
- 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
+ });
4455
4627
  }
4456
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
+
4457
4637
  /**
4458
4638
  * Medal - Tracks an unlockable medal
4459
4639
  * @example
@@ -4498,7 +4678,6 @@ class Medal
4498
4678
  ASSERT(medalsSaveName, 'save name must be set');
4499
4679
  localStorage[this.storageKey()] = this.unlocked = 1;
4500
4680
  medalsDisplayQueue.push(this);
4501
- newgrounds && newgrounds.unlockMedal(this.id);
4502
4681
  }
4503
4682
 
4504
4683
  /** Render a medal
@@ -4546,173 +4725,6 @@ class Medal
4546
4725
 
4547
4726
  // Get local storage key used by the medal
4548
4727
  storageKey() { return medalsSaveName + '_' + this.id; }
4549
- }
4550
-
4551
- // engine automatically renders medals
4552
- function medalsRender()
4553
- {
4554
- if (!medalsDisplayQueue.length)
4555
- return;
4556
-
4557
- // update first medal in queue
4558
- const medal = medalsDisplayQueue[0];
4559
- const time = timeReal - medalsDisplayTimeLast;
4560
- if (!medalsDisplayTimeLast)
4561
- medalsDisplayTimeLast = timeReal;
4562
- else if (time > medalDisplayTime)
4563
- {
4564
- medalsDisplayTimeLast = 0;
4565
- medalsDisplayQueue.shift();
4566
- }
4567
- else
4568
- {
4569
- // slide on/off medals
4570
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
4571
- const hidePercent =
4572
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
4573
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
4574
- medal.render(hidePercent);
4575
- }
4576
- }
4577
-
4578
- ///////////////////////////////////////////////////////////////////////////////
4579
-
4580
- // global Newgrounds object
4581
- let newgrounds;
4582
-
4583
- /** This can used to enable Newgrounds functionality
4584
- * @param {Number} app_id - The newgrounds App ID
4585
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4586
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
4587
- * @memberof Medals */
4588
- function newgroundsInit(app_id, cipher, cryptoJS)
4589
- { newgrounds = new Newgrounds(app_id, cipher, cryptoJS); }
4590
-
4591
- /**
4592
- * Newgrounds API wrapper object
4593
- * @example
4594
- * // create a newgrounds object, replace the app id with your own
4595
- * const app_id = '53123:1ZuSTQ9l';
4596
- * newgrounds = new Newgrounds(app_id);
4597
- */
4598
- class Newgrounds
4599
- {
4600
- /** Create a newgrounds object
4601
- * @param {Number} app_id - The newgrounds App ID
4602
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
4603
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */
4604
- constructor(app_id, cipher, cryptoJS)
4605
- {
4606
- ASSERT(!newgrounds && app_id>0, 'there can only be one newgrounds object');
4607
- ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
4608
-
4609
- this.app_id = app_id;
4610
- this.cipher = cipher;
4611
- this.cryptoJS = cryptoJS;
4612
- this.host = location ? location.hostname : '';
4613
-
4614
- // get session id from url search params
4615
- const url = new URL(location.href);
4616
- this.session_id = url.searchParams.get('ngio_session_id');
4617
-
4618
- if (!this.session_id)
4619
- return; // only use newgrounds when logged in
4620
-
4621
- // get medals
4622
- const medalsResult = this.call('Medal.getList');
4623
- this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
4624
- debugMedals && console.log(this.medals);
4625
- for (const newgroundsMedal of this.medals)
4626
- {
4627
- const medal = medals[newgroundsMedal['id']];
4628
- if (medal)
4629
- {
4630
- // copy newgrounds medal data
4631
- medal.image = new Image;
4632
- medal.image.src = newgroundsMedal['icon'];
4633
- medal.name = newgroundsMedal['name'];
4634
- medal.description = newgroundsMedal['description'];
4635
- medal.unlocked = newgroundsMedal['unlocked'];
4636
- medal.difficulty = newgroundsMedal['difficulty'];
4637
- medal.value = newgroundsMedal['value'];
4638
-
4639
- if (medal.value)
4640
- medal.description = medal.description + ' (' + medal.value + ')';
4641
- }
4642
- }
4643
-
4644
- // get scoreboards
4645
- const scoreboardResult = this.call('ScoreBoard.getBoards');
4646
- this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
4647
- debugMedals && console.log(this.scoreboards);
4648
-
4649
- const keepAliveMS = 5 * 60 * 1e3;
4650
- setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
4651
- }
4652
-
4653
- /** Send message to unlock a medal by id
4654
- * @param {Number} id - The medal id */
4655
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
4656
-
4657
- /** Send message to post score
4658
- * @param {Number} id - The scoreboard id
4659
- * @param {Number} value - The score value */
4660
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
4661
-
4662
- /** Get scores from a scoreboard
4663
- * @param {Number} id - The scoreboard id
4664
- * @param {String} [user] - A user's id or name
4665
- * @param {Number} [social] - If true, only social scores will be loaded
4666
- * @param {Number} [skip] - Number of scores to skip before start
4667
- * @param {Number} [limit] - Number of scores to include in the list
4668
- * @return {Object} - The response JSON object
4669
- */
4670
- getScores(id, user, social=0, skip=0, limit=10)
4671
- { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
4672
-
4673
- /** Send message to log a view */
4674
- logView() { return this.call('App.logView', {'host':this.host}, true); }
4675
-
4676
- /** Send a message to call a component of the Newgrounds API
4677
- * @param {String} component - Name of the component
4678
- * @param {Object} [parameters] - Parameters to use for call
4679
- * @param {Boolean} [async] - If true, don't wait for response before continuing
4680
- * @return {Object} - The response JSON object
4681
- */
4682
- call(component, parameters, async=false)
4683
- {
4684
- const call = {'component':component, 'parameters':parameters};
4685
- if (this.cipher)
4686
- {
4687
- // encrypt using AES-128 Base64 with cryptoJS
4688
- const cryptoJS = this.cryptoJS;
4689
- const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
4690
- const iv = cryptoJS['lib']['WordArray']['random'](16);
4691
- const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
4692
- call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
4693
- call['parameters'] = 0;
4694
- }
4695
-
4696
- // build the input object
4697
- const input =
4698
- {
4699
- 'app_id': this.app_id,
4700
- 'session_id': this.session_id,
4701
- 'call': call
4702
- };
4703
-
4704
- // build post data
4705
- const formData = new FormData();
4706
- formData.append('input', JSON.stringify(input));
4707
-
4708
- // send post data
4709
- const xmlHttp = new XMLHttpRequest();
4710
- const url = 'https://newgrounds.io/gateway_v3.php';
4711
- xmlHttp.open('POST', url, !debugMedals && async);
4712
- xmlHttp.send(formData);
4713
- debugMedals && console.log(xmlHttp.responseText);
4714
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
4715
- }
4716
4728
  }
4717
4729
  /**
4718
4730
  * LittleJS WebGL Interface
@@ -4803,7 +4815,7 @@ function glPreRender()
4803
4815
 
4804
4816
  // clear and set to same size as main canvas
4805
4817
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4806
- //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4818
+ glContext.clear(gl_COLOR_BUFFER_BIT);
4807
4819
 
4808
4820
  // set up the shader
4809
4821
  glContext.useProgram(glShader);
@@ -4986,99 +4998,6 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdd
4986
4998
  glInstanceCount++;
4987
4999
  }
4988
5000
 
4989
- ///////////////////////////////////////////////////////////////////////////////
4990
- // post processing - can be enabled to pass other canvases through a final shader
4991
-
4992
- let glPostShader, glPostTexture, glPostIncludeOverlay;
4993
-
4994
- /** Set up a post processing shader
4995
- * @param {String} shaderCode
4996
- * @param {Boolean} includeOverlay
4997
- * @memberof WebGL */
4998
- function glInitPostProcess(shaderCode, includeOverlay=false)
4999
- {
5000
- ASSERT(!glPostShader, 'can only have 1 post effects shader');
5001
- if (headlessMode) return;
5002
- if (!shaderCode) // default shader pass through
5003
- shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
5004
-
5005
- // create the shader
5006
- glPostShader = glCreateProgram(
5007
- '#version 300 es\n' + // specify GLSL ES version
5008
- 'precision highp float;'+ // use highp for better accuracy
5009
- 'in vec2 p;'+ // position
5010
- 'void main(){'+ // shader entry point
5011
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
5012
- '}' // end of shader
5013
- ,
5014
- '#version 300 es\n' + // specify GLSL ES version
5015
- 'precision highp float;'+ // use highp for better accuracy
5016
- 'uniform sampler2D iChannel0;'+ // input texture
5017
- 'uniform vec3 iResolution;'+ // size of output texture
5018
- 'uniform float iTime;'+ // time
5019
- 'out vec4 c;'+ // out color
5020
- '\n' + shaderCode + '\n'+ // insert custom shader code
5021
- 'void main(){'+ // shader entry point
5022
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
5023
- 'c.a=1.;'+ // always use full alpha
5024
- '}' // end of shader
5025
- );
5026
-
5027
- // create buffer and texture
5028
- glPostTexture = glCreateTexture(undefined);
5029
- glPostIncludeOverlay = includeOverlay;
5030
-
5031
- // hide the original 2d canvas
5032
- mainCanvas.style.visibility = 'hidden';
5033
- if (glPostIncludeOverlay)
5034
- overlayCanvas.style.visibility = 'hidden';
5035
- }
5036
-
5037
- // Render the post processing shader, called automatically by the engine
5038
- function glRenderPostProcess()
5039
- {
5040
- if (!glPostShader || headlessMode) return;
5041
-
5042
- // prepare to render post process shader
5043
- if (glEnable)
5044
- {
5045
- glFlush(); // clear out the buffer
5046
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
5047
- }
5048
- else
5049
- {
5050
- // set the viewport
5051
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
5052
- }
5053
-
5054
- // copy overlay canvas so it will be included in post processing
5055
- glPostIncludeOverlay && mainContext.drawImage(overlayCanvas, 0, 0);
5056
-
5057
- // setup shader program to draw one triangle
5058
- glContext.useProgram(glPostShader);
5059
- glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
5060
- glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, 1);
5061
- glContext.disable(gl_BLEND);
5062
-
5063
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
5064
- glContext.activeTexture(gl_TEXTURE0);
5065
- glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
5066
- glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
5067
-
5068
- // set vertex position attribute
5069
- const vertexByteStride = 8;
5070
- const pLocation = glContext.getAttribLocation(glPostShader, 'p');
5071
- glContext.enableVertexAttribArray(pLocation);
5072
- glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, false, vertexByteStride, 0);
5073
-
5074
- // set uniforms and draw
5075
- const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
5076
- glContext.uniform1i(uniformLocation('iChannel0'), 0);
5077
- glContext.uniform1f(uniformLocation('iTime'), time);
5078
- glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
5079
- glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 4);
5080
- }
5081
-
5082
5001
  ///////////////////////////////////////////////////////////////////////////////
5083
5002
  // store gl constants as integers so their name doesn't use space in minifed
5084
5003
  const
@@ -5143,7 +5062,7 @@ const engineName = 'LittleJS';
5143
5062
  * @type {String}
5144
5063
  * @default
5145
5064
  * @memberof Engine */
5146
- const engineVersion = '1.9.7';
5065
+ const engineVersion = '1.9.8';
5147
5066
 
5148
5067
  /** Frames per second to update
5149
5068
  * @type {Number}
@@ -5197,6 +5116,22 @@ function setPaused(isPaused) { paused = isPaused; }
5197
5116
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
5198
5117
 
5199
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
5200
5135
 
5201
5136
  /** Startup LittleJS engine with your callback functions
5202
5137
  * @param {Function} gameInit - Called once after the engine starts up, setup the game
@@ -5272,6 +5207,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5272
5207
  // update game and objects
5273
5208
  inputUpdate();
5274
5209
  gameUpdate();
5210
+ pluginUpdateList.forEach(f=>f());
5275
5211
  engineObjectsUpdate();
5276
5212
 
5277
5213
  // do post update
@@ -5293,8 +5229,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5293
5229
  for (const o of engineObjects)
5294
5230
  o.destroyed || o.render();
5295
5231
  gameRenderPost();
5296
- glRenderPostProcess();
5297
- medalsRender();
5232
+ pluginRenderList.forEach(f=>f());
5298
5233
  touchGamepadRender();
5299
5234
  debugRender();
5300
5235
  glCopyToContext(mainContext);
@@ -5584,21 +5519,23 @@ function drawEngineSplashScreen(t)
5584
5519
  x.setLineDash([99*p2,99]);
5585
5520
 
5586
5521
  // cab top
5587
- rect(7,17,18,-8,color(2,2));
5588
- rect(7,9,18,4,color(2,3));
5589
- rect(25,9,8,8,color(2,1));
5590
- rect(25,9,-18,8);
5591
- 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);
5592
5527
 
5593
5528
  // cab
5594
- rect(25,17,7,22,color());
5595
- rect(11,40,14,-23,color(1,1));
5596
- rect(11,17,14,17,color(1,2));
5597
- rect(11,17,14,9,color(1,3));
5598
- rect(15,31,6,-9,color(2,2));
5599
- circle(15,23,5,0,PI/2,color(2,4),1);
5600
- rect(25,17,-14,23);
5601
- 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);
5602
5539
 
5603
5540
  // little stack
5604
5541
  rect(37,14,9,6,color(3,2));
@@ -5606,16 +5543,16 @@ function drawEngineSplashScreen(t)
5606
5543
  rect(37,14,9,6);
5607
5544
 
5608
5545
  // big stack
5609
- rect(50,20,10,-8,color(0,1))
5610
- rect(50,20,6.5,-8,color(0,2))
5611
- rect(50,20,3.5,-8,color(0,3))
5612
- rect(50,20,10,-8)
5613
- circle(55,2,11.4,.5,PI-.5,color(3,3))
5614
- circle(55,2,11.4,.5,PI/2,color(3,2),1)
5615
- circle(55,2,11.4,.5,PI-.5)
5616
- rect(45,7,20,-7,color(0,2))
5617
- rect(45,-1,20,4,color(0,3))
5618
- 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);
5619
5556
 
5620
5557
  // engine
5621
5558
  for (let i=5; i--;)