littlejsengine 1.1.18 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +17 -11
  2. package/build.bat +1 -0
  3. package/engine/engine.all.js +495 -504
  4. package/engine/engine.all.min.js +1 -1
  5. package/engine/engine.all.release.js +418 -259
  6. package/engine/engine.js +31 -23
  7. package/engine/engineAudio.js +21 -7
  8. package/engine/engineBuild.bat +9 -1
  9. package/engine/engineBuildSetup.bat +2 -1
  10. package/engine/engineDebug.js +81 -247
  11. package/engine/engineDraw.js +109 -21
  12. package/engine/engineFont.png +0 -0
  13. package/engine/engineInput.js +103 -50
  14. package/engine/engineMedals.js +17 -20
  15. package/engine/engineObject.js +14 -23
  16. package/engine/engineParticles.js +8 -6
  17. package/engine/engineRelease.js +3 -1
  18. package/engine/engineSettings.js +20 -10
  19. package/engine/engineTileLayer.js +19 -18
  20. package/engine/engineUtilities.js +14 -12
  21. package/engine/engineWebGL.js +59 -68
  22. package/engine/index.d.ts +1617 -0
  23. package/examples/breakout/game.js +9 -7
  24. package/examples/breakout/gameObjects.js +3 -3
  25. package/examples/breakout/index.html +6 -4
  26. package/examples/platformer/game.js +5 -5
  27. package/examples/platformer/gameEffects.js +11 -14
  28. package/examples/platformer/gameLevel.js +14 -7
  29. package/examples/platformer/gameObjects.js +3 -3
  30. package/examples/platformer/gamePlayer.js +13 -8
  31. package/examples/platformer/index.html +9 -7
  32. package/examples/platformer/tiles.png +0 -0
  33. package/examples/puzzle/game.js +2 -2
  34. package/examples/puzzle/index.html +5 -3
  35. package/examples/screenshot.jpg +0 -0
  36. package/examples/stress/index.html +7 -5
  37. package/game.js +9 -8
  38. package/index.html +16 -14
  39. package/package.json +1 -1
@@ -1,8 +1,15 @@
1
+ /*
2
+ LittleJS - Debug Build
3
+ MIT License - Copyright 2021 Frank Force
4
+ */
5
+
1
6
  /**
2
7
  * LittleJS Debug System
3
- * <br> - Debug overlay with mouse pick
8
+ * <br> - Press ~ to show debug overlay with mouse pick
9
+ * <br> - Number keys toggle debug functions
10
+ * <br> - +/- apply time scale
4
11
  * <br> - Debug primitive rendering
5
- * <br> - Save screenshots to disk
12
+ * <br> - Save a 2d canvas as an image
6
13
  * @namespace Debug
7
14
  */
8
15
 
@@ -34,7 +41,7 @@ let showWatermark = 1;
34
41
  let godMode = 0;
35
42
 
36
43
  // Engine internal variables not exposed to documentation
37
- let debugPrimitives = [], debugOverlay = 0, debugPhysics = 0, debugRaycast = 0,
44
+ let debugPrimitives = [], debugOverlay = 0, debugPhysics = 0, debugRaycast = 0,
38
45
  debugParticles = 0, debugGamepads = 0, debugMedals = 0, debugTakeScreenshot, downloadLink;
39
46
 
40
47
  ///////////////////////////////////////////////////////////////////////////////
@@ -153,25 +160,28 @@ const debugUpdate = ()=>
153
160
  {
154
161
  if (!debug)
155
162
  return;
156
-
163
+
157
164
  if (keyWasPressed(192)) // ~
158
165
  debugOverlay = !debugOverlay;
159
- if (keyWasPressed(49)) // 1
160
- debugPhysics = !debugPhysics, debugParticles = 0;
161
- if (keyWasPressed(50)) // 2
162
- debugParticles = !debugParticles, debugPhysics = 0;
163
- if (keyWasPressed(51)) // 3
164
- godMode = !godMode;
165
- if (keyWasPressed(53)) // 5
166
- debugTakeScreenshot = 1;
167
- //if (keyWasPressed(54)) // 6
168
- // debugToggleParticleEditor();
169
- if (keyWasPressed(55)) // 7
170
- debugGamepads = !debugGamepads;
171
- //if (keyWasPressed(56)) // 8
172
- //if (keyWasPressed(57)) // 9
173
- if (keyWasPressed(48)) // 0
174
- showWatermark = !showWatermark;
166
+ if (debugOverlay)
167
+ {
168
+ if (keyWasPressed(48)) // 0
169
+ showWatermark = !showWatermark;
170
+ if (keyWasPressed(49)) // 1
171
+ debugPhysics = !debugPhysics, debugParticles = 0;
172
+ if (keyWasPressed(50)) // 2
173
+ debugParticles = !debugParticles, debugPhysics = 0;
174
+ if (keyWasPressed(51)) // 3
175
+ debugGamepads = !debugGamepads;
176
+ if (keyWasPressed(52)) // 4
177
+ godMode = !godMode;
178
+ if (keyWasPressed(53)) // 5
179
+ debugTakeScreenshot = 1;
180
+ //if (keyWasPressed(54)) // 6
181
+ //if (keyWasPressed(55)) // 7
182
+ //if (keyWasPressed(56)) // 8
183
+ //if (keyWasPressed(57)) // 9
184
+ }
175
185
  }
176
186
 
177
187
  const debugRender = ()=>
@@ -191,21 +201,20 @@ const debugRender = ()=>
191
201
 
192
202
  if (debugGamepads && gamepadsEnable && navigator.getGamepads)
