littlejsengine 1.9.8 → 1.9.11

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 (42) hide show
  1. package/README.md +2 -0
  2. package/dist/littlejs.d.ts +38 -5
  3. package/dist/littlejs.esm.js +106 -58
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +98 -56
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +81 -54
  8. package/examples/box2d/game.js +34 -16
  9. package/examples/box2d/gameObjects.js +485 -0
  10. package/examples/box2d/index.html +6 -18
  11. package/examples/box2d/scenes.js +32 -251
  12. package/examples/box2d/tiles.png +0 -0
  13. package/examples/breakout/index.html +3 -3
  14. package/examples/breakoutTutorial/index.html +2 -2
  15. package/examples/electron/build.js +1 -1
  16. package/examples/electron/index.html +2 -2
  17. package/examples/js13k/build.js +19 -1
  18. package/examples/js13k/index.html +13 -13
  19. package/examples/module/index.html +1 -1
  20. package/examples/particles/index.html +1 -1
  21. package/examples/platformer/gameCharacter.js +1 -0
  22. package/examples/platformer/gameEffects.js +0 -2
  23. package/examples/platformer/gameObjects.js +1 -1
  24. package/examples/platformer/index.html +8 -8
  25. package/examples/puzzle/index.html +2 -2
  26. package/examples/starter/build.js +1 -1
  27. package/examples/starter/game.js +1 -1
  28. package/examples/starter/index.html +13 -13
  29. package/examples/stress/index.html +1 -1
  30. package/examples/typescript/index.html +1 -1
  31. package/package.json +1 -1
  32. package/plugins/box2d.js +126 -47
  33. package/plugins/postProcess.js +3 -2
  34. package/src/engine.js +12 -11
  35. package/src/engineAudio.js +16 -16
  36. package/src/engineBuild.js +1 -1
  37. package/src/engineDebug.js +17 -2
  38. package/src/engineExport.js +7 -2
  39. package/src/engineInput.js +13 -6
  40. package/src/engineMedals.js +3 -2
  41. package/src/engineObject.js +25 -19
  42. package/src/engineSettings.js +12 -0
package/plugins/box2d.js CHANGED
@@ -43,8 +43,9 @@ class Box2dObject extends EngineObject
43
43
 
44
44
  destroy()
45
45
  {
46
- // destroy physics body and fixtures
47
- box2dWorld.DestroyBody(this.body);
46
+ // destroy physics body, fixtures, and joints
47
+ this.body && box2dWorld.DestroyBody(this.body);
48
+ this.body = 0;
48
49
  super.destroy();
49
50
  }
50
51
 
@@ -91,20 +92,20 @@ class Box2dObject extends EngineObject
91
92
  addShape(shape, density, friction, restitution, isSensor)
92
93
  {
93
94
  const fd = box2dCreateFixtureDef(shape, density, friction, restitution, isSensor);
94
- this.addFixture(fd);
95
+ return this.addFixture(fd);
95
96
  }
96
97
 
97
98
  addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
98
99
  {
99
100
  const shape = new box2d.b2PolygonShape();
100
101
  shape.SetAsBox(size.x/2, size.y/2, offset.getBox2d(), angle);
101
- this.addShape(shape, density, friction, restitution, isSensor);
102
+ return this.addShape(shape, density, friction, restitution, isSensor);
102
103
  }
103
104
 
104
105
  addPoly(points, density, friction, restitution, isSensor)
105
106
  {
106
107
  const shape = box2dCreatePolygonShape(points);
107
- this.addShape(shape, density, friction, restitution, isSensor);
108
+ return this.addShape(shape, density, friction, restitution, isSensor);
108
109
  }
109
110
 
110
111
  addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
@@ -113,7 +114,7 @@ class Box2dObject extends EngineObject
113
114
  const radius = diameter/2;
114
115
  for (let i=sides; i--;)
115
116
  points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
116
- this.addPoly(points, density, friction, restitution, isSensor);
117
+ return this.addPoly(points, density, friction, restitution, isSensor);
117
118
  }
