littlejsengine 1.1.17 → 1.2.0

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.
@@ -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
  ///////////////////////////////////////////////////////////////////////////////
@@ -156,22 +163,25 @@ const debugUpdate = ()=>
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;
235
246
 
247
+ const distance = mousePos.distanceSquared(o.pos);
248
+ if (distance < bestDistance)
249
+ {
250
+ bestDistance = distance;
251
+ bestObject = o
252
+ }
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
-
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
372
 
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
@@ -1057,7 +893,7 @@ class Timer
1057
893
  * @type {Vector2}
1058
894
  * @default
1059
895
  * @memberof Settings */
1060
- const canvasMaxSize = vec2(1920, 1200);
896
+ let canvasMaxSize = vec2(1920, 1200);
1061
897
 
1062
898
  /** Fixed size of the canvas, if enabled cavnvas size never changes
1063
899
  * @type {Vector2}
@@ -1082,12 +918,12 @@ let fontDefault = 'arial';
1082
918
  * @type {Vector2}
1083
919
  * @default
1084
920
  * @memberof Settings */
1085
- const tileSizeDefault = vec2(16);
921
+ let tileSizeDefault = vec2(16);
1086
922
 
1087
923
  /** Prevent tile bleeding from neighbors in pixels
1088
924
  * @default
1089
925
  * @memberof Settings */
1090
- const tileBleedShrinkFix = .3;
926
+ let tileFixBleedScale = .3;
1091
927
 
1092
928
  ///////////////////////////////////////////////////////////////////////////////
1093
929
  // Object settings
@@ -1096,43 +932,48 @@ const tileBleedShrinkFix = .3;
1096
932
  * @type {Vector2}
1097
933
  * @default
1098
934
  * @memberof Settings */
1099
- const objectDefaultSize = vec2(1);
935
+ let objectDefaultSize = vec2(1);
1100
936
 
1101
937
  /** Default object mass for collison calcuations (how heavy objects are)
1102
938
  * @default
1103
939
  * @memberof Settings */
1104
- const objectDefaultMass = 1;
940
+ let objectDefaultMass = 1;
1105
941
 
1106
942
  /** How much to slow velocity by each frame (0-1)
1107
943
  * @default
1108
944
  * @memberof Settings */
1109
- const objectDefaultDamping = .99;
945
+ let objectDefaultDamping = .99;
1110
946
 
1111
947
  /** How much to slow angular velocity each frame (0-1)
1112
948
  * @default
1113
949
  * @memberof Settings */
1114
- const objectDefaultAngleDamping = .99;
950
+ let objectDefaultAngleDamping = .99;
1115
951
 
1116
952
  /** How much to bounce when a collision occurs (0-1)
1117
953
  * @default
1118
954
  * @memberof Settings */
1119
- const objectDefaultElasticity = 0;
955
+ let objectDefaultElasticity = 0;
1120
956
 
1121
957
  /** How much to slow when touching (0-1)
1122
958
  * @default
1123
959
  * @memberof Settings */
1124
- const objectDefaultFriction = .8;
960
+ let objectDefaultFriction = .8;
1125
961
 
1126
962
  /** Clamp max speed to avoid fast objects missing collisions
1127
963
  * @default
1128
964
  * @memberof Settings */
1129
- const objectMaxSpeed = 1;
965
+ let objectMaxSpeed = 1;
1130
966
 
1131
967
  /** How much gravity to apply to objects along the Y axis, negative is down
1132
968
  * @default
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
 
@@ -1153,7 +994,7 @@ let cameraScale = max(tileSizeDefault.x, tileSizeDefault.y);
1153
994
  /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
1154
995
  * @default
1155
996
  * @memberof Settings */
1156
- const glEnable = 1;
997
+ let glEnable = 1;
1157
998
 
1158
999
  /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
1159
1000
  * @default
@@ -1166,34 +1007,39 @@ let glOverlay = 1;
1166
1007
  /** Should gamepads be allowed
1167
1008
  * @default
1168
1009
  * @memberof Settings */
1169
- const gamepadsEnable = 1;
1010
+ let gamepadsEnable = 1;
1170
1011
 
1171
1012
  /** If true, the dpad input is also routed to the left analog stick (for better accessability)
1172
1013
  * @default
1173
1014
  * @memberof Settings */
1174
- const gamepadDirectionEmulateStick = 1;
1015
+ let gamepadDirectionEmulateStick = 1;
1175
1016
 
1176
1017
  /** If true the WASD keys are also routed to the direction keys (for better accessability)
1177
1018
  * @default
1178
1019
  * @memberof Settings */
1179
- const inputWASDEmulateDirection = 1;
1020
+ let inputWASDEmulateDirection = 1;
1180
1021
 
1181
- /** If true touch input is routed to mouse functions
1022
+ /** True if touch gamepad should appear on mobile devices
1023
+ * <br> - Supports left analog stick, 4 face buttons and start button (button 9)
1024
+ * <br> - Must be set by end of gameInit to be activated
1182
1025
  * @default
1183
1026
  * @memberof Settings */
1184
- const touchMouseEnable = 1;
1027
+ let touchGamepadEnable = 0;
1185
1028
 
1186
- /** Size of virutal gamepad for touch devices in pixels, 80 recommended
1187
- * <br> - Supports left analog stick, 4 face buttons and start button (button 9)
1188
- * <br> - Must be set by end of gameInit to be activated
1029
+ /** Size of virutal gamepad for touch devices in pixels
1189
1030
  * @default
1190
1031
  * @memberof Settings */
1191
- let touchGamepadSize = 0;
1032
+ let touchGamepadSize = 80;
1192
1033
 
1193
1034
  /** Transparency of touch gamepad overlay
1194
1035
  * @default
1195
1036
  * @memberof Settings */
1196
- let touchGamepadAlpha = .5;
1037
+ let touchGamepadAlpha = .3;
1038
+
1039
+ /** Allow vibration hardware if it exists
1040
+ * @default
1041
+ * @memberof Settings */
1042
+ let vibrateEnable = 1;
1197
1043
 
1198
1044
  ///////////////////////////////////////////////////////////////////////////////
1199
1045
  // Audio settings
@@ -1206,17 +1052,17 @@ let soundVolume = .5;
1206
1052
  /** All audio code can be disabled and removed from build
1207
1053
  * @default
1208
1054
  * @memberof Settings */
1209
- const soundEnable = 1;
1055
+ let soundEnable = 1;
1210
1056
 
1211
1057
  /** Default range where sound no longer plays
1212
1058
  * @default
1213
1059
  * @memberof Settings */
1214
- const soundDefaultRange = 30;
1060
+ let soundDefaultRange = 30;
1215
1061
 
1216
1062
  /** Default range percent to start tapering off sound (0-1)
1217
1063
  * @default
1218
1064
  * @memberof Settings */
1219
- const soundDefaultTaper = .7;
1065
+ let soundDefaultTaper = .7;
1220
1066
 
1221
1067
  ///////////////////////////////////////////////////////////////////////////////
1222
1068
  // Medals settings
@@ -1224,27 +1070,27 @@ const soundDefaultTaper = .7;
1224
1070
  /** How long to show medals for in seconds
1225
1071
  * @default
1226
1072
  * @memberof Settings */
1227
- const medalDisplayTime = 5;
1073
+ let medalDisplayTime = 5;
1228
1074
 
1229
1075
  /** How quickly to slide on/off medals in seconds
1230
1076
  * @default
1231
1077
  * @memberof Settings */
1232
- const medalDisplaySlideTime = .5;
1078
+ let medalDisplaySlideTime = .5;
1233
1079
 
1234
1080
  /** Width of medal display
1235
1081
  * @default
1236
1082
  * @memberof Settings */
1237
- const medalDisplayWidth = 640;
1083
+ let medalDisplayWidth = 640;
1238
1084
 