193
203
  {
194
- // poll gamepads
204
+ // gamepad debug display
195
205
  const gamepads = navigator.getGamepads();
196
206
  for (let i = gamepads.length; i--;)
197
207
  {
198
208
  const gamepad = gamepads[i];
199
209
  if (gamepad)
200
210
  {
201
- // gamepad debug display
202
211
  const stickScale = 1;
203
212
  const buttonScale = .2;
204
213
  const centerPos = cameraPos;
205
214
  const sticks = stickData[i];
206
215
  for (let j = sticks.length; j--;)
207
216
  {
208
- const drawPos = centerPos.add(vec2(j*stickScale*2,i*stickScale*3));
217
+ const drawPos = centerPos.add(vec2(j*stickScale*2, i*stickScale*3));
209
218
  const stickPos = drawPos.add(sticks[j].scale(stickScale));
210
219
  debugCircle(drawPos, stickScale, '#fff7',0,1);
211
220
  debugLine(drawPos, stickPos, '#f00');
@@ -224,38 +233,36 @@ const debugRender = ()=>
224
233
 
225
234
  if (debugOverlay)
226
235
  {
236
+ // mouse pick
237
+ let bestDistance = Infinity, bestObject;
227
238
  for (const o of engineObjects)
228
239
  {
229
- if (o.canvas)
240
+ if (o.canvas || o.destroyed)
230
241
  continue; // skip tile layers
231
242
 
232
243
  const size = o.size.copy();
233
- size.x = max(size.x, .2);
234
- size.y = max(size.y, .2);
244
+ if (!size.x || !size.y)
245
+ continue;
246
+
247
+ const distance = mousePos.distanceSquared(o.pos);
248
+ if (distance < bestDistance)
249
+ {
250
+ bestDistance = distance;
251
+ bestObject = o
252
+ }
235
253
 
254
+ // show object info
236
255
  const color = new Color(
237
256
  o.collideTiles?1:0,
238
257
  o.collideSolidObjects?1:0,
239
258
  o.isSolid?1:0,
240
259
  o.parent ? .2 : .5);
241
-
242
- // show object info
260
+ size.x = max(size.x, .2);
261
+ size.y = max(size.y, .2);
243
262
  drawRect(o.pos, size, color);
244
263
  drawRect(o.pos, size.scale(.8), o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8));
245
264
  o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5));
246
265
  }
247
-
248
- // mouse pick
249
- let bestDistance = Infinity, bestObject;
250
- for (const o of engineObjects)
251
- {
252
- const distance = mousePos.distanceSquared(o.pos);
253
- if (distance < bestDistance)
254
- {
255
- bestDistance = distance;
256
- bestObject = o
257
- }
258
- }
259
266
 
260
267
  if (bestObject)
261
268
  {
@@ -275,7 +282,8 @@ const debugRender = ()=>
275
282
  pos = pos.add(height), ...args);
276
283
  drawText('vel = ' + printVec2(bestObject.velocity), pos = pos.add(height), ...args);
277
284
  drawText('size = ' + printVec2(bestObject.size), pos = pos.add(height), ...args);
278
- drawText('collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height), ...args);
285
+ drawText('type = ' + ( bestObject.constructor.name), pos = mousePos.subtract(height), ...args);
286
+ drawText('collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height.scale(2)), ...args);
279
287
  mainContext = saveContext;
280
288
  }
281
289
 
@@ -283,16 +291,15 @@ const debugRender = ()=>
283
291
  }
284
292
 
285
293
  {
286
- // render debug rects
287
- overlayContext.lineWidth = 1;
294
+ // draw debug primitives
295
+ overlayContext.save();
296
+ overlayContext.lineWidth = 2;
288
297
  const pointSize = debugPointSize * cameraScale;
289
298
  debugPrimitives.forEach(p=>
290
299
  {
291
300
  // create canvas transform from world space to screen space
292
301
  const pos = worldToScreen(p.pos);
293
302
 
294
- overlayContext.save();
295
- overlayContext.lineWidth = 2;
296
303
  overlayContext.translate(pos.x|0, pos.y|0);
297
304
  overlayContext.rotate(p.angle);
298
305
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
@@ -325,15 +332,17 @@ const debugRender = ()=>
325
332
  p.fill && overlayContext.fill();
326
333
  overlayContext.stroke();
327
334
  }
328
-
329
- overlayContext.restore();
330
335
  });
331
336
 
332
- overlayContext.fillStyle = overlayContext.strokeStyle = '#fff';
337
+ // remove expired pritives
338
+ debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
339
+
340
+ overlayContext.restore();
333
341
  }
334
342
 
335
343
  {
336
- let x = 9, y = -20, h = 30;
344
+ // draw debug overlay
345
+ overlayContext.save();
337
346
  overlayContext.fillStyle = '#fff';
338
347
  overlayContext.textAlign = 'left';
339
348
  overlayContext.textBaseline = 'top';
@@ -341,6 +350,7 @@ const debugRender = ()=>
341
350
  overlayContext.shadowColor = '#000';
342
351
  overlayContext.shadowBlur = 9;
343
352
 
353
+ let x = 9, y = -20, h = 30;
344
354
  if (debugOverlay)
345
355
  {
346
356
  overlayContext.fillText(engineName, x, y += h);
@@ -353,217 +363,41 @@ const debugRender = ()=>
353
363
  overlayContext.fillText('1: Debug Physics', x, y += h);
354
364
  overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
355
365
  overlayContext.fillText('2: Debug Particles', x, y += h);
366
+ overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
367
+ overlayContext.fillText('3: Debug Gamepads', x, y += h);
356
368
  overlayContext.fillStyle = godMode ? '#f00' : '#fff';
357
- overlayContext.fillText('3: God Mode', x, y += h);
369
+ overlayContext.fillText('4: God Mode', x, y += h);
358
370
  overlayContext.fillStyle = '#fff';
359
371
  overlayContext.fillText('5: Save Screenshot', x, y += h);
360
- //overlayContext.fillStyle = debugParticleEditor ? '#f00' : '#fff';
361
- //overlayContext.fillText('6: Particle Editor', x, y += h);
362
- overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
363
- overlayContext.fillText('7: Debug Gamepads', x, y += h);
364
- }
365
- else
366
- {
367
- overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
368
- overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
369
- overlayContext.fillText(godMode ? 'God Mode' : '', x, y += h);
370
- overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
371
- }
372
-
373
- overlayContext.shadowBlur = 0;
374
- }
375
372
 
376
- debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
377
- }
378
-
379
- ///////////////////////////////////////////////////////////////////////////////
380
- // particle system editor (work in progress)
381
- let debugParticleEditor = 0, debugParticleSystem, debugParticleSystemDiv, particleSystemCode;
382
-
383
- const debugToggleParticleEditor = ()=>
384
- {
385
- debugParticleEditor = !debugParticleEditor;
386
-
387
- if (debugParticleEditor)
388
- {
389
- if (!debugParticleSystem || debugParticleSystem.destroyed)
390
- debugParticleSystem = new ParticleEmitter(cameraPos);
391
- }
392
- else if (debugParticleSystem && !debugParticleSystem.destroyed)
393
- debugParticleSystem.destroy();
394
-
395
-
396
- const colorToHex = (color)=>
397
- {
398
- const componentToHex = (c)=>
399
- {
400
- const hex = (c*255|0).toString(16);
401
- return hex.length == 1 ? '0' + hex : hex;
402
- }
403
-
404
- return '#' + componentToHex(color.r) + componentToHex(color.g) + componentToHex(color.b);
405
- }
406
- const hexToColor = (hex)=>
407
- {
408
- return new Color(
409
- parseInt(hex.substr(1,2), 16)/255,
410
- parseInt(hex.substr(3,2), 16)/255,
411
- parseInt(hex.substr(5,2), 16)/255)
412
- }
413
-
414
- if (!debugParticleSystemDiv)
415
- {
416
- const div = debugParticleSystemDiv = document.createElement('div');
417
- div.innerHTML = '<big><b>Particle Editor';
418
- div.style = 'position:absolute;top:10;left:10;color:#fff';
419
- document.body.appendChild(div);
420
-
421
- for ( const setting of debugParticleSettings)
422
- {
423
- const input = setting[2] = document.createElement('input');
424
- const name = setting[0];
425
- const type = setting[1];
426
- if (type)
373
+ let keysPressed = '';
374
+ for(const i in inputData[0])
427
375
  {
428
- if (type == 'color')
429
- {
430
- input.type = type;
431
- const color = debugParticleSystem[name];
432
- input.value = colorToHex(color);
433
- }
434
- else if (type == 'alpha' && name == 'colorStartAlpha')
435
- input.value = debugParticleSystem.colorStartA.a;
436
- else if (type == 'alpha' && name == 'colorEndAlpha')
437
- input.value = debugParticleSystem.colorEndA.a;
438
- else if (name == 'tileSizeX')
439
- input.value = debugParticleSystem.tileSize.x;
440
- else if (name == 'tileSizeY')
441
- input.value = debugParticleSystem.tileSize.y;
376
+ if (i && keyIsDown(i, 0))
377
+ keysPressed += i + ' ' ;
442
378
  }
443
- else
444
- input.value = debugParticleSystem[name] || '0';
379
+ keysPressed && overlayContext.fillText('Keys Down: ' + keysPressed, x, y += h);
445
380
 
446
- input.oninput = (e)=>
381
+ let buttonsPressed = '';
382
+ if (inputData[1])
383
+ for(const i in inputData[1])
447
384
  {
448
- const inputFloat = parseFloat(input.value) || 0;
449
- if (type)
450
- {
451
- if (type == 'color')
452
- {
453
- const color = hexToColor(input.value);
454
- debugParticleSystem[name].r = color.r;
455
- debugParticleSystem[name].g = color.g;
456
- debugParticleSystem[name].b = color.b;
457
- }
458
- else if (type == 'alpha' && name == 'colorStartAlpha')
459
- {
460
- debugParticleSystem.colorStartA.a = clamp(inputFloat);
461
- debugParticleSystem.colorStartB.a = clamp(inputFloat);
462
- }
463
- else if (type == 'alpha' && name == 'colorEndAlpha')
464
- {
465
- debugParticleSystem.colorEndA.a = clamp(inputFloat);
466
- debugParticleSystem.colorEndB.a = clamp(inputFloat);
467
- }
468
- else if (name == 'tileSizeX')
469
- {
470
- debugParticleSystem.tileSize = vec2(parseInt(input.value), debugParticleSystem.tileSize.y);
471
- }
472
- else if (name == 'tileSizeY')
473
- {
474
- debugParticleSystem.tileSize.y = vec2(debugParticleSystem.tileSize.x, parseInt(input.value));
475
- }
476
- }
477
- else
478
- debugParticleSystem[name] = inputFloat;
479
-
480
- updateCode();
385
+ if (i && keyIsDown(i, 1))
386
+ buttonsPressed += i + ' ' ;
481
387
  }
482
- div.appendChild(document.createElement('br'));
483
- div.appendChild(input);
484
- div.appendChild(document.createTextNode(' ' + name));
388
+ buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
485
389
  }
486
-
487
- div.appendChild(document.createElement('br'));
488
- div.appendChild(document.createElement('br'));
489
- div.appendChild(particleSystemCode = document.createElement('input'));
490
- particleSystemCode.disabled = true;
491
- div.appendChild(document.createTextNode(' code'));
492
-
493
- div.appendChild(document.createElement('br'));
494
- const button = document.createElement('button')
495
- div.appendChild(button);
496
- button.innerHTML = 'Copy To Clipboard';
497
-
498
- button.onclick = (e)=> navigator.clipboard.writeText(particleSystemCode.value);
499
-
500
- const updateCode = ()=>
390
+ else
501
391
  {
502
- let code = '';
503
- let count = 0;
504
- for ( const setting of debugParticleSettings)
505
- {
506
- const name = setting[0];
507
- const type = setting[1];
508
- let value;
509
- if (name == 'tileSizeX' || type == 'alpha')
510
- continue;
511
-
512
- if (count++)
513
- code += ', ';
514
-
515
- if (name == 'tileSizeY')
516
- {
517
- value = `vec2(${debugParticleSystem.tileSize.x},${debugParticleSystem.tileSize.y})`;
518
- }
519
- else if (type == 'color')
520
- {
521
- const c = debugParticleSystem[name];
522
- value = `new Color(${c.r},${c.g},${c.b},${c.a})`;
523
- }
524
- else
525
- value = debugParticleSystem[name];
526
- code += value;
527
- }
528
-
529
- particleSystemCode.value = '...[' + code + ']';
392
+ overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
393
+ overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
394
+ overlayContext.fillText(godMode ? 'God Mode' : '', x, y += h);
395
+ overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
530
396
  }
531
- updateCode();
397
+
398
+ overlayContext.restore();
532
399
  }
533
- debugParticleSystemDiv.style.display = debugParticleEditor ? '' : 'none'
534
400
  }
535
-
536
- const debugParticleSettings =
537
- [
538
- ['emitSize'],
539
- ['emitTime'],
540
- ['emitRate'],
541
- ['emitConeAngle'],
542
- ['tileIndex'],
543
- ['tileSizeX', 'tileSize'],
544
- ['tileSizeY', 'tileSize'],
545
- ['colorStartA', 'color'],
546
- ['colorStartB', 'color'],
547
- ['colorStartAlpha', 'alpha'],
548
- ['colorEndA', 'color'],
549
- ['colorEndB', 'color'],
550
- ['colorEndAlpha', 'alpha'],
551
- ['particleTime'],
552
- ['sizeStart'],
553
- ['sizeEnd'],
554
- ['speed'],
555
- ['angleSpeed'],
556
- ['damping'],
557
- ['angleDamping'],
558
- ['gravityScale'],
559
- ['particleConeAngle'],
560
- ['fadeRate'],
561
- ['randomness'],
562
- ['collideTiles'],
563
- ['additive'],
564
- ['randomColorComponents'],
565
- ['renderOrder'],
566
- ];
567
401
  /**
568
402
  * LittleJS Utility Classes and Functions
569
403
  * <br> - General purpose math library
@@ -615,27 +449,27 @@ const mod = (a, b=1)=> ((a % b) + b) % b;
615
449
 
616
450
  /** Clamps the value beween max and min
617
451
  * @param {Number} value
618
- * @param {Number} [max=1]
619
452
  * @param {Number} [min=0]
453
+ * @param {Number} [max=1]
620
454
  * @return {Number}
621
455
  * @memberof Utilities */