118
119
 
119
120
  addRandomPoly(diameter=1, density, friction, restitution, isSensor)
@@ -123,7 +124,7 @@ class Box2dObject extends EngineObject
123
124
  const radius = diameter/2;
124
125
  for (let i=sides; i--;)
125
126
  points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));
126
- this.addPoly(points, density, friction, restitution, isSensor);
127
+ return this.addPoly(points, density, friction, restitution, isSensor);
127
128
  }
128
129
 
129
130
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
@@ -131,18 +132,19 @@ class Box2dObject extends EngineObject
131
132
  const shape = new box2d.b2CircleShape();
132
133
  shape.set_m_p(offset.getBox2d());
133
134
  shape.set_m_radius(diameter/2);
134
- this.addShape(shape, density, friction, restitution, isSensor);
135
+ return this.addShape(shape, density, friction, restitution, isSensor);
135
136
  }
136
137
 
137
138
  addEdge(point1, point2, density, friction, restitution, isSensor)
138
139
  {
139
140
  const shape = new box2d.b2EdgeShape();
140
141
  shape.Set(point1.getBox2d(), point2.getBox2d());
141
- this.addShape(shape, density, friction, restitution, isSensor);
142
+ return this.addShape(shape, density, friction, restitution, isSensor);
142
143
  }
143
144
 
144
145
  addEdgeLoop(points, density, friction, restitution, isSensor)
145
146
  {
147
+ const fixtures = [];
146
148
  const getPoint = i=> points[mod(i,points.length)];
147
149
  for (let i=0; i<points.length; ++i)
148
150
  {
@@ -151,12 +153,15 @@ class Box2dObject extends EngineObject
151
153
  shape.set_m_vertex1(getPoint(i+0).getBox2d());
152
154
  shape.set_m_vertex2(getPoint(i+1).getBox2d());
153
155
  shape.set_m_vertex3(getPoint(i+2).getBox2d());
154
- this.addShape(shape, density, friction, restitution, isSensor);
156
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
157
+ fixtures.push(f);
155
158
  }
159
+ return fixtures;
156
160
  }
157
161
 
158
162
  addEdgeList(points, density, friction, restitution, isSensor)
159
163
  {
164
+ const fixtures = [];
160
165
  for (let i=0; i<points.length-1; ++i)
161
166
  {
162
167
  const shape = new box2d.b2EdgeShape();
@@ -164,19 +169,37 @@ class Box2dObject extends EngineObject
164
169
  points[i+0] && shape.set_m_vertex1(points[i+0].getBox2d());
165
170
  points[i+1] && shape.set_m_vertex2(points[i+1].getBox2d());
166
171
  points[i+2] && shape.set_m_vertex3(points[i+2].getBox2d());
167
- this.addShape(shape, density, friction, restitution, isSensor);
172
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
173
+ fixtures.push(f);
168
174
  }
175
+ return fixtures;
169
176
  }
170
177
 
178
+ ///////////////////////////////////////////////////////////////////////////////
179
+ // lists of fixtures and joints
180
+
181
+ hasFixtures() { return !box2dIsNull(this.body.GetFixtureList()); }
171
182
  getFixtureList()
172
183
  {
173
- const fixtureList = [];
184
+ const fixtures = [];
174
185
  for (let fixture=this.body.GetFixtureList(); !box2dIsNull(fixture); )
175
186
  {
176
- fixtureList.push(fixture);
187
+ fixtures.push(fixture);
177
188
  fixture = fixture.GetNext();
178
189
  }
179
- return fixtureList;
190
+ return fixtures;
191
+ }
192
+
193
+ hasJoints() { return !box2dIsNull(this.body.GetJointList()); }
194
+ getJointList()
195
+ {
196
+ const joints = [];
197
+ for (let joint=this.body.GetJointList(); !box2dIsNull(joint); )
198
+ {
199
+ joints.push(joint);
200
+ joint = joint.get_next();
201
+ }
202
+ return joints;
180
203
  }