1239
1085
  /** Height of medal display
1240
1086
  * @default
1241
1087
  * @memberof Settings */
1242
- const medalDisplayHeight = 99;
1088
+ let medalDisplayHeight = 80;
1243
1089
 
1244
1090
  /** Size of icon in medal display
1245
1091
  * @default
1246
1092
  * @memberof Settings */
1247
- const medalDisplayIconSize = 80;
1093
+ let medalDisplayIconSize = 50;
1248
1094
  /*
1249
1095
  LittleJS Object System
1250
1096
  */
@@ -1279,7 +1125,7 @@ const medalDisplayIconSize = 80;
1279
1125
  class EngineObject
1280
1126
  {
1281
1127
  /** Create an engine object and adds it to the list of objects
1282
- * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1128
+ * @param {Vector2} [position=new Vector2()] - World space position of the object
1283
1129
  * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1284
1130
  * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1285
1131
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
@@ -1324,12 +1170,15 @@ class EngineObject
1324
1170
  this.gravityScale = 1;
1325
1171
  /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1326
1172
  this.renderOrder = renderOrder;
1173
+ /** @property {Vector2} [velocity=new Vector2()] - Velocity of the object */
1174
+ this.velocity = new Vector2();
1175
+ /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1176
+ this.angleVelocity = 0;
1327
1177
 
1328
1178
  // init other internal object stuff
1329
1179
  this.spawnTime = time;
1330
- this.velocity = vec2(this.collideSolidObjects = this.angleVelocity = 0);
1331
- this.collideTiles = 1;
1332
1180
  this.children = [];
1181
+ this.collideTiles = 1;
1333
1182
 
1334
1183
  // add to list of objects
1335
1184
  engineObjects.push(this);
@@ -1348,8 +1197,8 @@ class EngineObject
1348
1197
  }
1349
1198
 
1350
1199
  // limit max speed to prevent missing collisions
1351
- this.velocity.x = clamp(this.velocity.x, objectMaxSpeed, -objectMaxSpeed);
1352
- this.velocity.y = clamp(this.velocity.y, objectMaxSpeed, -objectMaxSpeed);
1200
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1201
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1353
1202
 
1354
1203
  // apply physics
1355
1204
  const oldPos = this.pos.copy();
@@ -1397,7 +1246,7 @@ class EngineObject
1397
1246
  // if already was touching, try to push away
1398
1247
  const deltaPos = oldPos.subtract(o.pos);
1399
1248
  const length = deltaPos.length();
1400
- const pushAwayAccel = .001; // push away if alread overlapping
1249
+ const pushAwayAccel = .001; // push away if already overlapping
1401
1250
  const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
1402
1251
  this.velocity = this.velocity.add(velocity);
1403
1252
  if (o.mass) // push away if not fixed
@@ -1439,8 +1288,8 @@ class EngineObject
1439
1288
 
1440
1289
  // lerp betwen elastic or inelastic based on elasticity
1441
1290
  const elasticity = max(this.elasticity, o.elasticity);
1442
- this.velocity.y = lerp(elasticity, elastic0, inelastic);
1443
- o.velocity.y = lerp(elasticity, elastic1, inelastic);
1291
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1292
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1444
1293
  }
1445
1294
  }
1446
1295
  if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
@@ -1460,8 +1309,8 @@ class EngineObject
1460
1309
 
1461
1310
  // lerp betwen elastic or inelastic based on elasticity
1462
1311
  const elasticity = max(this.elasticity, o.elasticity);
1463
- this.velocity.x = lerp(elasticity, elastic0, inelastic);
1464
- o.velocity.x = lerp(elasticity, elastic1, inelastic);
1312
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1313
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1465
1314
  }
1466
1315
  else // bounce if other object is fixed
1467
1316
  this.velocity.x *= -this.elasticity;
@@ -1491,7 +1340,7 @@ class EngineObject
1491
1340
 
1492
1341
  // adjust next velocity to settle on ground
1493
1342
  const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1494
- if (o < 0 && o > -1 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1343
+ if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1495
1344
  this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1496
1345
 
1497
1346
  // move to previous position
@@ -1592,18 +1441,6 @@ class EngineObject
1592
1441
  {
1593
1442
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1594
1443
 
1595
- // track collidable objects in separate list
1596
- if (collideSolidObjects && !this.collideSolidObjects)
1597
- {
1598
- ASSERT(!engineObjectsCollide.includes(this));
1599
- engineObjectsCollide.push(this);
1600
- }
1601
- else if (!collideSolidObjects && this.collideSolidObjects)
1602
- {
1603
- ASSERT(engineObjectsCollide.includes(this))
1604
- engineObjectsCollide.splice(engineObjectsCollide.indexOf(this), 1);
1605
- }
1606
-
1607
1444
  this.collideSolidObjects = collideSolidObjects;
1608
1445
  this.isSolid = isSolid;
1609
1446
  this.collideTiles = collideTiles;
@@ -1703,17 +1540,13 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1703
1540
  {
1704
1541
  // calculate uvs and render
1705
1542
  const cols = tileImageSize.x / tileSize.x |0;
1706
- const uvSizeX = tileSize.x * tileImageSizeInverse.x;
1707
- const uvSizeY = tileSize.y * tileImageSizeInverse.y;
1543
+ const uvSizeX = tileSize.x / tileImageSize.x;
1544
+ const uvSizeY = tileSize.y / tileImageSize.y;
1708
1545
  const uvX = (tileIndex%cols)*uvSizeX, uvY = (tileIndex/cols|0)*uvSizeY;
1709
-
1710
- // shrink uvs to prevent bleeding
1711
- const shrinkTilesX = tileBleedShrinkFix * tileImageSizeInverse.x;
1712
- const shrinkTilesY = tileBleedShrinkFix * tileImageSizeInverse.y;
1713
1546
 
1714
1547
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
1715
- uvX + shrinkTilesX, uvY + shrinkTilesY,
1716
- uvX - shrinkTilesX + uvSizeX, uvY - shrinkTilesX + uvSizeY,
1548
+ uvX + tileImageFixBleed.x, uvY + tileImageFixBleed.y,
1549
+ uvX - tileImageFixBleed.x + uvSizeX, uvY - tileImageFixBleed.y + uvSizeY,
1717
1550
  color.rgbaInt(), additiveColor.rgbaInt());
1718
1551
  }
1719
1552
  }
@@ -1732,10 +1565,10 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1732
1565
  {
1733
1566
  // calculate uvs and render
1734
1567
  const cols = tileImageSize.x / tileSize.x |0;
1735
- const sX = (tileIndex%cols)*tileSize.x + tileBleedShrinkFix;
1736
- const sY = (tileIndex/cols|0)*tileSize.y + tileBleedShrinkFix;
1737
- const sWidth = tileSize.x - 2*tileBleedShrinkFix;
1738
- const sHeight = tileSize.y - 2*tileBleedShrinkFix;
1568
+ const sX = (tileIndex%cols)*tileSize.x + tileFixBleedScale;
1569
+ const sY = (tileIndex/cols|0)*tileSize.y + tileFixBleedScale;
1570
+ const sWidth = tileSize.x - 2*tileFixBleedScale;
1571
+ const sHeight = tileSize.y - 2*tileFixBleedScale;
1739
1572
  context.globalAlpha = color.a; // only alpha is supported
1740
1573
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1741
1574
  }
@@ -1818,6 +1651,18 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1818
1651
  context.restore();
1819
1652
  }
1820
1653
 