622
- const clamp = (v, max=1, min=0)=> (ASSERT(max > min), v < min ? min : v > max ? max : v);
456
+ const clamp = (v, min=0, max=1)=> v < min ? min : v > max ? max : v;
623
457
 
624
458
  /** Returns what percentage the value is between max and min
625
459
  * @param {Number} value
626
- * @param {Number} [max=1]
627
460
  * @param {Number} [min=0]
461
+ * @param {Number} [max=1]
628
462
  * @return {Number}
629
463
  * @memberof Utilities */
630
- const percent = (v, max=1, min=0)=> max-min ? clamp((v-min) / (max-min)) : 0;
464
+ const percent = (v, min=0, max=1)=> max-min ? clamp((v-min) / (max-min)) : 0;
631
465
 
632
466
  /** Linearly interpolates the percent value between max and min
633
467
  * @param {Number} percent
634
- * @param {Number} [max=1]
635
468
  * @param {Number} [min=0]
469
+ * @param {Number} [max=1]
636
470
  * @return {Number}
637
471
  * @memberof Utilities */
638
- const lerp = (p, max=1, min=0)=> min + clamp(p) * (max-min);
472
+ const lerp = (p, min=0, max=1)=> min + clamp(p) * (max-min);
639
473
 
640
474
  /** Applies smoothstep function to the percentage value
641
475
  * @param {Number} value
@@ -650,11 +484,11 @@ const smoothStep = (p)=> p * p * (3 - 2 * p);
650
484
  const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
651
485
 
652
486
  /** Returns true if two axis aligned bounding boxes are overlapping
653
- * @param {Vector2} pointA - Center of box A
654
- * @param {Vector2} sizeA - Size of box A
655
- * @param {Vector2} pointB - Center of box B
656
- * @param {Vector2} sizeB - Size of box B
657
- * @return {Boolean} - True if overlapping
487
+ * @param {Vector2} pointA - Center of box A
488
+ * @param {Vector2} sizeA - Size of box A
489
+ * @param {Vector2} pointB - Center of box B
490
+ * @param {Vector2} [sizeB] - Size of box B
491
+ * @return {Boolean} - True if overlapping
658
492
  * @memberof Utilities */
659
493
  const isOverlapping = (pA, sA, pB, sB)=> abs(pA.x - pB.x)*2 < sA.x + sB.x & abs(pA.y - pB.y)*2 < sA.y + sB.y;
660
494
 
@@ -751,10 +585,12 @@ const vec2 = (x=0, y)=> x.x == undefined ? new Vector2(x, y == undefined? x : y)
751
585
 
752
586
  /**
753
587
  * 2D Vector object with vector math library
588
+ * <br> - Functions do not change this so they can be chained together
754
589
  * @example
755
590
  * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
756
591
  * let b = new Vector2; // vector with coordinates (0, 0)
757
592
  * let c = vec2(4, 2); // use the vec2 function to make a Vector2
593
+ * let d = a.add(b).scale(5); // operators can be chained
758
594
  */
759
595
  class Vector2
760
596
  {
@@ -1041,7 +877,7 @@ class Timer
1041
877
 
1042
878
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1043
879
  * @return {Number} */
1044
- getPercent() { return this.isSet()? percent(this.time - time, 0, this.setTime) : 0; }
880
+ getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
1045
881
  }
1046
882
  /**
1047
883
  * LittleJS Engine Settings
@@ -1087,7 +923,7 @@ let tileSizeDefault = vec2(16);
1087
923
  /** Prevent tile bleeding from neighbors in pixels
1088
924
  * @default
1089
925
  * @memberof Settings */
1090
- let tileBleedShrinkFix = .3;
926
+ let tileFixBleedScale = .3;
1091
927
 
1092
928
  ///////////////////////////////////////////////////////////////////////////////
1093
929
  // Object settings
@@ -1133,6 +969,11 @@ let objectMaxSpeed = 1;
1133
969
  * @memberof Settings */
1134
970
  let gravity = 0;
1135
971
 
972
+ /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
973
+ * @default
974
+ * @memberof Settings */
975
+ let particleEmitRateScale = 1;
976
+
1136
977
  ///////////////////////////////////////////////////////////////////////////////
1137
978
  // Camera settings
1138
979
 
@@ -1178,11 +1019,6 @@ let gamepadDirectionEmulateStick = 1;
1178
1019
  * @memberof Settings */
1179
1020
  let inputWASDEmulateDirection = 1;
1180
1021
 
1181
- /** If true touch input is routed to mouse functions
1182
- * @default
1183
- * @memberof Settings */
1184
- let touchMouseEnable = 1;
1185
-
1186
1022
  /** True if touch gamepad should appear on mobile devices
1187
1023
  * <br> - Supports left analog stick, 4 face buttons and start button (button 9)
1188
1024
  * <br> - Must be set by end of gameInit to be activated
@@ -1190,6 +1026,11 @@ let touchMouseEnable = 1;
1190
1026
  * @memberof Settings */
1191
1027
  let touchGamepadEnable = 0;
1192
1028
 
1029
+ /** True if touch gamepad should be analog stick or false to use if 8 way dpad
1030
+ * @default
1031
+ * @memberof Settings */
1032
+ let touchGamepadAnalog = 1;
1033
+
1193
1034
  /** Size of virutal gamepad for touch devices in pixels
1194
1035
  * @default
1195
1036
  * @memberof Settings */
@@ -1198,7 +1039,12 @@ let touchGamepadSize = 80;
1198
1039
  /** Transparency of touch gamepad overlay
1199
1040
  * @default
1200
1041
  * @memberof Settings */
1201
- let touchGamepadAlpha = .5;
1042
+ let touchGamepadAlpha = .3;
1043
+
1044
+ /** Allow vibration hardware if it exists
1045
+ * @default
1046
+ * @memberof Settings */
1047
+ let vibrateEnable = 1;
1202
1048
 
1203
1049
  ///////////////////////////////////////////////////////////////////////////////
1204
1050
  // Audio settings
@@ -1211,7 +1057,7 @@ let soundVolume = .5;
1211
1057
  /** All audio code can be disabled and removed from build
1212
1058
  * @default
1213
1059
  * @memberof Settings */
1214
- let soundEnable = 0;
1060
+ let soundEnable = 1;
1215
1061
 
1216
1062
  /** Default range where sound no longer plays
1217
1063
  * @default
@@ -1244,12 +1090,12 @@ let medalDisplayWidth = 640;
1244
1090
  /** Height of medal display
1245
1091
  * @default
1246
1092
  * @memberof Settings */
1247
- let medalDisplayHeight = 99;
1093
+ let medalDisplayHeight = 80;
1248
1094
 
1249
1095
  /** Size of icon in medal display
1250
1096
  * @default
1251
1097
  * @memberof Settings */
1252
- let medalDisplayIconSize = 80;
1098
+ let medalDisplayIconSize = 50;
1253
1099
  /*
1254
1100
  LittleJS Object System
1255
1101
  */
@@ -1284,7 +1130,7 @@ let medalDisplayIconSize = 80;
1284
1130
  class EngineObject
1285
1131
  {
1286
1132
  /** Create an engine object and adds it to the list of objects
1287
- * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1133
+ * @param {Vector2} [position=new Vector2()] - World space position of the object
1288
1134
  * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1289
1135
  * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1290
1136
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
@@ -1329,12 +1175,15 @@ class EngineObject
1329
1175
  this.gravityScale = 1;
1330
1176
  /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1331
1177
  this.renderOrder = renderOrder;
1178
+ /** @property {Vector2} [velocity=new Vector2()] - Velocity of the object */
1179
+ this.velocity = new Vector2();
1180
+ /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1181
+ this.angleVelocity = 0;
1332
1182
 
1333
1183
  // init other internal object stuff
1334
1184
  this.spawnTime = time;
1335
- this.velocity = vec2(this.collideSolidObjects = this.angleVelocity = 0);
1336
- this.collideTiles = 1;
1337
1185
  this.children = [];
1186
+ this.collideTiles = 1;
1338
1187
 
1339
1188
  // add to list of objects
1340
1189
  engineObjects.push(this);
@@ -1353,8 +1202,8 @@ class EngineObject
1353
1202
  }
1354
1203
 
1355
1204
  // limit max speed to prevent missing collisions
1356
- this.velocity.x = clamp(this.velocity.x, objectMaxSpeed, -objectMaxSpeed);
1357
- this.velocity.y = clamp(this.velocity.y, objectMaxSpeed, -objectMaxSpeed);
1205
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1206
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1358
1207
 
1359
1208
  // apply physics
1360
1209
  const oldPos = this.pos.copy();
@@ -1402,7 +1251,7 @@ class EngineObject
1402
1251
  // if already was touching, try to push away
1403
1252
  const deltaPos = oldPos.subtract(o.pos);
1404
1253
  const length = deltaPos.length();
1405
- const pushAwayAccel = .001; // push away if alread overlapping
1254
+ const pushAwayAccel = .001; // push away if already overlapping
1406
1255
  const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
1407
1256
  this.velocity = this.velocity.add(velocity);
1408
1257
  if (o.mass) // push away if not fixed
@@ -1444,8 +1293,8 @@ class EngineObject
1444
1293
 
1445
1294
  // lerp betwen elastic or inelastic based on elasticity