181
204
 
182
205
  ///////////////////////////////////////////////////////////////////////////////
@@ -214,6 +237,19 @@ class Box2dObject extends EngineObject
214
237
  setBodyType(type) { this.body.SetType(type); }
215
238
  setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
216
239
  setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
240
+ setCenterOfMass(center) { this.setMassData(center) }
241
+ setMass(mass) { this.setMassData(undefined, mass) }
242
+ setMomentOfInertia(I) { this.setMassData(undefined, undefined, I) }
243
+ resetMassData() { this.body.ResetMassData(); }
244
+ setMassData(localCenter, mass, momentOfInertia)
245
+ {
246
+ const data = new box2d.b2MassData();
247
+ this.body.GetMassData(data);
248
+ localCenter && data.set_center(localCenter.getBox2d());
249
+ mass && data.set_mass(mass);
250
+ momentOfInertia && data.set_I(momentOfInertia);
251
+ this.body.SetMassData(data);
252
+ }
217
253
  setFilterData(categoryBits=0, ignoreCategoryBits=0, groupIndex=0)
218
254
  {
219
255
  this.getFixtureList().forEach(fixture=>
@@ -228,17 +264,17 @@ class Box2dObject extends EngineObject
228
264
  { this.getFixtureList().forEach(f=>f.SetSensor(isSensor)); }
229
265
 
230
266
  ///////////////////////////////////////////////////////////////////////////////
231
- // physics kinematics
267
+ // physics force and torque functions
232
268
 
233
269
  applyForce(force, pos)
234
270
  {
235
- pos = pos || this.getCenterOfMass();
271
+ pos ||= this.getCenterOfMass();
236
272
  this.setAwake();
237
273
  this.body.ApplyForce(force.getBox2d(), pos.getBox2d());
238
274
  }
239
275
  applyAcceleration(acceleration, pos)
240
276
  {
241
- pos = pos || this.getCenterOfMass();
277
+ pos ||= this.getCenterOfMass();
242
278
  this.setAwake();
243
279
  this.body.ApplyLinearImpulse(acceleration.getBox2d(), pos.getBox2d());
244
280
  }
@@ -270,7 +306,7 @@ class Box2dRaycastResult
270
306
  }
271
307
  }
272
308
 
273
- // raycast and return a list of all results
309
+ // raycast and return a list of all the results
274
310
  function box2dRaycastAll(start, end)