1654
+ /** Enable normal or additive blend mode
1655
+ * @param {Boolean} [additive=0]
1656
+ * @param {Boolean} [useWebGL=glEnable]
1657
+ * @memberof Draw */
1658
+ function setBlendMode(additive, useWebGL=glEnable)
1659
+ {
1660
+ if (glEnable && useWebGL)
1661
+ glSetBlendMode(additive);
1662
+ else
1663
+ mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1664
+ }
1665
+
1821
1666
  /** Draw text on overlay canvas in world space
1822
1667
  * @param {String} text
1823
1668
  * @param {Vector2} pos
@@ -1843,16 +1688,93 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
1843
1688
  overlayContext.fillText(text, pos.x, pos.y);
1844
1689
  }
1845
1690
 
1846
- /** Enable normal or additive blend mode
1847
- * @param {Boolean} [additive=0]
1848
- * @param {Boolean} [useWebGL=glEnable]
1849
- * @memberof Draw */
1850
- function setBlendMode(additive, useWebGL=glEnable)
1691
+ ///////////////////////////////////////////////////////////////////////////////
1692
+
1693
+ /**
1694
+ * Font Image Object - Draw text on a 2D canvas by using characters in an image
1695
+ * <br> - 96 characters (from space to tilde) are stored in an image
1696
+ * <br> - Uses a default 8x8 font if none is supplied
1697
+ * <br> - You can also use fonts from the main tile sheet
1698
+ * @example
1699
+ * // use built in font
1700
+ * const font = new ImageFont;
1701
+ *
1702
+ * // draw text
1703
+ * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1704
+ */
1705
+ class FontImage
1851
1706
  {
1852
- if (glEnable && useWebGL)
1853
- glSetBlendMode(additive);
1854
- else
1855
- mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1707
+ /** Create an image font
1708
+ * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
1709
+ * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1710
+ * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1711
+ * @param {Number} [startTileIndex=0] - Tile index in image where font starts
1712
+ * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
1713
+ */
1714
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1715
+ {
1716
+ if (!image)
1717
+ {
1718
+ // use default font image
1719
+ image = new Image();
1720
+ image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6MCIBdFY+Q0PMNgf0yCgsSAaZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWw1hPWNRcEyWh2npdpBmSUBrRaN1WSlQAWXaWLF0kkwR7C/N7XUvIaQr4xpeqeQigjLwBRZ/6KkufAACcv5z7GYhvgWHII8xN/4C2hvLCkmZSVgIJMHNTxpYOsJOiitEjUQnIQAFFP+opoQfCbjWmfzWiNzCjEvxAPFARmw/Y5Te0sl2zEp4M+m7cToJjdJS7y1xcFlFT7EtWBZ8nl7uLSff7UgYewtlyY6yHM4RrjNrnsfn/W0DBK9xNZctZ7gsTeXTe5eqLqXFlKnAn+vkNXzH6TqPW10ksKoSlauKk1TcncZLSTxNnQ8Av/8RGPXOFIuMCZ/fpwIZx5vU4QIRQ44HwUyHx/QrZhm5VtXFex7ja2se0KBpnPT0DSwS4xd86uAy+SqnLBgkPY5JolhJ33ENyK6n0VyRzCnzebIkFdY/NUtM66lTdhix1KUss6q5snH5ftVJXG0QITb1gUDvzD3z/BbyOLMirwCtPzAAAAAElFTkSuQmCC';
1721
+ }
1722
+
1723
+ this.image = image;
1724
+ this.tileSize = tileSize;
1725
+ this.paddingSize = paddingSize;
1726
+ this.startTileIndex = startTileIndex;
1727
+ this.context = context;
1728
+ }
1729
+
1730
+ /** Draw text in screen space using the image font
1731
+ * @param {String} text
1732
+ * @param {Vector2} pos
1733
+ * @param {Number} [scale=4]
1734
+ * @param {Boolean} [center]
1735
+ */
1736
+ drawTextScreen(text, pos, scale=4, center)
1737
+ {
1738
+ const context = this.context;
1739
+ context.save();
1740
+ context.imageSmoothingEnabled = !cavasPixelated;
1741
+
1742
+ const size = this.tileSize;
1743
+ const drawSize = size.add(this.paddingSize).scale(scale);
1744
+ const cols = this.image.width / this.tileSize.x |0;
1745
+ text.split('\n').forEach((line, i)=>
1746
+ {
1747
+ const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
1748
+ for(let j=line.length; j--;)
1749
+ {
1750
+ // draw each character
1751
+ let charCode = line[j].charCodeAt();
1752
+ if (charCode < 32 || charCode > 127)
1753
+ charCode = 127; // unknown character
1754
+
1755
+ // get the character source location and draw it
1756
+ const tile = this.startTileIndex + charCode - 32;
1757
+ const x = tile % cols;
1758
+ const y = tile / cols |0;
1759
+ const drawPos = pos.add(vec2(j,i).multiply(drawSize));
1760
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
1761
+ drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
1762
+ }
1763
+ });
1764
+
1765
+ context.restore();
1766
+ }
1767
+
1768
+ /** Draw text in world space using the image font
1769
+ * @param {String} text
1770
+ * @param {Vector2} pos
1771
+ * @param {Number} [scale=.25]
1772
+ * @param {Boolean} [center]
1773
+ */
1774
+ drawText(text, pos, scale=1, center)
1775
+ {
1776
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1777
+ }
1856
1778
  }
1857
1779
 
1858
1780
  ///////////////////////////////////////////////////////////////////////////////