1446
1295
  const elasticity = max(this.elasticity, o.elasticity);
1447
- this.velocity.y = lerp(elasticity, elastic0, inelastic);
1448
- o.velocity.y = lerp(elasticity, elastic1, inelastic);
1296
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1297
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1449
1298
  }
1450
1299
  }
1451
1300
  if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
@@ -1465,8 +1314,8 @@ class EngineObject
1465
1314
 
1466
1315
  // lerp betwen elastic or inelastic based on elasticity
1467
1316
  const elasticity = max(this.elasticity, o.elasticity);
1468
- this.velocity.x = lerp(elasticity, elastic0, inelastic);
1469
- o.velocity.x = lerp(elasticity, elastic1, inelastic);
1317
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1318
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1470
1319
  }
1471
1320
  else // bounce if other object is fixed
1472
1321
  this.velocity.x *= -this.elasticity;
@@ -1496,7 +1345,7 @@ class EngineObject
1496
1345
 
1497
1346
  // adjust next velocity to settle on ground
1498
1347
  const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1499
- if (o < 0 && o > -1 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1348
+ if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1500
1349
  this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1501
1350
 
1502
1351
  // move to previous position
@@ -1597,18 +1446,6 @@ class EngineObject
1597
1446
  {
1598
1447
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1599
1448
 
1600
- // track collidable objects in separate list
1601
- if (collideSolidObjects && !this.collideSolidObjects)
1602
- {
1603
- ASSERT(!engineObjectsCollide.includes(this));
1604
- engineObjectsCollide.push(this);
1605
- }
1606
- else if (!collideSolidObjects && this.collideSolidObjects)
1607
- {
1608
- ASSERT(engineObjectsCollide.includes(this))
1609
- engineObjectsCollide.splice(engineObjectsCollide.indexOf(this), 1);
1610
- }
1611
-
1612
1449
  this.collideSolidObjects = collideSolidObjects;
1613
1450
  this.isSolid = isSolid;
1614
1451
  this.collideTiles = collideTiles;
@@ -1708,17 +1545,13 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1708
1545
  {
1709
1546
  // calculate uvs and render
1710
1547
  const cols = tileImageSize.x / tileSize.x |0;
1711
- const uvSizeX = tileSize.x * tileImageSizeInverse.x;
1712
- const uvSizeY = tileSize.y * tileImageSizeInverse.y;
1548
+ const uvSizeX = tileSize.x / tileImageSize.x;
1549
+ const uvSizeY = tileSize.y / tileImageSize.y;
1713
1550
  const uvX = (tileIndex%cols)*uvSizeX, uvY = (tileIndex/cols|0)*uvSizeY;
1714
-
1715
- // shrink uvs to prevent bleeding
1716
- const shrinkTilesX = tileBleedShrinkFix * tileImageSizeInverse.x;
1717
- const shrinkTilesY = tileBleedShrinkFix * tileImageSizeInverse.y;
1718
1551
 
1719
1552
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
1720
- uvX + shrinkTilesX, uvY + shrinkTilesY,
1721
- uvX - shrinkTilesX + uvSizeX, uvY - shrinkTilesX + uvSizeY,
1553
+ uvX + tileImageFixBleed.x, uvY + tileImageFixBleed.y,
1554
+ uvX - tileImageFixBleed.x + uvSizeX, uvY - tileImageFixBleed.y + uvSizeY,
1722
1555
  color.rgbaInt(), additiveColor.rgbaInt());
1723
1556
  }
1724
1557
  }
@@ -1737,10 +1570,10 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1737
1570
  {
1738
1571
  // calculate uvs and render
1739
1572
  const cols = tileImageSize.x / tileSize.x |0;
1740
- const sX = (tileIndex%cols)*tileSize.x + tileBleedShrinkFix;
1741
- const sY = (tileIndex/cols|0)*tileSize.y + tileBleedShrinkFix;
1742
- const sWidth = tileSize.x - 2*tileBleedShrinkFix;
1743
- const sHeight = tileSize.y - 2*tileBleedShrinkFix;
1573
+ const sX = (tileIndex%cols)*tileSize.x + tileFixBleedScale;
1574
+ const sY = (tileIndex/cols|0)*tileSize.y + tileFixBleedScale;
1575
+ const sWidth = tileSize.x - 2*tileFixBleedScale;
1576
+ const sHeight = tileSize.y - 2*tileFixBleedScale;
1744
1577
  context.globalAlpha = color.a; // only alpha is supported
1745
1578
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1746
1579
  }
@@ -1823,6 +1656,18 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1823
1656
  context.restore();
1824
1657
  }
1825
1658
 
1659
+ /** Enable normal or additive blend mode
1660
+ * @param {Boolean} [additive=0]
1661
+ * @param {Boolean} [useWebGL=glEnable]
1662
+ * @memberof Draw */
1663
+ function setBlendMode(additive, useWebGL=glEnable)
1664
+ {
1665
+ if (glEnable && useWebGL)
1666
+ glSetBlendMode(additive);
1667
+ else
1668
+ mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1669
+ }
1670
+
1826
1671
  /** Draw text on overlay canvas in world space
1827
1672
  * @param {String} text
1828
1673
  * @param {Vector2} pos
@@ -1848,16 +1693,96 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
1848
1693
  overlayContext.fillText(text, pos.x, pos.y);
1849
1694
  }
1850
1695
 
1851
- /** Enable normal or additive blend mode
1852
- * @param {Boolean} [additive=0]
1853
- * @param {Boolean} [useWebGL=glEnable]
1854
- * @memberof Draw */
1855
- function setBlendMode(additive, useWebGL=glEnable)
1696
+ ///////////////////////////////////////////////////////////////////////////////
1697
+
1698
+ /**
1699
+ * Font Image Object - Draw text on a 2D canvas by using characters in an image
1700
+ * <br> - 96 characters (from space to tilde) are stored in an image
1701
+ * <br> - Uses a default 8x8 font if none is supplied
1702
+ * <br> - You can also use fonts from the main tile sheet
1703
+ * @example
1704
+ * // use built in font
1705
+ * const font = new ImageFont;
1706
+ *
1707
+ * // draw text
1708
+ * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1709
+ */
1710
+
1711
+ let engineFontImage;
1712
+
1713
+ class FontImage
1856
1714
  {
1857
- if (glEnable && useWebGL)
1858
- glSetBlendMode(additive);
1859
- else
1860
- mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1715
+ /** Create an image font
1716
+ * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
1717
+ * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1718
+ * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1719
+ * @param {Number} [startTileIndex=0] - Tile index in image where font starts
1720
+ * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
1721
+ */
1722
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1723
+ {
1724
+ if (!image && !engineFontImage)
1725
+ {
1726
+ // load default font image
1727
+ engineFontImage = new Image();
1728
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
1729
+ }
1730
+
1731
+ this.image = image || engineFontImage;
1732
+ this.tileSize = tileSize;
1733
+ this.paddingSize = paddingSize;
1734
+ this.startTileIndex = startTileIndex;
1735
+ this.context = context;
1736
+ }
1737
+
1738
+ /** Draw text in screen space using the image font
1739
+ * @param {String} text
1740
+ * @param {Vector2} pos
1741
+ * @param {Number} [scale=4]
1742
+ * @param {Boolean} [center]
1743
+ */
1744
+ drawTextScreen(text, pos, scale=4, center)
1745
+ {
1746
+ const context = this.context;
1747
+ context.save();
1748
+ context.imageSmoothingEnabled = !cavasPixelated;
1749
+
1750
+ const size = this.tileSize;
1751
+ const drawSize = size.add(this.paddingSize).scale(scale);
1752
+ const cols = this.image.width / this.tileSize.x |0;
1753
+ text.split('\n').forEach((line, i)=>
1754
+ {
1755
+ const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
1756
+ for(let j=line.length; j--;)
1757
+ {
1758
+ // draw each character
1759
+ let charCode = line[j].charCodeAt();
1760
+ if (charCode < 32 || charCode > 127)
1761
+ charCode = 127; // unknown character
1762
+
1763
+ // get the character source location and draw it
1764
+ const tile = this.startTileIndex + charCode - 32;
1765
+ const x = tile % cols;
1766
+ const y = tile / cols |0;
1767
+ const drawPos = pos.add(vec2(j,i).multiply(drawSize));
1768
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
1769
+ drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
1770
+ }
1771
+ });
1772
+
1773
+ context.restore();
1774
+ }
1775
+
1776
+ /** Draw text in world space using the image font
1777
+ * @param {String} text
1778
+ * @param {Vector2} pos
1779
+ * @param {Number} [scale=.25]
1780
+ * @param {Boolean} [center]
1781
+ */
1782
+ drawText(text, pos, scale=1, center)
1783
+ {
1784
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1785
+ }
1861
1786
  }
1862
1787
 
1863
1788
  ///////////////////////////////////////////////////////////////////////////////