275
311
  {
276
312
  const raycastCallback = new box2d.JSRayCastCallback();
@@ -293,11 +329,12 @@ function box2dRaycastAll(start, end)
293
329
  function box2dRaycast(start, end)
294
330
  {
295
331
  const raycastResults = box2dRaycastAll(start, end);
296
- return raycastResults.length ?
297
- raycastResults.reduce((a,b)=>a.fraction < b.fraction ? a : b) : undefined;
332
+ if (!raycastResults.length)
333
+ return undefined;
334
+ return raycastResults.reduce((a,b)=>a.fraction < b.fraction ? a : b);
298
335
  }
299
336
 
300
- // box aabb cast and return the all results
337
+ // box aabb cast and return all the objects
301
338
  function box2dBoxCastAll(pos, size)
302
339
  {
303
340
  const queryCallback = new box2d.JSQueryCallback();
@@ -320,7 +357,7 @@ function box2dBoxCastAll(pos, size)
320
357
  return queryObjects;
321
358
  }
322
359
 
323
- // box aabb cast and return the first result
360
+ // box aabb cast and return the first object
324
361
  function box2dBoxCast(pos, size)
325
362
  {
326
363
  const queryCallback = new box2d.JSQueryCallback();
@@ -341,7 +378,7 @@ function box2dBoxCast(pos, size)
341
378
  return queryObject;
342
379
  }
343
380
 
344
- // circle cast and return the all results
381
+ // circle cast and return all the objects
345
382
  function box2dCircleCastAll(pos, diameter)
346
383
  {
347
384
  const radius2 = (diameter/2)**2;
@@ -349,17 +386,26 @@ function box2dCircleCastAll(pos, diameter)
349
386
  return results.filter(o=>o.pos.distanceSquared(pos) < radius2);
350
387
  }
351
388
 
352
- // circle cast and return the first result
389
+ // circle cast and return the first object
353
390
  function box2dCircleCast(pos, diameter)
354
391
  {
355
392
  const radius2 = (diameter/2)**2;
356
393
  let results = box2dBoxCastAll(pos, vec2(diameter));
357
- results = results.filter(o=>o.pos.distanceSquared(pos) < radius2);
358
- results = results.sort((a,b)=>a.pos.distanceSquared(pos)-b.pos.distanceSquared(pos));
359
- return results[0];
394
+
395
+ let bestResult, bestDistance2;
396
+ for (const result of results)
397
+ {
398
+ const distance2 = result.pos.distanceSquared(pos);
399
+ if (distance2 < radius2 && (!bestResult || distance2 < bestDistance2))
400
+ {
401
+ bestResult = result;
402
+ bestDistance2 = distance2;
403
+ }
404
+ }
405
+ return bestResult;
360
406
  }
361
407
 
362
- // point cast and return the first result
408
+ // point cast and return the first object
363
409
  function box2dPointCast(pos, dynamicOnly=true)
364
410
  {
365
411
  const queryCallback = new box2d.JSQueryCallback();
@@ -384,6 +430,28 @@ function box2dPointCast(pos, dynamicOnly=true)
384
430
  return queryResult;
385
431
  }
386
432
 
433
+ // box aabb cast and return all the fixtures
434
+ function box2dBoxCastAllFixtures(pos, size)
435
+ {
436
+ const queryCallback = new box2d.JSQueryCallback();
437
+ queryCallback.ReportFixture = function(fixturePointer)
438
+ {
439
+ const fixture = box2d.wrapPointer(fixturePointer, box2d.b2Fixture);
440
+ if (!queryObjects.includes(fixture))
441
+ queryObjects.push(fixture); // add if not already in list
442
+ return true; // continue getting results
443
+ };
444
+
445
+ const aabb = new box2d.b2AABB();
446
+ aabb.set_lowerBound(pos.subtract(size.scale(.5)).getBox2d());
447
+ aabb.set_upperBound(pos.add(size.scale(.5)).getBox2d());
448
+
449
+ let queryFixtures = [];
450
+ box2dWorld.QueryAABB(queryCallback, aabb);
451
+ debugRaycast && debugRect(pos, size, raycstResult ? '#f00' : '#00f', .02);
452
+ return queryFixtures;
453
+ }
454
+
387
455
  ///////////////////////////////////////////////////////////////////////////////
388
456
  // Box2D Joints
389
457
 
@@ -394,14 +462,19 @@ function box2dCreateMouseJoint(object, fixedObject, worldPos)
394
462
  jointDef.set_bodyA(fixedObject.body);
395
463
  jointDef.set_bodyB(object.body);
396
464
  jointDef.set_target(worldPos.getBox2d());
397
- jointDef.set_maxForce(1e3 * object.getMass());
465
+ jointDef.set_maxForce(2e3 * object.getMass());
398
466
  return box2dCastObject(box2dWorld.CreateJoint(jointDef));
399
467
  }
400
468
 
469
+ function box2dCreatePinJoint(objectA, objectB, collide=false)
470
+ {
471
+ return box2dCreateDistanceJoint(objectA, objectB, objectB.pos, undefined, collide);
472
+ }
473
+
401
474
  function box2dCreateDistanceJoint(objectA, objectB, anchorA, anchorB, collide=false)