@@ -1916,7 +1838,7 @@ const keyWasReleased = (key, device=0)=> inputData[device] && inputData[device][
1916
1838
 
1917
1839
  /** Clears all input
1918
1840
  * @memberof Input */
1919
- const clearInput = ()=> inputData[0] = [];
1841
+ const clearInput = ()=> inputData = [[]];
1920
1842
 
1921
1843
  /** Returns true if mouse button is down
1922
1844
  * @param {Number} button
@@ -1985,7 +1907,9 @@ const gamepadStick = (stick, gamepad=0)=> stickData[gamepad] ? stickData[gamepa
1985
1907
  ///////////////////////////////////////////////////////////////////////////////
1986
1908
  // Input update called by engine
1987
1909
 
1988
- const inputData = [[]];
1910
+ // store input as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
1911
+ // mouse and keyboard are stored together in device 0, gamepads are in devices > 0
1912
+ let inputData = [[]];
1989
1913
 
1990
1914
  function inputUpdate()
1991
1915
  {
@@ -2011,36 +1935,38 @@ function inputUpdatePost()
2011
1935
  ///////////////////////////////////////////////////////////////////////////////
2012
1936
  // Keyboard event handlers
2013
1937
 
2014
- onkeydown = e=>
1938
+ onkeydown = (e)=>
2015
1939
  {
2016
1940
  if (debug && e.target != document.body) return;
2017
1941
  e.repeat || (inputData[isUsingGamepad = 0][remapKeyCode(e.keyCode)] = 3);
2018
1942
  debug || e.preventDefault();
2019
1943
  }
2020
- onkeyup = e=>
1944
+ onkeyup = (e)=>
2021
1945
  {
2022
1946
  if (debug && e.target != document.body) return;
2023
1947
  inputData[0][remapKeyCode(e.keyCode)] = 4;
2024
1948
  }
2025
- const remapKeyCode = c=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
1949
+ const remapKeyCode = (c)=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
2026
1950
 
2027
1951
  ///////////////////////////////////////////////////////////////////////////////
2028
1952
  // Mouse event handlers
2029
1953
 
2030
- onmousedown = e=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
2031
- onmouseup = e=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2032
- onmousemove = e=>
1954
+ onmousedown = (e)=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
1955
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
1956
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
1957
+ onwheel = (e)=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
1958
+ oncontextmenu = (e)=> !1; // prevent right click menu
1959
+
1960
+ // convert a mouse or touch event position to screen space
1961
+ const mouseToScreen = (mousePos)=>
2033
1962
  {
2034
1963
  if (!mainCanvas)
2035
- return; // fix bug that can occur if user clicks before page loads
1964
+ return vec2(); // fix bug that can occur if user clicks before page loads
2036
1965
 
2037
- // convert mouse pos to canvas space
2038
1966
  const rect = mainCanvas.getBoundingClientRect();
2039
- mousePosScreen = mainCanvasSize.multiply(
2040
- vec2(percent(e.x, rect.right, rect.left), percent(e.y, rect.bottom, rect.top)));
1967
+ return mainCanvasSize.multiply(
1968
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
2041
1969
  }
2042
- onwheel = e=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
2043
- oncontextmenu = e=> !1; // prevent right click menu
2044
1970
 
2045
1971
  ///////////////////////////////////////////////////////////////////////////////
2046
1972
  // Gamepad input
@@ -2048,6 +1974,21 @@ oncontextmenu = e=> !1; // prevent right click menu
2048
1974
  const stickData = [];
2049
1975
  function gamepadsUpdate()
2050
1976
  {
1977
+ if (touchGamepadEnable && touchGamepadTimer.isSet())
1978
+ {
1979
+ // read virtual analog stick
1980
+ const sticks = stickData[0] || (stickData[0] = []);
1981
+ sticks[0] = vec2(touchGamepadStick.x, -touchGamepadStick.y); // flip vertical
1982
+
1983
+ // read virtual gamepad buttons
1984
+ const data = inputData[1] || (inputData[1] = []);
1985
+ for (let i=10; i--;)
1986
+ {
1987
+ const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
1988
+ data[j] = touchGamepadButtons[i] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
1989
+ }
1990
+ }
1991
+
2051
1992
  if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
2052
1993
  return;
2053
1994
 
@@ -2065,8 +2006,8 @@ function gamepadsUpdate()
2065
2006
  // read clamp dead zone of analog sticks
2066
2007
  const deadZone = .3, deadZoneMax = .8;
2067
2008
  const applyDeadZone = (v)=>
2068
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
2069
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
2009
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
2010
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
2070
2011
 
2071
2012
  // read analog sticks
2072
2013
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -2078,6 +2019,7 @@ function gamepadsUpdate()
2078
2019
  const button = gamepad.buttons[j];
2079
2020
  data[j] = button.pressed ? 1 + 2*!gamepadIsDown(j,i) : 4*gamepadIsDown(j,i);
2080
2021
  isUsingGamepad |= !i && button.pressed;
2022
+ touchGamepadEnable && touchGamepadTimer.unset(); // disable touch gamepad if using real gamepad
2081
2023
  }
2082
2024
 
2083
2025
  if (gamepadDirectionEmulateStick)
@@ -2091,6 +2033,17 @@ function gamepadsUpdate()
2091
2033
  }
2092
2034
  }
2093
2035
 
2036
+ ///////////////////////////////////////////////////////////////////////////////
2037
+
2038
+ /** Pulse the vibration hardware if it exists
2039
+ * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
2040
+ * @memberof Input */
2041
+ const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
2042
+
2043
+ /** Cancel any ongoing vibration
2044
+ * @memberof Input */
2045
+ const vibrateStop = ()=> vibrate(0);
2046
+
2094
2047
  ///////////////////////////////////////////////////////////////////////////////
2095
2048
  // Touch input
2096
2049
 
@@ -2098,7 +2051,9 @@ function gamepadsUpdate()
2098
2051
  * @const {boolean}
2099
2052
  * @memberof Input */
2100
2053
  const isTouchDevice = window.ontouchstart !== undefined;
2101
- if (touchMouseEnable && isTouchDevice)
2054
+
2055
+ // try to enable touch mouse
2056
+ if (isTouchDevice)
2102
2057
  {
2103
2058
  // handle all touch events the same way
2104
2059
  let wasTouching, hadTouchInput;
@@ -2129,19 +2084,25 @@ if (touchMouseEnable && isTouchDevice)
2129
2084
  }
2130
2085
 
2131
2086
  ///////////////////////////////////////////////////////////////////////////////
2132
- // Touch gamepad - on screen virtual gamepad
2087
+ // touch gamepad, virtual on screen gamepad emulator for touch devices
2133
2088
 
2134
- // create the touch gamepad, called automatically by the engine if touchGamepadSize is set
2135
- const touchGamepadTimer = new Timer;
2089
+ // touch input internal variables
2090
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
2091
+
2092
+ // create the touch gamepad, called automatically by the engine
2136
2093
  function touchGamepadCreate()
2137
2094
  {
2138
- if (!touchGamepadSize || !isTouchDevice)
2095
+ if (!touchGamepadEnable || !isTouchDevice)
2139
2096
  return;
2140
2097
 
2141
2098
  ontouchstart = ontouchmove = ontouchend = (e)=>
2142
2099
  {
2143
- if (!touchGamepadSize)
2100
+ if (!touchGamepadEnable)
2144
2101
  return;
2102
+
2103
+ // clear touch gamepad input
2104
+ touchGamepadStick = vec2();
2105
+ touchGamepadButtons = [];
2145
2106
 
2146
2107
  const touching = e.touches.length;
2147
2108
  if (touching)
@@ -2151,6 +2112,13 @@ function touchGamepadCreate()
2151
2112
  // set that gamepad is active
2152
2113
  isUsingGamepad = 1;
2153
2114
  touchGamepadTimer.set();
2115
+
2116
+ if (paused)
2117
+ {
2118
+ // touch anywhere to press start when paused
2119
+ touchGamepadButtons[9] = 1;
2120
+ return;
2121
+ }
2154
2122
  }
2155
2123
 
2156
2124
  // get center of left and right sides
@@ -2158,68 +2126,58 @@ function touchGamepadCreate()
2158
2126
  const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
2159
2127
  const startCenter = mainCanvasSize.scale(.5);
2160
2128
 
2161
- // update virtual gamepad
2162
- const data = inputData[1] || (inputData[1] = []);
2163
- const sticks = stickData[0] || (stickData[0] = []);
2164
- sticks[0] = vec2();
2165
-
2166
2129
  // check each touch point
2167
- const buttons = [];
2168
2130
  for (const touch of e.touches)
2169
2131
  {
2170
- const touchPos = vec2(touch.clientX, touch.clientY);
2132
+ const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
2171
2133
  if (touchPos.distance(stickCenter) < touchGamepadSize)
2172
2134
  {
2173
2135
  // virtual analog stick
2174
- sticks[0] = touchPos.subtract(stickCenter).clampLength();
2175
- sticks[0].y = -sticks[0].y; // flip vertical
2136
+ if (touchGamepadAnalog)
2137
+ touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2138
+ else
2139
+ {
2140
+ // 8 way dpad
2141
+ const angle = touchPos.subtract(stickCenter).angle();
2142
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2143
+ }
2176
2144
  }
2177
2145
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2178
2146
  {
2179
2147
  // virtual face buttons
2180
2148
  const button = touchPos.subtract(buttonCenter).direction();
2181
- buttons[button > 2 ? 2 : button > 1 ? 3 : button] = 1; // fix button locations
2149
+ touchGamepadButtons[button] = 1;
2182
2150
  }
2183
2151
  else if (touchPos.distance(startCenter) < touchGamepadSize)
2184
2152
  {
2185
- // virtual start button
2186
- buttons[9] = 1;
2153
+ // virtual start button in center
2154
+ touchGamepadButtons[9] = 1;
2187
2155
  }
2188
2156
  }
2189
-
2190
- // read virtual buttons
2191
- for (let j=10; j--;)
2192
- data[j] = buttons[j] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
2193
2157
  }
2194
2158
  }
2195
2159
 
2196
2160
  // render the touch gamepad, called automatically by the engine
2197
2161
  function touchGamepadRender()
2198
2162
  {
2199
- if (!touchGamepadSize || !touchGamepadTimer.isSet())
2163
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet())
2200
2164
  return;