@@ -1921,7 +1846,7 @@ const keyWasReleased = (key, device=0)=> inputData[device] && inputData[device][
1921
1846
 
1922
1847
  /** Clears all input
1923
1848
  * @memberof Input */
1924
- const clearInput = ()=> inputData[0] = [];
1849
+ const clearInput = ()=> inputData = [[]];
1925
1850
 
1926
1851
  /** Returns true if mouse button is down
1927
1852
  * @param {Number} button
@@ -1990,7 +1915,9 @@ const gamepadStick = (stick, gamepad=0)=> stickData[gamepad] ? stickData[gamepa
1990
1915
  ///////////////////////////////////////////////////////////////////////////////
1991
1916
  // Input update called by engine
1992
1917
 
1993
- const inputData = [[]];
1918
+ // store input as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
1919
+ // mouse and keyboard are stored together in device 0, gamepads are in devices > 0
1920
+ let inputData = [[]];
1994
1921
 
1995
1922
  function inputUpdate()
1996
1923
  {
@@ -2016,36 +1943,38 @@ function inputUpdatePost()
2016
1943
  ///////////////////////////////////////////////////////////////////////////////
2017
1944
  // Keyboard event handlers
2018
1945
 
2019
- onkeydown = e=>
1946
+ onkeydown = (e)=>
2020
1947
  {
2021
1948
  if (debug && e.target != document.body) return;
2022
1949
  e.repeat || (inputData[isUsingGamepad = 0][remapKeyCode(e.keyCode)] = 3);
2023
1950
  debug || e.preventDefault();
2024
1951
  }
2025
- onkeyup = e=>
1952
+ onkeyup = (e)=>
2026
1953
  {
2027
1954
  if (debug && e.target != document.body) return;
2028
1955
  inputData[0][remapKeyCode(e.keyCode)] = 4;
2029
1956
  }
2030
- const remapKeyCode = c=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
1957
+ const remapKeyCode = (c)=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
2031
1958
 
2032
1959
  ///////////////////////////////////////////////////////////////////////////////
2033
1960
  // Mouse event handlers
2034
1961
 
2035
- onmousedown = e=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
2036
- onmouseup = e=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2037
- onmousemove = e=>
1962
+ onmousedown = (e)=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
1963
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
1964
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
1965
+ onwheel = (e)=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
1966
+ oncontextmenu = (e)=> !1; // prevent right click menu
1967
+
1968
+ // convert a mouse or touch event position to screen space
1969
+ const mouseToScreen = (mousePos)=>
2038
1970
  {
2039
1971
  if (!mainCanvas)
2040
- return; // fix bug that can occur if user clicks before page loads
1972
+ return vec2(); // fix bug that can occur if user clicks before page loads
2041
1973
 
2042
- // convert mouse pos to canvas space
2043
1974
  const rect = mainCanvas.getBoundingClientRect();
2044
- mousePosScreen = mainCanvasSize.multiply(
2045
- vec2(percent(e.x, rect.right, rect.left), percent(e.y, rect.bottom, rect.top)));
1975
+ return mainCanvasSize.multiply(
1976
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
2046
1977
  }
2047
- onwheel = e=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
2048
- oncontextmenu = e=> !1; // prevent right click menu
2049
1978
 
2050
1979
  ///////////////////////////////////////////////////////////////////////////////
2051
1980
  // Gamepad input
@@ -2053,6 +1982,21 @@ oncontextmenu = e=> !1; // prevent right click menu
2053
1982
  const stickData = [];
2054
1983
  function gamepadsUpdate()
2055
1984
  {
1985
+ if (touchGamepadEnable && touchGamepadTimer.isSet())
1986
+ {
1987
+ // read virtual analog stick
1988
+ const sticks = stickData[0] || (stickData[0] = []);
1989
+ sticks[0] = vec2(touchGamepadStick.x, -touchGamepadStick.y); // flip vertical
1990
+
1991
+ // read virtual gamepad buttons
1992
+ const data = inputData[1] || (inputData[1] = []);
1993
+ for (let i=10; i--;)
1994
+ {
1995
+ const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
1996
+ data[j] = touchGamepadButtons[i] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
1997
+ }
1998
+ }
1999
+
2056
2000
  if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
2057
2001
  return;
2058
2002
 
@@ -2070,8 +2014,8 @@ function gamepadsUpdate()
2070
2014
  // read clamp dead zone of analog sticks
2071
2015
  const deadZone = .3, deadZoneMax = .8;
2072
2016
  const applyDeadZone = (v)=>
2073
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
2074
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
2017
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
2018
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
2075
2019
 
2076
2020
  // read analog sticks
2077
2021
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -2083,6 +2027,7 @@ function gamepadsUpdate()
2083
2027
  const button = gamepad.buttons[j];
2084
2028
  data[j] = button.pressed ? 1 + 2*!gamepadIsDown(j,i) : 4*gamepadIsDown(j,i);
2085
2029
  isUsingGamepad |= !i && button.pressed;
2030
+ touchGamepadEnable && touchGamepadTimer.unset(); // disable touch gamepad if using real gamepad
2086
2031
  }
2087
2032
 
2088
2033
  if (gamepadDirectionEmulateStick)
@@ -2096,6 +2041,17 @@ function gamepadsUpdate()
2096
2041
  }
2097
2042
  }
2098
2043
 
2044
+ ///////////////////////////////////////////////////////////////////////////////
2045
+
2046
+ /** Pulse the vibration hardware if it exists
2047
+ * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
2048
+ * @memberof Input */
2049
+ const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
2050
+
2051
+ /** Cancel any ongoing vibration
2052
+ * @memberof Input */
2053
+ const vibrateStop = ()=> vibrate(0);
2054
+
2099
2055
  ///////////////////////////////////////////////////////////////////////////////
2100
2056
  // Touch input
2101
2057
 
@@ -2103,7 +2059,9 @@ function gamepadsUpdate()
2103
2059
  * @const {boolean}
2104
2060
  * @memberof Input */
2105
2061
  const isTouchDevice = window.ontouchstart !== undefined;
2106
- if (touchMouseEnable && isTouchDevice)
2062
+
2063
+ // try to enable touch mouse
2064
+ if (isTouchDevice)
2107
2065
  {
2108
2066
  // handle all touch events the same way
2109
2067
  let wasTouching, hadTouchInput;
@@ -2134,10 +2092,12 @@ if (touchMouseEnable && isTouchDevice)
2134
2092
  }
2135
2093
 
2136
2094
  ///////////////////////////////////////////////////////////////////////////////
2137
- // Touch gamepad - on screen virtual gamepad
2095
+ // touch gamepad, virtual on screen gamepad emulator for touch devices
2096
+
2097
+ // touch input internal variables
2098
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
2138
2099
 
2139
2100
  // create the touch gamepad, called automatically by the engine
2140
- const touchGamepadTimer = new Timer;
2141
2101
  function touchGamepadCreate()
2142
2102
  {
2143
2103
  if (!touchGamepadEnable || !isTouchDevice)
@@ -2147,6 +2107,10 @@ function touchGamepadCreate()
2147
2107
  {
2148
2108
  if (!touchGamepadEnable)
2149
2109
  return;
2110
+
2111
+ // clear touch gamepad input
2112
+ touchGamepadStick = vec2();
2113
+ touchGamepadButtons = [];
2150
2114
 
2151
2115
  const touching = e.touches.length;
2152
2116
  if (touching)
@@ -2156,6 +2120,13 @@ function touchGamepadCreate()
2156
2120
  // set that gamepad is active
2157
2121
  isUsingGamepad = 1;
2158
2122
  touchGamepadTimer.set();
2123
+
2124
+ if (paused)
2125
+ {
2126
+ // touch anywhere to press start when paused
2127
+ touchGamepadButtons[9] = 1;
2128
+ return;
2129
+ }
2159
2130
  }
2160
2131
 
2161
2132
  // get center of left and right sides
@@ -2163,38 +2134,34 @@ function touchGamepadCreate()
2163
2134
  const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
2164
2135
  const startCenter = mainCanvasSize.scale(.5);
2165
2136
 
2166
- // update virtual gamepad
2167
- const data = inputData[1] || (inputData[1] = []);
2168
- const sticks = stickData[0] || (stickData[0] = []);
2169
- sticks[0] = vec2();
2170
-
2171
2137
  // check each touch point
2172
- const buttons = [];
2173
2138
  for (const touch of e.touches)
2174
2139
  {
2175
- const touchPos = vec2(touch.clientX, touch.clientY);
2140
+ const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
2176
2141
  if (touchPos.distance(stickCenter) < touchGamepadSize)
2177
2142
  {
2178
2143
  // virtual analog stick
2179
- sticks[0] = touchPos.subtract(stickCenter).clampLength();
2180
- sticks[0].y = -sticks[0].y; // flip vertical
2144
+ if (touchGamepadAnalog)
2145
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
2146
+ else
2147
+ {
2148
+ // 8 way dpad
2149
+ const angle = touchPos.subtract(stickCenter).angle();
2150
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2151
+ }
2181
2152
  }
2182
2153
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2183
2154
  {
2184
2155
  // virtual face buttons
2185
2156
  const button = touchPos.subtract(buttonCenter).direction();
2186
- buttons[button > 2 ? 2 : button > 1 ? 3 : button] = 1; // fix button locations
2157
+ touchGamepadButtons[button] = 1;
2187
2158
  }
2188
2159
  else if (touchPos.distance(startCenter) < touchGamepadSize)
2189
2160
  {
2190
- // virtual start button
2191
- buttons[9] = 1;
2161
+ // virtual start button in center
2162
+ touchGamepadButtons[9] = 1;
2192
2163
  }
2193
2164
  }
2194
-
2195
- // read virtual buttons
2196
- for (let j=10; j--;)
2197
- data[j] = buttons[j] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
2198
2165
  }
2199
2166
  }
2200
2167
 
@@ -2204,37 +2171,48 @@ function touchGamepadRender()
2204
2171
  if (!touchGamepadEnable || !touchGamepadTimer.isSet())
2205
2172
  return;
2206
2173
 
2207
- // fade off when not touching
2208
- const alpha = percent(touchGamepadTimer.get(), 4, 5);
2209
- if (!alpha)
2174
+ // fade off when not touching or paused
2175
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
2176
+ if (!alpha || paused)
2210
2177
  return;
2211
2178
 
2212
2179
  // setup the canvas
2213
2180
  overlayContext.save();
2214
2181
  overlayContext.globalAlpha = alpha*touchGamepadAlpha;
2215
- overlayContext.fillStyle = '#fff6';
2216
2182
  overlayContext.strokeStyle = '#fff';
2217
2183
  overlayContext.lineWidth = 3;
2218
2184
 