402
475
  {
403
- anchorA = anchorA || vec2(objectA.body.GetPosition());
404
- anchorB = anchorB || vec2(objectB.body.GetPosition());
476
+ anchorA ||= vec2(objectA.body.GetPosition());
477
+ anchorB ||= vec2(objectB.body.GetPosition());
405
478
  const localAnchorA = objectA.worldToLocal(anchorA);
406
479
  const localAnchorB = objectB.worldToLocal(anchorB);
407
480
  const jointDef = new box2d.b2DistanceJointDef();
@@ -416,7 +489,7 @@ function box2dCreateDistanceJoint(objectA, objectB, anchorA, anchorB, collide=fa
416
489
 
417
490
  function box2dCreateRevoluteJoint(objectA, objectB, anchor, collide=false)
418
491
  {
419
- anchor = anchor || vec2(objectB.body.GetPosition());
492
+ anchor ||= vec2(objectB.body.GetPosition());
420
493
  const localAnchorA = objectA.worldToLocal(anchor);
421
494
  const localAnchorB = objectB.worldToLocal(anchor);
422
495
  const jointDef = new box2d.b2RevoluteJointDef();
@@ -431,7 +504,7 @@ function box2dCreateRevoluteJoint(objectA, objectB, anchor, collide=false)
431
504
 
432
505
  function box2dCreatePrismaticJoint(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
433
506
  {
434
- anchor = anchor || vec2(objectB.body.GetPosition());
507
+ anchor ||= vec2(objectB.body.GetPosition());
435
508
  const localAnchorA = objectA.worldToLocal(anchor);
436
509
  const localAnchorB = objectB.worldToLocal(anchor);
437
510
  const localAxisA = objectB.worldToLocalVector(worldAxis);
@@ -448,7 +521,7 @@ function box2dCreatePrismaticJoint(objectA, objectB, anchor, worldAxis=vec2(0,1)
448
521
 
449
522
  function box2dCreateWheelJoint(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
450
523
  {
451
- anchor = anchor || vec2(objectB.body.GetPosition());
524
+ anchor ||= vec2(objectB.body.GetPosition());
452
525
  const localAnchorA = objectA.worldToLocal(anchor);
453
526
  const localAnchorB = objectB.worldToLocal(anchor);
454
527
  const localAxisA = objectB.worldToLocalVector(worldAxis);
@@ -464,7 +537,7 @@ function box2dCreateWheelJoint(objectA, objectB, anchor, worldAxis=vec2(0,1), co
464
537
 
465
538
  function box2dCreateWeldJoint(objectA, objectB, anchor, collide=false)
466
539
  {
467
- anchor = anchor || vec2(objectB.body.GetPosition());
540
+ anchor ||= vec2(objectB.body.GetPosition());
468
541
  const localAnchorA = objectA.worldToLocal(anchor);
469
542
  const localAnchorB = objectB.worldToLocal(anchor);
470
543
  const jointDef = new box2d.b2WeldJointDef();
@@ -479,7 +552,7 @@ function box2dCreateWeldJoint(objectA, objectB, anchor, collide=false)
479
552
 
480
553
  function box2dCreateFrictionJoint(objectA, objectB, anchor, collide=false)
481
554
  {
482
- anchor = anchor || vec2(objectB.body.GetPosition());
555
+ anchor ||= vec2(objectB.body.GetPosition());
483
556
  const localAnchorA = objectA.worldToLocal(anchor);
484
557
  const localAnchorB = objectB.worldToLocal(anchor);
485
558
  const jointDef = new box2d.b2FrictionJointDef();
@@ -493,8 +566,8 @@ function box2dCreateFrictionJoint(objectA, objectB, anchor, collide=false)
493
566
 
494
567
  function box2dCreateRopeJoint(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false)
495
568
  {
496
- anchorA = anchorA || vec2(objectA.body.GetPosition());
497
- anchorB = anchorB || vec2(objectB.body.GetPosition());
569
+ anchorA ||= vec2(objectA.body.GetPosition());
570
+ anchorB ||= vec2(objectB.body.GetPosition());
498
571
  const localAnchorA = objectA.worldToLocal(anchorA);
499
572
  const localAnchorB = objectB.worldToLocal(anchorB);
500
573
  const jointDef = new box2d.b2RopeJointDef();
@@ -509,8 +582,8 @@ function box2dCreateRopeJoint(objectA, objectB, anchorA, anchorB, extraLength=0,
509
582
 
510
583
  function box2dCreatePulleyJoint(objectA, objectB, groundAnchorA, groundAnchorB, anchorA, anchorB, ratio=1, collide=false)
511
584
  {
512
- anchorA = anchorA || vec2(objectA.body.GetPosition());
513
- anchorB = anchorB || vec2(objectB.body.GetPosition());
585
+ anchorA ||= vec2(objectA.body.GetPosition());
586
+ anchorB ||= vec2(objectB.body.GetPosition());
514
587
  const localAnchorA = objectA.worldToLocal(anchorA);
515
588
  const localAnchorB = objectB.worldToLocal(anchorB);
516
589
  const jointDef = new box2d.b2PulleyJointDef();
@@ -638,6 +711,13 @@ function box2dCastObject(object)
638
711
  ASSERT(false, 'Unknown object type');
639
712
  }
640
713
 
714
+ function box2dWarmup(frames=100)
715
+ {
716
+ // run the sim for a few frames to let objects settle
717
+ for (let i=frames; i--;)
718
+ box2dWorld.Step(timeDelta, box2dStepIterations, box2dStepIterations);
719
+ }
720
+
641
721
  ///////////////////////////////////////////////////////////////////////////////
642
722
  // Box2D Drawing
643
723
 
@@ -792,17 +872,16 @@ function box2dEngineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameR
792
872
  box2dWorld.SetDebugDraw(box2dDebugDraw);
793
873
 
794
874
  // hook up box2d plugin to update and render
795
- addPluginUpdate(function()
875
+ engineAddPlugin(box2dUpdate, box2dRender);
876
+ function box2dUpdate()
796
877
  {
797
- const velocityIterations = box2dStepIterations;
798
- const positionIterations = box2dStepIterations;
799
- box2dWorld.Step(timeDelta, velocityIterations, positionIterations);
800
- });
801
- addPluginRender(function()
878
+ box2dWorld.Step(timeDelta, box2dStepIterations, box2dStepIterations);
879
+ }
880
+ function box2dRender()
802
881
  {
803
882
  if (box2dDebug || debugPhysics && debugOverlay)
804
883
  box2dWorld.DrawDebugData();
805
- });
884
+ }
806
885
 
807
886
  // start littlejs
808
887
  engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources);
@@ -55,7 +55,8 @@ function initPostProcess(shaderCode, includeOverlay=false)
55
55
  overlayCanvas.style.visibility = 'hidden';
56
56
 
57
57
  // Render the post processing shader, called automatically by the engine
58
- addPluginRender(function()
58
+ engineAddPlugin(undefined, postProcessRender);
59
+ function postProcessRender()
59
60
  {
60
61
  if (headlessMode) return;
61
62
 
@@ -97,5 +98,5 @@ function initPostProcess(shaderCode, includeOverlay=false)
97
98
  glContext.uniform1f(uniformLocation('iTime'), time);
98
99
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
99
100
  glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 4);
100
- });
101
+ }
101
102
  }
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {String}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.9.8';
33
+ const engineVersion = '1.9.11';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {Number}
@@ -89,14 +89,14 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
89
89
  const pluginUpdateList = [], pluginRenderList = [];
90
90
 
91
91
  /** Add a new update function for a plugin
92
- * @param {Function} updateFunction
92
+ * @param {Function} [updateFunction]
93
+ * @param {Function} [renderFunction]
93
94
  * @memberof Engine */
94
- function addPluginUpdate(updateFunction) { pluginUpdateList.push(updateFunction); }
95
-
96
- /** Add a new render function for a plugin
97
- * @param {Function} renderFunction
98
- * @memberof Engine */
99
- function addPluginRender(renderFunction) { pluginRenderList.push(renderFunction); }
95
+ function engineAddPlugin(updateFunction, renderFunction)
96
+ {
97
+ updateFunction && pluginUpdateList.push(updateFunction);
98
+ renderFunction && pluginRenderList.push(renderFunction);
99
+ }
100
100
 
101
101
  ///////////////////////////////////////////////////////////////////////////////
102
102
  // Main engine functions
@@ -269,10 +269,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
269
269
  const styleBody =
270
270
  'margin:0;overflow:hidden;' + // fill the window
271
271
  'background:#000;' + // set background color
272
- 'touch-action:none;' + // prevent mobile pinch to resize
273
- 'user-select:none;' + // prevent mobile hold to select
272
+ 'user-select:none;' + // prevent hold to select
274
273
  '-webkit-user-select:none;' + // compatibility for ios
275
- '-webkit-touch-callout:none'; // compatibility for ios
274
+ (!touchInputEnable ? '' : // no touch css setttings
275
+ 'touch-action:none;' + // prevent mobile pinch to resize
276
+ '-webkit-touch-callout:none');// compatibility for ios
276
277
  document.body.style.cssText = styleBody;
277
278
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
278
279
  mainContext = mainCanvas.getContext('2d');
@@ -73,6 +73,7 @@ class Sound
73
73
  // generate zzfx sound now for fast playback
74
74
  const defaultRandomness = .05;
75
75
  this.randomness = zzfxSound[1] || defaultRandomness;
76
+ zzfxSound[1] = 0; // generate without randomness
76
77
  this.sampleChannels = [zzfxG(...zzfxSound)];
77
78
  this.sampleRate = zzfxR;
78
79
  }
@@ -305,10 +306,6 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
305
306
 
306
307
  ///////////////////////////////////////////////////////////////////////////////
307
308
 
308
- // internal tracking if audio was suspended when last sound was played
309
- // allows first suspended sound to play when audio is resumed
310
- let audioSuspended = false;
311
-
312
309
  /** Play cached audio samples with given settings
313
310
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
314
311
  * @param {Number} [volume] - How much to scale volume by
@@ -324,20 +321,21 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
324
321
  if (!soundEnable || headlessMode) return;
325
322
 
326
323
  // prevent sounds from building up if they can't be played
327
- const audioWasSuspended = audioSuspended;
328
- if (audioSuspended = audioContext.state != 'running')
324
+ if (audioContext.state != 'running')
329
325
  {
330
326
  // fix stalled audio
331
- audioContext.resume();
327
+ audioContext.resume().then(()=>
328
+ playSamples(sampleChannels, volume, rate, pan, loop, sampleRate, gainNode));
332
329
 
333
330
  // prevent suspended sounds from building up
334
- if (audioWasSuspended)
335
- return;
331
+ return;
336
332
  }
337
333
 
338
334
  // create buffer and source
339
- const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, sampleRate),
340
- source = audioContext.createBufferSource();
335
+ const channelCount = sampleChannels.length;
336
+ const sampleLength = sampleChannels[0].length;
337
+ const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
338
+ const source = audioContext.createBufferSource();
341
339
 
342
340
  // copy samples to buffer and setup source
343
341
  sampleChannels.forEach((c,i)=> buffer.getChannelData(i).set(c));
@@ -351,7 +349,8 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
351
349
  gainNode.connect(audioGainNode);
352
350
 
353
351
  // connect source to stereo panner and gain
354
- source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
352
+ const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
353
+ source.connect(pannerNode).connect(gainNode);
355
354
 
356
355
  // play and return sound
357
356
  source.start();
@@ -367,7 +366,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
367
366
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
368
367
  * @return {AudioBufferSourceNode} - The audio node of the sound played
369
368
  * @memberof Audio */
370
- function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
369
+ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
371
370
 
372
371
  /** Sample rate used for all ZzFX sounds
373
372
  * @default 44100
@@ -376,7 +375,7 @@ const zzfxR = 44100;
376
375
 
377
376
  /** Generate samples for a ZzFX sound
378
377
  * @param {Number} [volume] - Volume scale (percent)
379
- * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
378
+ * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
380
379
  * @param {Number} [frequency] - Frequency of sound (Hz)
381
380
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
382
381
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -402,7 +401,7 @@ const zzfxR = 44100;
402
401
  function zzfxG
403
402
  (
404
403
  // parameters
405
- volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
404
+ volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
406
405
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
407
406
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
408
407
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
@@ -413,7 +412,8 @@ function zzfxG
413
412
  // init parameters
414
413
  let PI2 = PI*2, sampleRate = zzfxR,
415
414
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
416
- startFrequency = frequency *= PI2 / sampleRate,
415
+ startFrequency = frequency *=
416
+ rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
417
417
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
418
418
 
419
419
  // biquad LP/HP filter
@@ -137,7 +137,7 @@ function closureCompilerStep(filename)
137
137
  fs.copyFileSync(filename, filenameTemp);
138
138
  try
139
139
  {
140
- child_process.execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --language_out=ECMASCRIPT_2021 --warning_level=VERBOSE --jscomp_off=*`);
140
+ child_process.execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --warning_level=VERBOSE --jscomp_off=*`);
141
141
  fs.rmSync(filenameTemp);
142
142
  }
143
143
  catch (e) { handleError(e, 'Failed to run Closure Compiler step!'); }
@@ -190,8 +190,23 @@ function debugSaveDataURL(dataURL, filename)
190
190
  downloadLink.click();
191
191
  }
192
192
 
193
+ /** Show error as full page of red text
194
+ * @memberof Debug */
195
+ function debugShowErrors()
196
+ {
197
+ onunhandledrejection = (event)=>showError(event.reason);
198
+ onerror = (event, source, lineno, colno)=>
199
+ showError(`${event}\n${source}\nLn ${lineno}, Col ${colno}`);
200
+
201
+ const showError = (message)=>
202
+ {
203
+ document.body.style.backgroundColor = '#111';
204
+ document.body.innerHTML = `<pre style=color:#f00;font-size:50px>` + message;
205
+ }
206
+ }
207
+
193
208
  ///////////////////////////////////////////////////////////////////////////////
194
- // Engine debug function (called automatically)
209
+ // Engine debug functions (called automatically)
195
210
 
196
211
  function debugInit()
197
212
  {
@@ -322,7 +337,7 @@ function debugRender()
322
337
  const pos = worldToScreen(p.pos);
323
338
  overlayContext.translate(pos.x|0, pos.y|0);
324
339
  overlayContext.rotate(p.angle);
325
- overlayContext.scale(1, -1);
340
+ overlayContext.scale(1, p.text ? 1 : -1);
326
341
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
327
342
 
328
343
  if (p.text != undefined)
@@ -20,6 +20,7 @@ export {
20
20
  engineObjectsUpdate,
21
21
  engineObjectsDestroy,
22
22
  engineObjectsCallback,
23
+ engineAddPlugin,
23
24
 
24
25
  // Globals
25
26
  debug,
@@ -101,6 +102,7 @@ export {
101
102
  setObjectMaxSpeed,
102
103
  setGravity,
103
104
  setParticleEmitRateScale,
105
+ setTouchInputEnable,
104
106
  setGamepadsEnable,
105
107
  setGamepadDirectionEmulateStick,
106
108
  setInputWASDEmulateDirection,
@@ -186,10 +188,13 @@ export {
186
188
  // WebGL
187
189
  glCanvas,
188
190
  glContext,
189
- glSetTexture,
190
191
  glCompileShader,
192
+ glCopyToContext,
191
193
  glCreateProgram,
192
194
  glCreateTexture,
195
+ glDraw,
196
+ glFlush,
197
+ glSetTexture,
193
198
 
194
199
  // Input
195
200
  keyIsDown,
@@ -249,4 +254,4 @@ export {
249
254
  medalsPreventUnlock,
250
255
  medalsInit,
251
256
  Medal,
252
- };
257
+ };