2201
2165
 
2202
- // fade off when not touching
2203
- const alpha = percent(touchGamepadTimer.get(), 4, 5);
2204
- if (!alpha)
2166
+ // fade off when not touching or paused
2167
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
2168
+ if (!alpha || paused)
2205
2169
  return;
2206
2170
 
2207
2171
  // setup the canvas
2208
2172
  overlayContext.save();
2209
2173
  overlayContext.globalAlpha = alpha*touchGamepadAlpha;
2210
- overlayContext.fillStyle = '#fff6';
2211
2174
  overlayContext.strokeStyle = '#fff';
2212
2175
  overlayContext.lineWidth = 3;
2213
2176
 
2214
- // draw start button
2215
- //overlayContext.beginPath();
2216
- //overlayContext.arc(mainCanvasSize.x/2, mainCanvasSize.y/2, touchGamepadSize*.9, 0,9);
2217
- //overlayContext.fill();
2218
- //overlayContext.stroke();
2219
-
2220
2177
  // draw left analog stick
2178
+ overlayContext.fillStyle = gamepadStick(0).length() > 0 ? '#fff' : '#000';
2221
2179
  overlayContext.beginPath();
2222
- overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize*.9, 0,9);
2180
+ overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize/2, 0,9);
2223
2181
  overlayContext.fill();
2224
2182
  overlayContext.stroke();
2225
2183
 
@@ -2227,9 +2185,10 @@ function touchGamepadRender()
2227
2185
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2228
2186
  for (let i=4; i--;)
2229
2187
  {
2230
- const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize*.6));
2188
+ const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize/2));
2189
+ overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2231
2190
  overlayContext.beginPath();
2232
- overlayContext.arc(pos.x, pos.y, touchGamepadSize*.3, 0,9);
2191
+ overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2233
2192
  overlayContext.fill();
2234
2193
  overlayContext.stroke();
2235
2194
  }
@@ -2308,7 +2267,7 @@ class Sound
2308
2267
  return; // out of range
2309
2268
 
2310
2269
  // attenuate volume by distance
2311
- volume *= percent(lengthSquared**.5, range*this.taper, range);
2270
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
2312
2271
  }
2313
2272
 
2314
2273
  // get pan from screen space coords
@@ -2434,7 +2393,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
2434
2393
 
2435
2394
  /** Stop all queued speech
2436
2395
  * @memberof Audio */
2437
- const stopSpeech = ()=> speechSynthesis && speechSynthesis.cancel();
2396
+ const speakStop = ()=> speechSynthesis && speechSynthesis.cancel();
2438
2397
 
2439
2398
  /** Get frequency of a note on a musical scale
2440
2399
  * @param {Number} semitoneOffset - How many semitones away from the root note
@@ -2465,6 +2424,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2465
2424
  if (!audioContext)
2466
2425
  audioContext = new (window.AudioContext||webkitAudioContext);
2467
2426
 
2427
+ // fix stalled audio
2428
+ audioContext.resume();
2429
+
2430
+ // prevent sounds from building up if they can't be played
2431
+ if (audioContext.state != 'running')
2432
+ return;
2433
+
2468
2434
  // create buffer and source
2469
2435
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
2470
2436
  source = audioContext.createBufferSource();
@@ -2475,11 +2441,18 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2475
2441
  source.playbackRate.value = rate;
2476
2442
  source.loop = loop;
2477
2443
 
2478
- // create pan and gain nodes
2479
- source
2480
- .connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
2481
- .connect(new GainNode(audioContext, {'gain':soundVolume*volume}))
2482
- .connect(audioContext.destination);
2444
+ // create and connect gain node (createGain is more widley spported then GainNode construtor)
2445
+ const gainNode = audioContext.createGain();
2446
+ gainNode.gain.value = soundVolume*volume;
2447
+ gainNode.connect(audioContext.destination);
2448
+
2449
+ // connect source to gain
2450
+ (
2451
+ window.StereoPannerNode ? // create pan node if possible
2452
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2453
+ : source
2454
+ )
2455
+ .connect(gainNode);
2483
2456
 
2484
2457
  // play and return sound
2485
2458
  source.start();
@@ -2837,8 +2810,9 @@ class TileLayerData
2837
2810
  /**
2838
2811
  * Tile layer object - cached rendering system for tile layers
2839
2812
  * <br> - Each Tile layer is rendered to an off screen canvas
2840
- * <br> - Tile layers are not rendered using WebGL to allow modifications at run time
2841
- * <br> - Tile layers are sorted
2813
+ * <br> - To allow dynamic modifications, layers are rendered using canvas 2d
2814
+ * <br> - Some devices like mobile phones are limited to 4k texture resolution
2815
+ * <br> - So with 16x16 tiles this limits layers to 256x256 on mobile devices
2842
2816
  * @extends EngineObject
2843
2817
  * @example
2844
2818
  * // create tile collision and visible tile layer
@@ -2848,11 +2822,11 @@ class TileLayerData
2848
2822
  class TileLayer extends EngineObject
2849
2823
  {
2850
2824
  /** Create a tile layer object
2851
- * @param {Vector2} [position=new Vector2(0,0)] - World space position
2852
- * @param {Vector2} [size=objectDefaultSize] - World space size
2853
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2854
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2855
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2825
+ * @param {Vector2} [position=new Vector2()] - World space position
2826
+ * @param {Vector2} [size=objectDefaultSize] - World space size
2827
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2828
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2829
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2856
2830
  */
2857
2831
  constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2858
2832
  {
@@ -2903,7 +2877,7 @@ class TileLayer extends EngineObject
2903
2877
  // flush and copy gl canvas because tile canvas does not use webgl
2904
2878
  glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
2905
2879
 
2906
- // draw the entire cached level onto the main canvas
2880
+ // draw the entire cached level onto the canvas
2907
2881
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
2908
2882
  (this.isOverlay ? overlayContext : mainContext).drawImage
2909
2883
  (
@@ -2912,35 +2886,36 @@ class TileLayer extends EngineObject
2912
2886
  );
2913
2887
  }
2914
2888
 
2915
- /** Draw all the tile data to an offscreen canvas using webgl if possible */
2889
+ /** Draw all the tile data to an offscreen canvas
2890
+ * - This may be slow in some browsers
2891
+ */
2916
2892
  redraw()
2917
2893
  {
2918
- this.redrawStart();
2894
+ this.redrawStart(1);
2919
2895
  this.drawAllTileData();
2920
2896
  this.redrawEnd();
2921
2897
  }
2922
2898
 
2923
2899
  /** Call to start the redraw process
2924
- * @param {Boolean} [clear=1] - Should it clear the canvas before drawing */
2925
- redrawStart(clear = 1)
2900
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2901
+ redrawStart(clear = 0)
2926
2902
  {
2927
- // clear and set size
2928
- const width = this.size.x * this.tileSize.x;
2929
- const height = this.size.y * this.tileSize.y;
2930
2903
  if (clear)
2931
2904
  {
2932
- this.canvas.width = width;
2933
- this.canvas.height = height;
2905
+ // clear and set size
2906
+ this.canvas.width = this.size.x * this.tileSize.x;
2907
+ this.canvas.height = this.size.y * this.tileSize.y;
2934
2908
  }
2935
2909
 
2936
2910
  // save current render settings
2937
- this.savedRenderSettings = [mainCanvas, mainContext];
2911
+ this.savedRenderSettings = [mainCanvas, mainContext, cameraPos, cameraScale];
2938
2912
 
2939
- // set camera transform for renering
2913
+ // use normal rendering system to render the tiles
2940
2914
  mainCanvas = this.canvas;
2941
2915
  mainContext = this.context;
2942
- mainContext.imageSmoothingEnabled = !cavasPixelated; // disable smoothing for pixel art
2943
- glPreRender(width, height, this.size.x/2, this.size.y/2, this.tileSize.x);
2916
+ cameraPos = this.size.scale(.5);
2917
+ cameraScale = this.tileSize.x;
2918
+ enginePreRender();
2944
2919
  }