2219
- // draw start button
2220
- //overlayContext.beginPath();
2221
- //overlayContext.arc(mainCanvasSize.x/2, mainCanvasSize.y/2, touchGamepadSize*.9, 0,9);
2222
- //overlayContext.fill();
2223
- //overlayContext.stroke();
2224
-
2225
2185
  // draw left analog stick
2186
+ overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2226
2187
  overlayContext.beginPath();
2227
- overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize*.9, 0,9);
2228
- overlayContext.fill();
2229
- overlayContext.stroke();
2230
2188
 
2189
+ const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2190
+ if (touchGamepadAnalog)
2191
+ {
2192
+ overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2193
+ overlayContext.fill();
2194
+ overlayContext.stroke();
2195
+ }
2196
+ else // draw cross shaped gamepad
2197
+ {
2198
+ for(let i=10; i--;)
2199
+ {
2200
+ const angle = i*PI/4;
2201
+ overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2202
+ i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2203
+ i==1 && overlayContext.fill();
2204
+ }
2205
+ overlayContext.stroke();
2206
+ }
2207
+
2231
2208
  // draw right face buttons
2232
2209
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2233
2210
  for (let i=4; i--;)
2234
2211
  {
2235
- const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize*.6));
2212
+ const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize/2));
2213
+ overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2236
2214
  overlayContext.beginPath();
2237
- overlayContext.arc(pos.x, pos.y, touchGamepadSize*.3, 0,9);
2215
+ overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2238
2216
  overlayContext.fill();
2239
2217
  overlayContext.stroke();
2240
2218
  }
@@ -2313,7 +2291,7 @@ class Sound
2313
2291
  return; // out of range
2314
2292
 
2315
2293
  // attenuate volume by distance
2316
- volume *= percent(lengthSquared**.5, range*this.taper, range);
2294
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
2317
2295
  }
2318
2296
 
2319
2297
  // get pan from screen space coords
@@ -2439,7 +2417,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
2439
2417
 
2440
2418
  /** Stop all queued speech
2441
2419
  * @memberof Audio */
2442
- const stopSpeech = ()=> speechSynthesis && speechSynthesis.cancel();
2420
+ const speakStop = ()=> speechSynthesis && speechSynthesis.cancel();
2443
2421
 
2444
2422
  /** Get frequency of a note on a musical scale
2445
2423
  * @param {Number} semitoneOffset - How many semitones away from the root note
@@ -2470,6 +2448,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2470
2448
  if (!audioContext)
2471
2449
  audioContext = new (window.AudioContext||webkitAudioContext);
2472
2450
 
2451
+ // fix stalled audio
2452
+ audioContext.resume();
2453
+
2454
+ // prevent sounds from building up if they can't be played
2455
+ if (audioContext.state != 'running')
2456
+ return;
2457
+
2473
2458
  // create buffer and source
2474
2459
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
2475
2460
  source = audioContext.createBufferSource();
@@ -2480,11 +2465,18 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2480
2465
  source.playbackRate.value = rate;
2481
2466
  source.loop = loop;
2482
2467
 
2483
- // create pan and gain nodes
2484
- source
2485
- .connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
2486
- .connect(new GainNode(audioContext, {'gain':soundVolume*volume}))
2487
- .connect(audioContext.destination);
2468
+ // create and connect gain node (createGain is more widley spported then GainNode construtor)
2469
+ const gainNode = audioContext.createGain();
2470
+ gainNode.gain.value = soundVolume*volume;
2471
+ gainNode.connect(audioContext.destination);
2472
+
2473
+ // connect source to gain
2474
+ (
2475
+ window.StereoPannerNode ? // create pan node if possible
2476
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2477
+ : source
2478
+ )
2479
+ .connect(gainNode);
2488
2480
 
2489
2481
  // play and return sound
2490
2482
  source.start();
@@ -2842,8 +2834,9 @@ class TileLayerData
2842
2834
  /**
2843
2835
  * Tile layer object - cached rendering system for tile layers
2844
2836
  * <br> - Each Tile layer is rendered to an off screen canvas
2845
- * <br> - Tile layers are not rendered using WebGL to allow modifications at run time
2846
- * <br> - Tile layers are sorted
2837
+ * <br> - To allow dynamic modifications, layers are rendered using canvas 2d
2838
+ * <br> - Some devices like mobile phones are limited to 4k texture resolution
2839
+ * <br> - So with 16x16 tiles this limits layers to 256x256 on mobile devices
2847
2840
  * @extends EngineObject
2848
2841
  * @example
2849
2842
  * // create tile collision and visible tile layer
@@ -2853,11 +2846,11 @@ class TileLayerData
2853
2846
  class TileLayer extends EngineObject
2854
2847
  {
2855
2848
  /** Create a tile layer object
2856
- * @param {Vector2} [position=new Vector2(0,0)] - World space position
2857
- * @param {Vector2} [size=objectDefaultSize] - World space size
2858
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2859
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2860
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2849
+ * @param {Vector2} [position=new Vector2()] - World space position
2850
+ * @param {Vector2} [size=objectDefaultSize] - World space size
2851
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2852
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2853
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2861
2854
  */
2862
2855
  constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2863
2856
  {
@@ -2908,7 +2901,7 @@ class TileLayer extends EngineObject
2908
2901
  // flush and copy gl canvas because tile canvas does not use webgl
2909
2902
  glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
2910
2903
 
2911
- // draw the entire cached level onto the main canvas
2904
+ // draw the entire cached level onto the canvas
2912
2905
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
2913
2906
  (this.isOverlay ? overlayContext : mainContext).drawImage
2914
2907
  (
@@ -2917,25 +2910,25 @@ class TileLayer extends EngineObject
2917
2910
  );
2918
2911
  }
2919
2912
 
2920
- /** Draw all the tile data to an offscreen canvas using webgl if possible */
2913
+ /** Draw all the tile data to an offscreen canvas
2914
+ * - This may be slow in some browsers
2915
+ */
2921
2916
  redraw()
2922
2917
  {
2923
- this.redrawStart();
2918
+ this.redrawStart(1);
2924
2919
  this.drawAllTileData();
2925
2920
  this.redrawEnd();
2926
2921
  }
2927
2922
 