2945
2920
 
2946
2921
  /** Call to end the redraw process */
@@ -2951,7 +2926,7 @@ class TileLayer extends EngineObject
2951
2926
  //debugSaveCanvas(this.canvas);
2952
2927
 
2953
2928
  // set stuff back to normal
2954
- [mainCanvas, mainContext] = this.savedRenderSettings;
2929
+ [mainCanvas, mainContext, cameraPos, cameraScale] = this.savedRenderSettings;
2955
2930
  }
2956
2931
 
2957
2932
  /** Draw the tile at a given position
@@ -2979,7 +2954,7 @@ class TileLayer extends EngineObject
2979
2954
  this.drawTileData(vec2(x,y));
2980
2955
  }
2981
2956
 
2982
- /** Draw directly to the 2d canvas in world space (bipass webgl)
2957
+ /** Draw directly to the 2D canvas in world space (bipass webgl)
2983
2958
  * @param {Vector2} pos
2984
2959
  * @param {Vector2} size
2985
2960
  * @param {Number} [angle=0]
@@ -3019,7 +2994,7 @@ class TileLayer extends EngineObject
3019
2994
  else
3020
2995
  {
3021
2996
  const cols = tileImage.width/tileSize.x;
3022
- context.globalAlpha = color.a; // full color not supported in this mode
2997
+ context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
3023
2998
  context.drawImage(tileImage,
3024
2999
  (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.x,
3025
3000
  tileSize.x, tileSize.y, -.5, -.5, 1, 1);
@@ -3087,7 +3062,7 @@ class ParticleEmitter extends EngineObject
3087
3062
  * @param {Number} [randomness=.2] - Apply extra randomness percent
3088
3063
  * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
3089
3064
  * @param {Boolean} [additive=0] - Should particles use addtive blend
3090
- * @param {Boolean} [randomColorLinear=0] - Should color be randomized linearly or across each component
3065
+ * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3091
3066
  * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3092
3067
  */
3093
3068
  constructor
@@ -3188,9 +3163,9 @@ class ParticleEmitter extends EngineObject
3188
3163
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
3189
3164
  {
3190
3165
  // emit particles
3191
- if (this.emitRate)
3166
+ if (this.emitRate * particleEmitRateScale)
3192
3167
  {
3193
- const rate = 1/this.emitRate;
3168
+ const rate = 1/this.emitRate/particleEmitRateScale;
3194
3169
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
3195
3170
  this.emitParticle();
3196
3171
  }
@@ -3353,7 +3328,7 @@ function medalsInit(saveName)
3353
3328
  {
3354
3329
  // check if medals are unlocked
3355
3330
  medalsSaveName = saveName;
3356
- debugMedals || medals.forEach(medal=> medal.unlocked = localStorage[medal.storageKey()]);
3331
+ debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
3357
3332
  }
3358
3333
 
3359
3334
  /**
@@ -3386,13 +3361,9 @@ class Medal
3386
3361
  this.name = name;
3387
3362
  this.description = description;
3388
3363
  this.icon = icon;
3389
-
3364
+ this.image = new Image();
3390
3365
  if (src)
3391
- {
3392
- // load image
3393
- this.image = new Image();
3394
3366
  this.image.src = src;
3395
- }
3396
3367
  }
3397
3368
 
3398
3369
  /** Unlocks a medal if not already unlocked */
@@ -3422,21 +3393,23 @@ class Medal
3422
3393
  const y = -medalDisplayHeight*hidePercent;
3423
3394
 
3424
3395
  // draw containing rect and clip to that region
3425
- context.beginPath(context.save());
3396
+ context.save();
3397
+ context.beginPath();
3426
3398
  context.fillStyle = '#ddd'
3427
3399
  context.fill(context.rect(x, y, width, medalDisplayHeight));
3428
- context.strokeStyle = context.fillStyle = '#000';
3429
- context.lineWidth = 2;
3400
+ context.strokeStyle = '#000';
3401
+ context.lineWidth = 3;
3430
3402
  context.stroke();
3431
3403
  context.clip();
3432
3404
 
3433
3405
  // draw the icon and text
3434
3406
  this.renderIcon(x+15+medalDisplayIconSize/2, y+medalDisplayHeight/2);
3435
3407
  context.textAlign = 'left';
3436
- context.font = '40px '+ fontDefault;
3437
- context.fillText(this.name, x+medalDisplayIconSize+25, y+35);
3438
- context.font = '25px '+ fontDefault;
3439
- context.restore(context.fillText(this.description, x+medalDisplayIconSize+25, y+70));
3408
+ context.font = '38px '+ fontDefault;
3409
+ context.fillText(this.name, x+medalDisplayIconSize+30, y+28);
3410
+ context.font = '24px '+ fontDefault;
3411
+ context.fillText(this.description, x+medalDisplayIconSize+30, y+60);
3412
+ context.restore();
3440
3413
  }
3441
3414
 
3442
3415
  /** Render the icon for a medal
@@ -3448,11 +3421,11 @@ class Medal
3448
3421
  {
3449
3422
  // draw the image or icon
3450
3423
  const context = overlayContext;
3424
+ context.fillStyle = '#000';
3451
3425
  context.textAlign = 'center';
3452
3426
  context.textBaseline = 'middle';
3453
- context.font = size*.6 + 'px '+ fontDefault;
3454
- context.fillStyle = '#000';
3455
- if (this.image)
3427
+ context.font = size*.7 + 'px '+ fontDefault;
3428
+ if (this.image.src)
3456
3429
  context.drawImage(this.image, x-size/2, y-size/2, size, size);
3457
3430
  else
3458
3431
  context.fillText(this.icon, x, y); // show icon if there is no image
@@ -3528,7 +3501,6 @@ class Newgrounds
3528
3501
  if (medal)
3529
3502
  {
3530
3503
  // copy newgrounds medal data
3531
- medal.image = new Image();
3532
3504
  medal.image.src = newgroundsMedal['icon'];
3533
3505
  medal.name = newgroundsMedal['name'];
3534
3506
  medal.description = newgroundsMedal['description'];
@@ -3657,32 +3629,27 @@ function glInit()
3657
3629
 
3658
3630
  // create the canvas and tile texture
3659
3631
  glCanvas = document.createElement('canvas');
3660
- glContext = glCanvas.getContext('webgl');
3661
-
3632
+ glContext = glCanvas.getContext('webgl', {antialias: false});
3633
+ glCanvas.style = styleCanvas;
3662
3634
  glTileTexture = glCreateTexture(tileImage);
3663
3635
 
3664
- if (glOverlay)
3665
- {
3666
- // some browsers are much faster without copying the gl buffer so we just overlay it instead
3667
- document.body.appendChild(glCanvas);
3668
- glCanvas.style = mainCanvas.style.cssText;
3669
- }
3636
+ // some browsers are much faster without copying the gl buffer so we just overlay it instead
3637
+ glOverlay && document.body.appendChild(glCanvas);
3670
3638
 
3671
3639
  // setup vertex and fragment shaders
3672
3640
  glShader = glCreateProgram(
3673
3641
  'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3674
3642
  'uniform mat4 m;'+ // transform matrix
3675
- 'attribute float a;'+ // angle
3676
- 'attribute vec2 p,s,t;'+ // position, size, uv
3677
- 'attribute vec4 c,b;'+ // color, additiveColor
3643
+ 'attribute vec2 p,t;'+ // position, uv
3644
+ 'attribute vec4 c,a;'+ // color, additiveColor
3678
3645
  'varying vec2 v;'+ // return uv
3679
3646
  'varying vec4 d,e;'+ // return color, additiveColor
3680
3647
  'void main(){'+ // shader entry point
3681
- 'gl_Position=m*vec4((s*cos(a)-vec2(-s.y,s)*sin(a))*.5+p,1,1);'+// transform position
3682
- 'v=t;d=c;e=b;'+ // pass stuff to fragment shader
3648
+ 'gl_Position=m*vec4(p,1,1);'+// transform position
3649
+ 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3683
3650
  '}' // end of shader
3684
3651
  ,
3685
- 'precision lowp float;'+ // use highp for better accuracy, lowp for better perf
3652
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3686
3653
  'varying vec2 v;'+ // uv
3687
3654
  'varying vec4 d,e;'+ // color, additiveColor
3688
3655
  'uniform sampler2D j;'+ // texture
@@ -3698,6 +3665,7 @@ function glInit()
3698
3665
  glColorData = new Uint32Array(glVertexData);
3699
3666
 
3700
3667
  // setup the vertex data array
3668
+ let offset = glBatchCount = 0;
3701
3669
  const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3702
3670
  {
3703
3671
  const location = glContext.getAttribLocation(glShader, name);
@@ -3705,13 +3673,10 @@ function glInit()
3705
3673
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3706
3674
  offset += size*typeSize;
3707
3675
  }
3708
- let offset = glBatchCount = 0;
3709
- initVertexAttribArray('a', gl_FLOAT, 4, 1); // angle
3710
3676
  initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3711
- initVertexAttribArray('s', gl_FLOAT, 4, 2); // size
3712
3677
  initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3713
3678
  initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3714
- initVertexAttribArray('b', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3679
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3715
3680
  }
3716
3681
 
3717
3682
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3732,7 +3697,8 @@ function glSetBlendMode(additive)
3732
3697
  function glSetTexture(texture=glTileTexture)
3733
3698
  {
3734
3699
  if (!glEnable) return;
3735
-
3700
+
3701
+ // must flush cache with the old texture to set a new one
3736
3702
  if (texture != glActiveTexture)
3737
3703
  glFlush();
3738
3704
 
@@ -3825,6 +3791,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3825
3791
 
3826
3792
  // clear and set to same size as main canvas
3827
3793
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3794
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3828
3795
 
3829
3796
  // set up the shader
3830
3797
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
@@ -3868,15 +3835,13 @@ function glFlush()
3868
3835
  * @memberof WebGL */
3869
3836
  function glCopyToContext(context, forceDraw)
3870
3837
  {
3871
- if (!glEnable || !glBatchCount) return;
3838
+ if (!glEnable || !glBatchCount) return;
3872
3839
 
3873
3840
  glFlush();
3841
+
3842
+ // do not draw in overlay mode because the canvas is visible
3874
3843
  if (!glOverlay || forceDraw)
3875
- {
3876
- // do not draw/clear in overlay mode because the canvas is visible
3877
3844
  context.drawImage(glCanvas, 0, 0);
3878
- glContext.clear(gl_COLOR_BUFFER_BIT);
3879
- }
3880
3845
  }
3881
3846
 
3882
3847
  /** Add a sprite to the gl draw list, used by all gl draw functions
@@ -3899,51 +3864,49 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3899
3864
  // flush if there is no room for more verts or if different blend mode
3900
3865
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3901
3866
  glFlush();
3867
+
3868
+ // prepare to create the verts from size and angle
3869
+ const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
3870
+ const cX = c*sizeX, cY = c*sizeY, sX = s*sizeX, sY = s*sizeY;
3902
3871
 
3903
3872
  // setup 2 triangles to form a quad
3904
3873
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3905
3874
 
3906
3875
  // vertex 0
3907
- glPositionData[offset++] = angle;
3908
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3909
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3910
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3911
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3876
+ glPositionData[offset++] = x - cX - sY;
3877
+ glPositionData[offset++] = y - cY + sX;
3878
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3879
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3912
3880
 
3913
3881
  // vertex 1
3914
- glPositionData[offset++] = angle;
3915
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3916
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3917
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3918
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3882
+ glPositionData[offset++] = x + cX + sY;
3883
+ glPositionData[offset++] = y + cY - sX;
3884
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3885
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3919
3886
 
3920
3887
  // vertex 2
3921
- glPositionData[offset++] = angle;
3922
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3923
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = sizeY;
3924
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3925
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3888
+ glPositionData[offset++] = x - cX + sY;
3889
+ glPositionData[offset++] = y + cY + sX;
3890
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3891
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3926
3892
 
3927
3893
  // vertex 0
3928
- glPositionData[offset++] = angle;
3929
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3930
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3931
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3932
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3894
+ glPositionData[offset++] = x - cX - sY;
3895
+ glPositionData[offset++] = y - cY + sX;
3896
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3897
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3933
3898
 
3934
3899
  // vertex 3
3935
- glPositionData[offset++] = angle;
3936
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3937
- glPositionData[offset++] = sizeX; glPositionData[offset++] = -sizeY;
3938
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3939
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3900
+ glPositionData[offset++] = x + cX - sY;
3901
+ glPositionData[offset++] = y - cY - sX;
3902
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3903
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3940
3904
 
3941
3905
  // vertex 1
3942
- glPositionData[offset++] = angle;
3943
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3944
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3945
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3946
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3906
+ glPositionData[offset++] = x + cX + sY;
3907
+ glPositionData[offset++] = y + cY - sX;
3908
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3909
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3947
3910
  }
3948
3911
 
3949
3912
  ///////////////////////////////////////////////////////////////////////////////
@@ -3975,9 +3938,9 @@ gl_LINK_STATUS = 35714,
3975
3938
 
3976
3939
  // constants for batch rendering
3977
3940
  gl_VERTICES_PER_QUAD = 6,
3978
- gl_INDICIES_PER_VERT = 9,
3941
+ gl_INDICIES_PER_VERT = 6,
3979
3942
  gl_MAX_BATCH = 1<<16,
3980
- gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char * 4) * 2
3943
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
3981
3944
  /*
3982
3945
  LittleJS - The Tiny JavaScript Game Engine That Can!
3983
3946
  MIT License - Copyright 2021 Frank Force
@@ -4002,7 +3965,7 @@ gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char *
4002
3965
  const engineName = 'LittleJS';
4003
3966
 
4004
3967
  /** Version of engine */
4005
- const engineVersion = '1.1.17';
3968
+ const engineVersion = '1.2.0';
4006
3969
 
4007
3970
  /** Frames per second to update objects
4008
3971
  * @default */
@@ -4015,7 +3978,7 @@ const timeDelta = 1/frameRate;
4015
3978
  /** Array containing all engine objects */
4016
3979
  let engineObjects = [];
4017
3980
 
4018
- /** Array containing only objects that are set to collide with other objects (for optimization) */
3981
+ /** Array containing only objects that are set to collide with other objects this frame (for optimization) */
4019
3982
  let engineObjectsCollide = [];
4020
3983
 
4021
3984
  /** Current update frame, used to calculate time */
@@ -4031,8 +3994,15 @@ let timeReal = 0;
4031
3994
  let paused = 0;
4032
3995
 
4033
3996
  // Engine internal variables not exposed to documentation
4034
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0,
4035
- shrinkTilesX, shrinkTilesY, drawCount, tileImageSize, tileImageSizeInverse;
3997
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed;
3998
+
3999
+ // Engine stat tracking, if showWatermark is true
4000
+ let averageFPS, drawCount;
4001
+
4002
+ // css text used for elements created by engine
4003
+ const styleBody = 'margin:0;overflow:hidden;background:#000' +
4004
+ ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
4005
+ const styleCanvas = 'position:absolute';
4036
4006
 