2928
2923
  /** Call to start the redraw process
2929
- * @param {Boolean} [clear=1] - Should it clear the canvas before drawing */
2930
- redrawStart(clear = 1)
2924
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2925
+ redrawStart(clear = 0)
2931
2926
  {
2932
- // clear and set size
2933
- const width = this.size.x * this.tileSize.x;
2934
- const height = this.size.y * this.tileSize.y;
2935
2927
  if (clear)
2936
2928
  {
2937
- this.canvas.width = width;
2938
- this.canvas.height = height;
2929
+ // clear and set size
2930
+ this.canvas.width = this.size.x * this.tileSize.x;
2931
+ this.canvas.height = this.size.y * this.tileSize.y;
2939
2932
  }
2940
2933
 
2941
2934
  // save current render settings
@@ -3025,7 +3018,7 @@ class TileLayer extends EngineObject
3025
3018
  else
3026
3019
  {
3027
3020
  const cols = tileImage.width/tileSize.x;
3028
- context.globalAlpha = color.a; // full color not supported in this mode
3021
+ context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
3029
3022
  context.drawImage(tileImage,
3030
3023
  (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.x,
3031
3024
  tileSize.x, tileSize.y, -.5, -.5, 1, 1);
@@ -3057,9 +3050,9 @@ class TileLayer extends EngineObject
3057
3050
  * let pos = vec2(2,3);
3058
3051
  * let particleEmiter = new ParticleEmitter
3059
3052
  * (
3060
- * pos, 1, 0, 500, PI, // pos, emitSize, emitTime, emitRate, emiteCone
3053
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emiteCone
3061
3054
  * 0, vec2(16), // tileIndex, tileSize
3062
- * new Color, new Color(0,0,0), // colorStartA, colorStartB
3055
+ * new Color(1,1,1), new Color(0,0,0), // colorStartA, colorStartB
3063
3056
  * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
3064
3057
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
3065
3058
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
@@ -3070,6 +3063,7 @@ class ParticleEmitter extends EngineObject
3070
3063
  {
3071
3064
  /** Create a particle system with the given settings
3072
3065
  * @param {Vector2} position - World space position of the emitter
3066
+ * @param {Number} [angle=0] - Angle to rotate the object
3073
3067
  * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3074
3068
  * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3075
3069
  * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
@@ -3093,12 +3087,13 @@ class ParticleEmitter extends EngineObject
3093
3087
  * @param {Number} [randomness=.2] - Apply extra randomness percent
3094
3088
  * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
3095
3089
  * @param {Boolean} [additive=0] - Should particles use addtive blend
3096
- * @param {Boolean} [randomColorLinear=0] - Should color be randomized linearly or across each component
3090
+ * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3097
3091
  * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3098
3092
  */
3099
3093
  constructor
3100
3094
  (
3101
3095
  pos,
3096
+ angle,
3102
3097
  emitSize = 0,
3103
3098
  emitTime = 0,
3104
3099
  emitRate = 100,
@@ -3126,7 +3121,7 @@ class ParticleEmitter extends EngineObject
3126
3121
  renderOrder = additive ? 1e9 : 0
3127
3122
  )
3128
3123
  {
3129
- super(pos, new Vector2, tileIndex, tileSize, 0, undefined, renderOrder);
3124
+ super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
3130
3125
 
3131
3126
  // emitter settings
3132
3127
  /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
@@ -3194,9 +3189,9 @@ class ParticleEmitter extends EngineObject
3194
3189
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
3195
3190
  {
3196
3191
  // emit particles
3197
- if (this.emitRate)
3192
+ if (this.emitRate * particleEmitRateScale)
3198
3193
  {
3199
- const rate = 1/this.emitRate;
3194
+ const rate = 1/this.emitRate/particleEmitRateScale;
3200
3195
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
3201
3196
  this.emitParticle();
3202
3197
  }
@@ -3359,7 +3354,7 @@ function medalsInit(saveName)
3359
3354
  {
3360
3355
  // check if medals are unlocked
3361
3356
  medalsSaveName = saveName;
3362
- debugMedals || medals.forEach(medal=> medal.unlocked = localStorage[medal.storageKey()]);
3357
+ debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
3363
3358
  }
3364
3359
 
3365
3360
  /**
@@ -3392,13 +3387,9 @@ class Medal
3392
3387
  this.name = name;
3393
3388
  this.description = description;
3394
3389
  this.icon = icon;
3395
-
3390
+ this.image = new Image();
3396
3391
  if (src)
3397
- {
3398
- // load image
3399
- this.image = new Image();
3400
3392
  this.image.src = src;
3401
- }
3402
3393
  }
3403
3394
 
3404
3395
  /** Unlocks a medal if not already unlocked */
@@ -3428,21 +3419,23 @@ class Medal
3428
3419
  const y = -medalDisplayHeight*hidePercent;
3429
3420
 
3430
3421
  // draw containing rect and clip to that region
3431
- context.beginPath(context.save());
3422
+ context.save();
3423
+ context.beginPath();
3432
3424
  context.fillStyle = '#ddd'
3433
3425
  context.fill(context.rect(x, y, width, medalDisplayHeight));
3434
- context.strokeStyle = context.fillStyle = '#000';
3435
- context.lineWidth = 2;
3426
+ context.strokeStyle = '#000';
3427
+ context.lineWidth = 3;
3436
3428
  context.stroke();
3437
3429
  context.clip();
3438
3430
 
3439
3431
  // draw the icon and text
3440
3432
  this.renderIcon(x+15+medalDisplayIconSize/2, y+medalDisplayHeight/2);
3441
3433
  context.textAlign = 'left';
3442
- context.font = '40px '+ fontDefault;
3443
- context.fillText(this.name, x+medalDisplayIconSize+25, y+35);
3444
- context.font = '25px '+ fontDefault;
3445
- context.restore(context.fillText(this.description, x+medalDisplayIconSize+25, y+70));
3434
+ context.font = '38px '+ fontDefault;
3435
+ context.fillText(this.name, x+medalDisplayIconSize+30, y+28);
3436
+ context.font = '24px '+ fontDefault;
3437
+ context.fillText(this.description, x+medalDisplayIconSize+30, y+60);
3438
+ context.restore();
3446
3439
  }
3447
3440
 
3448
3441
  /** Render the icon for a medal
@@ -3454,11 +3447,11 @@ class Medal
3454
3447
  {
3455
3448
  // draw the image or icon
3456
3449
  const context = overlayContext;
3450
+ context.fillStyle = '#000';
3457
3451
  context.textAlign = 'center';
3458
3452
  context.textBaseline = 'middle';
3459
- context.font = size*.6 + 'px '+ fontDefault;
3460
- context.fillStyle = '#000';
3461
- if (this.image)
3453
+ context.font = size*.7 + 'px '+ fontDefault;
3454
+ if (this.image.src)
3462
3455
  context.drawImage(this.image, x-size/2, y-size/2, size, size);
3463
3456
  else
3464
3457
  context.fillText(this.icon, x, y); // show icon if there is no image
@@ -3534,7 +3527,6 @@ class Newgrounds
3534
3527
  if (medal)
3535
3528
  {
3536
3529
  // copy newgrounds medal data
3537
- medal.image = new Image();
3538
3530
  medal.image.src = newgroundsMedal['icon'];
3539
3531
  medal.name = newgroundsMedal['name'];
3540
3532
  medal.description = newgroundsMedal['description'];
@@ -3562,9 +3554,6 @@ class Newgrounds
3562
3554
  * @param {Number} value - The score value */
3563
3555
  postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
3564
3556
 
3565
- /** Send message to log a view */
3566
- logView() { return this.call('App.logView', {'host':this.host}, 1); }
3567
-
3568
3557
  /** Get scores from a scoreboard
3569
3558
  * @param {Number} id - The scoreboard id
3570
3559
  * @param {String} [user=0] - A user's id or name
@@ -3576,6 +3565,9 @@ class Newgrounds
3576
3565
  getScores(id, user=0, social=0, skip=0, limit=10)
3577
3566
  { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3578
3567
 
3568
+ /** Send message to log a view */
3569
+ logView() { return this.call('App.logView', {'host':this.host}, 1); }
3570
+
3579
3571
  /** Send a message to call a component of the Newgrounds API
3580
3572
  * @param {String} component - Name of the component
3581
3573
  * @param {Object} [parameters=0] - Parameters to use for call
@@ -3663,38 +3655,33 @@ function glInit()
3663
3655
 
3664
3656
  // create the canvas and tile texture
3665
3657
  glCanvas = document.createElement('canvas');
3666
- glContext = glCanvas.getContext('webgl');
3667
-
3658
+ glContext = glCanvas.getContext('webgl', {antialias: false});
3659
+ glCanvas.style = styleCanvas;
3668
3660
  glTileTexture = glCreateTexture(tileImage);
3669
3661
 
3670
- if (glOverlay)
3671
- {
3672
- // some browsers are much faster without copying the gl buffer so we just overlay it instead
3673
- document.body.appendChild(glCanvas);
3674
- glCanvas.style = mainCanvas.style.cssText;
3675
- }
3662
+ // some browsers are much faster without copying the gl buffer so we just overlay it instead
3663
+ glOverlay && document.body.appendChild(glCanvas);
3676
3664
 
3677
3665
  // setup vertex and fragment shaders
3678
3666
  glShader = glCreateProgram(
3679
- 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3680
- 'uniform mat4 m;'+ // transform matrix
3681
- 'attribute float a;'+ // angle
3682
- 'attribute vec2 p,s,t;'+ // position, size, uv
3683
- 'attribute vec4 c,b;'+ // color, additiveColor
3684
- 'varying vec2 v;'+ // return uv
3685
- 'varying vec4 d,e;'+ // return color, additiveColor
3686
- 'void main(){'+ // shader entry point
3687
- 'gl_Position=m*vec4((s*cos(a)-vec2(-s.y,s)*sin(a))*.5+p,1,1);'+// transform position
3688
- 'v=t;d=c;e=b;'+ // pass stuff to fragment shader
3689
- '}' // end of shader
3667
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3668
+ 'uniform mat4 m;'+ // transform matrix
3669
+ 'attribute vec2 p,t;'+ // position, uv
3670
+ 'attribute vec4 c,a;'+ // color, additiveColor
3671
+ 'varying vec2 v;'+ // return uv
3672
+ 'varying vec4 d,e;'+ // return color, additiveColor
3673
+ 'void main(){'+ // shader entry point
3674
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
3675
+ 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3676
+ '}' // end of shader
3690
3677
  ,
3691
- 'precision lowp float;'+ // use highp for better accuracy, lowp for better perf
3692
- 'varying vec2 v;'+ // uv
3693
- 'varying vec4 d,e;'+ // color, additiveColor
3694
- 'uniform sampler2D j;'+ // texture
3695
- 'void main(){'+ // shader entry point
3696
- 'gl_FragColor=texture2D(j,v)*d+e;'+ // modulate texture by color plus additive
3697
- '}' // end of shader
3678
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3679
+ 'varying vec2 v;'+ // uv
3680
+ 'varying vec4 d,e;'+ // color, additiveColor
3681
+ 'uniform sampler2D s;'+ // texture
3682
+ 'void main(){'+ // shader entry point
3683
+ 'gl_FragColor=texture2D(s,v)*d+e;'+ // modulate texture by color plus additive
3684
+ '}' // end of shader
3698
3685
  );
3699
3686
 
3700
3687
  // init buffers
@@ -3704,6 +3691,7 @@ function glInit()
3704
3691
  glColorData = new Uint32Array(glVertexData);
3705
3692
 
3706
3693
  // setup the vertex data array
3694
+ let offset = glBatchCount = 0;
3707
3695
  const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3708
3696
  {
3709
3697
  const location = glContext.getAttribLocation(glShader, name);
@@ -3711,13 +3699,10 @@ function glInit()
3711
3699
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3712
3700
  offset += size*typeSize;
3713
3701
  }
3714
- let offset = glBatchCount = 0;
3715
- initVertexAttribArray('a', gl_FLOAT, 4, 1); // angle
3716
3702
  initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3717
- initVertexAttribArray('s', gl_FLOAT, 4, 2); // size
3718
3703
  initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3719
3704
  initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3720
- initVertexAttribArray('b', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3705
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3721
3706
  }
3722
3707
 
3723
3708
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3738,7 +3723,8 @@ function glSetBlendMode(additive)
3738
3723
  function glSetTexture(texture=glTileTexture)
3739
3724
  {
3740
3725
  if (!glEnable) return;
3741
-
3726
+
3727
+ // must flush cache with the old texture to set a new one
3742
3728
  if (texture != glActiveTexture)
3743
3729
  glFlush();
3744
3730
 
@@ -3831,6 +3817,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3831
3817
 
3832
3818
  // clear and set to same size as main canvas
3833
3819
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3820
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3834
3821
 
3835
3822
  // set up the shader
3836
3823
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
@@ -3874,15 +3861,13 @@ function glFlush()
3874
3861
  * @memberof WebGL */
3875
3862
  function glCopyToContext(context, forceDraw)
3876
3863
  {
3877
- if (!glEnable || !glBatchCount) return;
3864
+ if (!glEnable || !glBatchCount) return;
3878
3865
 
3879
3866
  glFlush();
3867
+
3868
+ // do not draw in overlay mode because the canvas is visible
3880
3869
  if (!glOverlay || forceDraw)
3881
- {
3882
- // do not draw/clear in overlay mode because the canvas is visible
3883
3870
  context.drawImage(glCanvas, 0, 0);
3884
- glContext.clear(gl_COLOR_BUFFER_BIT);
3885
- }
3886
3871
  }
3887
3872
 
3888
3873
  /** Add a sprite to the gl draw list, used by all gl draw functions
@@ -3905,51 +3890,49 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3905
3890
  // flush if there is no room for more verts or if different blend mode
3906
3891
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3907
3892
  glFlush();
3893
+
3894
+ // prepare to create the verts from size and angle
3895
+ const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
3896
+ const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
3908
3897
 
3909
3898
  // setup 2 triangles to form a quad
3910
3899
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3911
3900
 
3912
3901
  // vertex 0
3913
- glPositionData[offset++] = angle;
3914
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3915
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3916
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3917
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3902
+ glPositionData[offset++] = x - cx - sy;
3903
+ glPositionData[offset++] = y - cy + sx;
3904
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3905
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3918
3906
 
3919
3907
  // vertex 1
3920
- glPositionData[offset++] = angle;
3921
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3922
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3923
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3924
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3908
+ glPositionData[offset++] = x + cx + sy;
3909
+ glPositionData[offset++] = y + cy - sx;
3910
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3911
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3925
3912
 
3926
3913
  // vertex 2
3927
- glPositionData[offset++] = angle;
3928
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3929
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = sizeY;
3930
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3931
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3914
+ glPositionData[offset++] = x - cx + sy;
3915
+ glPositionData[offset++] = y + cy + sx;
3916
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3917
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3932
3918
 
3933
3919
  // vertex 0
3934
- glPositionData[offset++] = angle;
3935
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3936
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3937
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3938
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3920
+ glPositionData[offset++] = x - cx - sy;
3921
+ glPositionData[offset++] = y - cy + sx;
3922
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3923
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3939
3924
 
3940
3925
  // vertex 3
3941
- glPositionData[offset++] = angle;
3942
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3943
- glPositionData[offset++] = sizeX; glPositionData[offset++] = -sizeY;
3944
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3945
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3926
+ glPositionData[offset++] = x + cx - sy;
3927
+ glPositionData[offset++] = y - cy - sx;
3928
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3929
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3946
3930
 
3947
3931
  // vertex 1
3948
- glPositionData[offset++] = angle;
3949
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3950
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3951
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3952
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3932
+ glPositionData[offset++] = x + cx + sy;
3933
+ glPositionData[offset++] = y + cy - sx;
3934
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3935
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3953
3936
  }
3954
3937
 
3955
3938
  ///////////////////////////////////////////////////////////////////////////////
@@ -3981,9 +3964,9 @@ gl_LINK_STATUS = 35714,
3981
3964
 
3982
3965
  // constants for batch rendering
3983
3966
  gl_VERTICES_PER_QUAD = 6,
3984
- gl_INDICIES_PER_VERT = 9,
3967
+ gl_INDICIES_PER_VERT = 6,
3985
3968
  gl_MAX_BATCH = 1<<16,
3986
- gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char * 4) * 2
3969
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
3987
3970
  /*
3988
3971
  LittleJS - The Tiny JavaScript Game Engine That Can!
3989
3972
  MIT License - Copyright 2021 Frank Force
@@ -4008,7 +3991,7 @@ gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char *
4008
3991
  const engineName = 'LittleJS';
4009
3992
 
4010
3993
  /** Version of engine */
4011
- const engineVersion = '1.1.18';
3994
+ const engineVersion = '1.2.3';
4012
3995
 
4013
3996
  /** Frames per second to update objects
4014
3997
  * @default */
@@ -4021,7 +4004,7 @@ const timeDelta = 1/frameRate;
4021
4004
  /** Array containing all engine objects */
4022
4005
  let engineObjects = [];
4023
4006
 
4024
- /** Array containing only objects that are set to collide with other objects (for optimization) */
4007
+ /** Array containing only objects that are set to collide with other objects this frame (for optimization) */
4025
4008
  let engineObjectsCollide = [];
4026
4009
 
4027
4010
  /** Current update frame, used to calculate time */
@@ -4037,8 +4020,15 @@ let timeReal = 0;
4037
4020
  let paused = 0;
4038
4021
 
4039
4022
  // Engine internal variables not exposed to documentation
4040
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0,
4041
- shrinkTilesX, shrinkTilesY, drawCount, tileImageSize, tileImageSizeInverse;
4023
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed;
4024
+
4025
+ // Engine stat tracking, if showWatermark is true
4026
+ let averageFPS, drawCount;
4027
+
4028
+ // css text used for elements created by engine
4029
+ const styleBody = 'margin:0;overflow:hidden;background:#000' +
4030
+ ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
4031
+ const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4042
4032
 
4043
4033
  ///////////////////////////////////////////////////////////////////////////////
4044
4034
 
@@ -4056,17 +4046,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4056
4046
  tileImage.onerror = tileImage.onload = ()=>
4057
4047
  {
4058
4048
  // save tile image info
4059
- tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
4049
+ tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
4060
4050
  debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
4061
- shrinkTilesX = tileBleedShrinkFix/tileImageSize.x;
4062
- shrinkTilesY = tileBleedShrinkFix/tileImageSize.y;
4063
4051
 
4064
4052
  // setup html
4053
+ document.body.style = styleBody;
4065
4054
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
4066
- document.body.style = 'margin:0;overflow:hidden;background:#000' +
4067
- (touchMouseEnable?';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none':'');
4068
- mainCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4069
4055
  mainContext = mainCanvas.getContext('2d');
4056
+ mainCanvas.style = styleCanvas;
4070
4057
 
4071
4058
  // init stuff and start engine
4072
4059
  debugInit();
@@ -4074,8 +4061,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4074
4061
 
4075
4062
  // create overlay canvas for hud to appear above gl canvas
4076
4063
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
4077
- overlayCanvas.style = mainCanvas.style.cssText;
4078
4064
  overlayContext = overlayCanvas.getContext('2d');
4065
+ overlayCanvas.style = styleCanvas;
4079
4066
 
4080
4067
  gameInit();
4081
4068
  touchGamepadCreate();
@@ -4091,7 +4078,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4091
4078
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4092
4079
  frameTimeLastMS = frameTimeMS;
4093
4080
  if (debug || showWatermark)
4094
- debugFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), debugFPS);
4081
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
4095
4082
  if (debug)
4096
4083
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
4097
4084
  timeReal += frameTimeDeltaMS / 1e3;
@@ -4178,7 +4165,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4178
4165
  overlayContext.font = '1em monospace';
4179
4166
  overlayContext.fillStyle = '#000';
4180
4167
  const text = engineName + ' ' + 'v' + engineVersion + ' / '
4181
- + drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1);
4168
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4169
+ + ' ' + (glEnable ? 'GL' : '2D') ;
4182
4170
  overlayContext.fillText(text, mainCanvas.width-3, 3);
4183
4171
  overlayContext.fillStyle = '#fff';
4184
4172
  overlayContext.fillText(text, mainCanvas.width-2, 2);
@@ -4195,7 +4183,11 @@ function enginePreRender()
4195
4183
  {
4196
4184
  // save canvas size and clear canvases
4197
4185
  mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
4198
- mainContext.imageSmoothingEnabled = !cavasPixelated; // disable smoothing for pixel art
4186
+
4187
+ // disable smoothing for pixel art
4188
+ mainContext.imageSmoothingEnabled = !cavasPixelated;
4189
+
4190
+ // setup gl rendering if enabled
4199
4191
  glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
4200
4192
  }
4201
4193
 
@@ -4204,6 +4196,9 @@ function enginePreRender()
4204
4196
  /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */
4205
4197
  function engineObjectsUpdate()
4206
4198
  {
4199
+ // get list of solid objects for physics optimzation
4200
+ engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
4201
+
4207
4202
  // recursive object update
4208
4203
  const updateObject = (o)=>
4209
4204
  {
@@ -4219,17 +4214,16 @@ function engineObjectsUpdate()
4219
4214
 
4220
4215
  // remove destroyed objects
4221
4216
  engineObjects = engineObjects.filter(o=>!o.destroyed);
4222
- engineObjectsCollide = engineObjectsCollide.filter(o=>!o.destroyed);
4223
4217
 
4224
4218
  // increment frame and update time
4225
4219
  time = ++frame / frameRate;
4226
4220
  }
4227
4221
 
4228
- /** Detroy and remove all objects that are not persistent or descendants of a persistent object */
4222
+ /** Destroy and remove all objects */
4229
4223
  function engineObjectsDestroy()
4230
4224
  {
4231
4225
  for (const o of engineObjects)
4232
- o.persistent || o.parent || o.destroy();
4226
+ o.parent || o.destroy();
4233
4227
  engineObjects = engineObjects.filter(o=>!o.destroyed);
4234
4228
  }
4235
4229
 
@@ -4240,21 +4234,18 @@ function engineObjectsDestroy()
4240
4234
  * @param {Array} [objects=engineObjects] - List of objects to check */
4241
4235
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
4242
4236
  {
4243
- if (!pos)
4237
+ if (!pos) // all objects
4244
4238
  {
4245
- // all objects
4246
4239
  for (const o of objects)
4247
4240
  callbackFunction(o);
4248
4241
  }
4249
- else if (size.x != undefined)
4242
+ else if (size.x != undefined) // bounding box test
4250
4243
  {
4251
- // aabb test
4252
4244
  for (const o of objects)
4253
4245
  isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
4254
4246
  }
4255
- else
4247
+ else // circle test
4256
4248
  {
4257
- // circle test
4258
4249
  const sizeSquared = size*size;
4259
4250
  for (const o of objects)
4260
4251
  pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);