4037
4007
  ///////////////////////////////////////////////////////////////////////////////
4038
4008
 
@@ -4050,17 +4020,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4050
4020
  tileImage.onerror = tileImage.onload = ()=>
4051
4021
  {
4052
4022
  // save tile image info
4053
- tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
4023
+ tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
4054
4024
  debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
4055
- shrinkTilesX = tileBleedShrinkFix/tileImageSize.x;
4056
- shrinkTilesY = tileBleedShrinkFix/tileImageSize.y;
4057
4025
 
4058
4026
  // setup html
4027
+ document.body.style = styleBody;
4059
4028
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
4060
- document.body.style = 'margin:0;overflow:hidden;background:#000' +
4061
- (touchMouseEnable?';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none':'');
4062
- mainCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4063
4029
  mainContext = mainCanvas.getContext('2d');
4030
+ mainCanvas.style = styleCanvas;
4064
4031
 
4065
4032
  // init stuff and start engine
4066
4033
  debugInit();
@@ -4068,8 +4035,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4068
4035
 
4069
4036
  // create overlay canvas for hud to appear above gl canvas
4070
4037
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
4071
- overlayCanvas.style = mainCanvas.style.cssText;
4072
4038
  overlayContext = overlayCanvas.getContext('2d');
4039
+ overlayCanvas.style = styleCanvas;
4073
4040
 
4074
4041
  gameInit();
4075
4042
  touchGamepadCreate();
@@ -4085,7 +4052,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4085
4052
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4086
4053
  frameTimeLastMS = frameTimeMS;
4087
4054
  if (debug || showWatermark)
4088
- debugFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), debugFPS);
4055
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
4089
4056
  if (debug)
4090
4057
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
4091
4058
  timeReal += frameTimeDeltaMS / 1e3;
@@ -4136,13 +4103,24 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4136
4103
 
4137
4104
  // fit to window by adding space on top or bottom if necessary
4138
4105
  const aspect = innerWidth / innerHeight;
4139
- const fixedAspect = mainCanvas.width / mainCanvas.height;
4140
- mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
4141
- mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
4142
- if (glCanvas)
4106
+ const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
4107
+
4108
+ // clear css
4109
+ mainCanvas.style.width = overlayCanvas.style.width =
4110
+ mainCanvas.style.height = overlayCanvas.style.height =
4111
+ mainCanvas.style.top = overlayCanvas.style.top =
4112
+ mainCanvas.style.left = overlayCanvas.style.left = '';
4113
+
4114
+ // letterbox center canvas
4115
+ if (aspect < fixedAspect)
4116
+ {
4117
+ mainCanvas.style.width = overlayCanvas.style.width = innerWidth;
4118
+ mainCanvas.style.top = overlayCanvas.style.top = (innerHeight - innerWidth/fixedAspect)/2;
4119
+ }
4120
+ else
4143
4121
  {
4144
- glCanvas.style.width = mainCanvas.style.width;
4145
- glCanvas.style.height = mainCanvas.style.height;
4122
+ mainCanvas.style.height = overlayCanvas.style.height = innerHeight;
4123
+ mainCanvas.style.left = overlayCanvas.style.left = (innerWidth - innerHeight*fixedAspect)/2;
4146
4124
  }
4147
4125
  }
4148
4126
  else
@@ -4150,14 +4128,22 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4150
4128
  // clear and set size to same as window
4151
4129
  mainCanvas.width = min(innerWidth, canvasMaxSize.x);
4152
4130
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
4131
+
4132
+ // center canvas
4133
+ overlayCanvas.style.top = mainCanvas.style.top = (innerHeight - mainCanvas.height)/2;
4134
+ overlayCanvas.style.left = mainCanvas.style.left = (innerWidth - mainCanvas.width)/2;
4135
+ }
4136
+
4137
+ if (glEnable)
4138
+ {
4139
+ glCanvas.style.width = mainCanvas.style.width;
4140
+ glCanvas.style.height = mainCanvas.style.height;
4141
+ glCanvas.style.top = mainCanvas.style.top;
4142
+ glCanvas.style.left = mainCanvas.style.left;
4153
4143
  }
4154
4144
 
4155
- // save canvas size and clear overlay canvas
4156
- mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
4157
- mainContext.imageSmoothingEnabled = !cavasPixelated; // disable smoothing for pixel art
4158
-
4159
4145
  // render sort then render while removing destroyed objects
4160
- glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
4146
+ enginePreRender();
4161
4147
  gameRender();
4162
4148
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
4163
4149
  for (const o of engineObjects)
@@ -4176,7 +4162,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4176
4162
  overlayContext.font = '1em monospace';
4177
4163
  overlayContext.fillStyle = '#000';
4178
4164
  const text = engineName + ' ' + 'v' + engineVersion + ' / '
4179
- + drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1);
4165
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4166
+ + ' ' + (glEnable ? 'GL' : '2D') ;
4180
4167
  overlayContext.fillText(text, mainCanvas.width-3, 3);
4181
4168
  overlayContext.fillStyle = '#fff';
4182
4169
  overlayContext.fillText(text, mainCanvas.width-2, 2);
@@ -4188,12 +4175,27 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4188
4175
  tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
4189
4176
  }
4190
4177
 
4178
+ // called by engine to setup render system
4179
+ function enginePreRender()
4180
+ {
4181
+ // save canvas size and clear canvases
4182
+ mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
4183
+
4184
+ // disable smoothing for pixel art
4185
+ mainContext.imageSmoothingEnabled = !cavasPixelated;
4186
+
4187
+ // setup gl rendering if enabled
4188
+ glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
4189
+ }
4191
4190
 
4192
4191
  ///////////////////////////////////////////////////////////////////////////////
4193
4192
 
4194
4193
  /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */
4195
4194
  function engineObjectsUpdate()
4196
4195
  {
4196
+ // get list of solid objects for physics optimzation
4197
+ engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
4198
+
4197
4199
  // recursive object update
4198
4200
  const updateObject = (o)=>
4199
4201
  {
@@ -4209,17 +4211,16 @@ function engineObjectsUpdate()
4209
4211
 
4210
4212
  // remove destroyed objects
4211
4213
  engineObjects = engineObjects.filter(o=>!o.destroyed);
4212
- engineObjectsCollide = engineObjectsCollide.filter(o=>!o.destroyed);
4213
4214
 
4214
4215
  // increment frame and update time
4215
4216
  time = ++frame / frameRate;
4216
4217
  }
4217
4218
 
4218
- /** Detroy and remove all objects that are not persistent or descendants of a persistent object */
4219
+ /** Destroy and remove all objects */
4219
4220
  function engineObjectsDestroy()
4220
4221
  {
4221
4222
  for (const o of engineObjects)
4222
- o.persistent || o.parent || o.destroy();
4223
+ o.parent || o.destroy();
4223
4224
  engineObjects = engineObjects.filter(o=>!o.destroyed);
4224
4225
  }
4225
4226
 
@@ -4230,21 +4231,18 @@ function engineObjectsDestroy()
4230
4231
  * @param {Array} [objects=engineObjects] - List of objects to check */
4231
4232
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
4232
4233
  {
4233
- if (!pos)
4234
+ if (!pos) // all objects
4234
4235
  {
4235
- // all objects
4236
4236
  for (const o of objects)
4237
4237
  callbackFunction(o);
4238
4238
  }
4239
- else if (size.x != undefined)
4239
+ else if (size.x != undefined) // bounding box test
4240
4240
  {
4241
- // aabb test
4242
4241
  for (const o of objects)
4243
4242
  isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
4244
4243
  }
4245
- else
4244
+ else // circle test
4246
4245
  {
4247
- // circle test
4248
4246
  const sizeSquared = size*size;
4249
4247
  for (const o of objects)
4250
4248
  pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);