littlejsengine 1.18.18 → 1.18.21

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.
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.18';
38
+ const engineVersion = '1.18.21';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -474,6 +474,11 @@ function engineObjectsUpdate()
474
474
  // get list of solid objects for physics optimization
475
475
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
476
476
 
477
+ // update physics before object update
478
+ for (const o of engineObjects)
479
+ if (!o.parent && !o.destroyed)
480
+ o.updatePhysics();
481
+
477
482
  // recursive object update
478
483
  function updateChildObject(o)
479
484
  {
@@ -489,7 +494,6 @@ function engineObjectsUpdate()
489
494
 
490
495
  // update top level objects
491
496
  o.update();
492
- o.updatePhysics();
493
497
  for (const child of o.children)
494
498
  updateChildObject(child);
495
499
  o.updateTransforms();
@@ -815,7 +819,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
815
819
  * @param {number} value
816
820
  * @return {boolean}
817
821
  * @memberof Math */
818
- function isPowerOfTwo(value) { return !(value & (value - 1)); }
822
+ function isPowerOfTwo(value) { return value > 0 && !(value & (value - 1)); }
819
823
 
820
824
  /** Returns the nearest power of two not less than the value
821
825
  * @param {number} value
@@ -892,7 +896,7 @@ function isIntersecting(start, end, pos, size)
892
896
  * @memberof Math */
893
897
  function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
894
898
  {
895
- const phase = (offset + t*frequency) % 1;
899
+ const phase = mod(offset + t*frequency, 1);
896
900
  let value;
897
901
 
898
902
  if (type === 1) // triangle
@@ -1828,7 +1832,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1828
1832
  class Timer
1829
1833
  {
1830
1834
  /** Create a timer object set time passed in
1831
- * @param {number} [timeLeft] - How much time left before the timer
1835
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
1832
1836
  * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1833
1837
  constructor(timeLeft, useRealTime=false)
1834
1838
  {
@@ -2427,6 +2431,7 @@ let vibrateEnable = true;
2427
2431
  let soundEnable = true;
2428
2432
 
2429
2433
  /** Volume scale to apply to all sound, music and speech
2434
+ * Use setSoundVolume to also update the audio master gain immediately
2430
2435
  * @type {number}
2431
2436
  * @default
2432
2437
  * @memberof Settings */
@@ -2981,8 +2986,6 @@ class EngineObject
2981
2986
 
2982
2987
  // notify objects of collision and check if should be resolved
2983
2988
  const collide1 = this.collideWithObject(o);
2984
- // callback may have destroyed us; stop resolving against more objects
2985
- if (this.destroyed) return;
2986
2989
  const collide2 = o.collideWithObject(this);
2987
2990
  if (!collide1 || !collide2) continue;
2988
2991
 
@@ -3508,6 +3511,14 @@ class TileInfo
3508
3511
  return this.offset(new Vector2(x));
3509
3512
  }
3510
3513
 
3514
+ /**
3515
+ * Returns a tile info for an index using this tile as reference
3516
+ * @param {Vector2|number} [index=0]
3517
+ * @return {TileInfo}
3518
+ */
3519
+ index(index)
3520
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3521
+
3511
3522
  /**
3512
3523
  * Set this tile to use a full image in a texture info
3513
3524
  * @param {TextureInfo} [textureInfo]
@@ -3521,14 +3532,6 @@ class TileInfo
3521
3532
  this.bleed = this.padding = 0;
3522
3533
  return this;
3523
3534
  }
3524
-
3525
- /**
3526
- * Returns a tile info for an index using this tile as reference
3527
- * @param {Vector2|number} [index=0]
3528
- * @return {TileInfo}
3529
- */
3530
- tile(index)
3531
- { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3532
3535
  }
3533
3536
 
3534
3537
  /**
@@ -5226,7 +5229,10 @@ function inputInit()
5226
5229
  {
5227
5230
  inputData[0][e.code] = (inputData[0][e.code]&2) | 4;
5228
5231
  if (inputWASDEmulateDirection)
5229
- inputData[0][remapKey(e.code)] = 4;
5232
+ {
5233
+ const remap = remapKey(e.code);
5234
+ inputData[0][remap] = (inputData[0][remap]&2) | 4;
5235
+ }
5230
5236
  }
5231
5237
  function remapKey(k)
5232
5238
  {
@@ -5301,7 +5307,7 @@ function inputInit()
5301
5307
  document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
5302
5308
 
5303
5309
  // handle all touch events the same way
5304
- let wasTouching;
5310
+ let wasTouching, touchIdentifier;
5305
5311
  function handleTouch(e)
5306
5312
  {
5307
5313
  if (!touchInputEnable) return;
@@ -5332,10 +5338,11 @@ function inputInit()
5332
5338
  const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
5333
5339
  const mousePosScreenLast = mousePosScreen;
5334
5340
  mousePosScreen = mouseEventToScreen(pos);
5335
- if (wasTouching)
5341
+ if (wasTouching && gameTouches[0].identifier === touchIdentifier)
5336
5342
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
5337
- else
5343
+ else if (!wasTouching)
5338
5344
  inputData[0][button] = 3;
5345
+ touchIdentifier = gameTouches[0].identifier;
5339
5346
  }
5340
5347
  else if (wasTouching)
5341
5348
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -6493,7 +6500,7 @@ class SoundInstance
6493
6500
  * @param {number} [rate] - How quickly to speak
6494
6501
  * @param {number} [pitch] - How much to change the pitch by
6495
6502
  * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
6496
- * @return {SpeechSynthesisUtterance} - The utterance that was spoken
6503
+ * @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
6497
6504
  * @memberof Audio */
6498
6505
  function speak(text, volume=1, rate=1, pitch=1, language='')
6499
6506
  {
@@ -6546,7 +6553,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
6546
6553
  * @param {number} [pan] - How much to apply stereo panning
6547
6554
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
6548
6555
  * @param {number} [sampleRate=44100] - Sample rate for the sound
6549
- * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
6556
+ * @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
6550
6557
  * @param {number} [offset] - Offset in seconds to start playback from
6551
6558
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
6552
6559
  * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
@@ -6788,10 +6795,14 @@ function tileCollisionGetData(pos, solidOnly=true)
6788
6795
  // check all tile collision layers
6789
6796
  for (const layer of tileCollisionLayers)
6790
6797
  if (!solidOnly || layer.isSolid)
6791
- if (pos.arrayCheck(layer.size))
6792
6798
  {
6793
- const data = layer.getCollisionData(pos);
6794
- if (data) return data;
6799
+ // convert world pos to layer local space
6800
+ const layerPos = pos.subtract(layer.pos);
6801
+ if (layerPos.arrayCheck(layer.size))
6802
+ {
6803
+ const data = layer.getCollisionData(layerPos);
6804
+ if (data) return data;
6805
+ }
6795
6806
  }
6796
6807
  return 0;
6797
6808
  }
@@ -7066,7 +7077,7 @@ class TileLayer extends CanvasLayer
7066
7077
 
7067
7078
  /** @property {TileInfo} - Default tile info for layer */
7068
7079
  this.tileInfo = undefined;
7069
- /** @property {Array<TileLayerData>} - Default tile info for layer */
7080
+ /** @property {Array<TileLayerData>} - Array of tile data for the layer */
7070
7081
  this.data = [];
7071
7082
  /** @property {boolean} - Is this layer using a webgl texture? */
7072
7083
  this.isUsingWebGL = false;
@@ -7151,7 +7162,7 @@ class TileLayer extends CanvasLayer
7151
7162
 
7152
7163
  const size = this.drawSize || this.size;
7153
7164
  const pos = this.pos.add(size.scale(.5));
7154
- this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
7165
+ this.draw(pos, size, this.color, this.angle, this.mirror, this.additiveColor);
7155
7166
  }
7156
7167
 
7157
7168
  /** Called after this layer is redrawn, does nothing by default */
@@ -7240,7 +7251,7 @@ class TileLayer extends CanvasLayer
7240
7251
  const d = this.getData(layerPos);
7241
7252
  if (!d || !d.tile) return;
7242
7253
 
7243
- const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
7254
+ const tileInfo = this.tileInfo && this.tileInfo.index(d.tile);
7244
7255
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
7245
7256
  }
7246
7257
 
@@ -7534,8 +7545,8 @@ class TileCollisionLayer extends TileLayer
7534
7545
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
7535
7546
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
7536
7547
  * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
7537
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
7538
- * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
7548
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
7549
+ * .5, 1 // randomness, collide
7539
7550
  * );
7540
7551
  */
7541
7552
  class ParticleEmitter extends EngineObject
@@ -7927,13 +7938,12 @@ class Particle
7927
7938
  const hitLayer = tileCollisionTest(this.pos);
7928
7939
  if (!testCollision(oldPos))
7929
7940
  {
7930
- // testCollision already invoked collideCallback with the
7931
- // correct (this, data, pos) args; no need to re-check here.
7932
7941
  // test which side we bounced off (or both if a corner)
7933
7942
  const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
7934
7943
  const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
7935
- const hitRestitution = max(restitution, hitLayer.restitution);
7936
- const hitFriction = max(friction, hitLayer.friction);
7944
+ // collide callback may hit where the layer test does not, so hitLayer can be undefined
7945
+ const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
7946
+ const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
7937
7947
  if (isBlockedX)
7938
7948
  {
7939
7949
  // move to previous X position and bounce
@@ -9582,7 +9592,8 @@ class NewgroundsPlugin
9582
9592
  return;
9583
9593
  }
9584
9594
  debugMedals && LOG(xmlHttp.responseText);
9585
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
9595
+ try { return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); }
9596
+ catch(e) { debugMedals && LOG('newgrounds response is not valid JSON', e); }
9586
9597
  }
9587
9598
  }
9588
9599
  /**
@@ -9601,8 +9612,8 @@ class NewgroundsPlugin
9601
9612
  let postProcess;
9602
9613
 
9603
9614
  /////////////////////////////////////////////////////////////////////////
9604
- /**
9605
- * UI System Global Object
9615
+ /**
9616
+ * Post Process Plugin - Applies a full screen shader to the rendered output
9606
9617
  * @memberof PostProcess
9607
9618
  */
9608
9619
  class PostProcessPlugin
@@ -9716,8 +9727,9 @@ class PostProcessPlugin
9716
9727
  workCanvas.height = mainCanvasSize.y;
9717
9728
  glCopyToContext(workContext);
9718
9729
  workContext.drawImage(mainCanvas, 0, 0);
9719
- mainCanvas.width |= 0
9720
-
9730
+ mainCanvas.width |= 0; // setting size clears the main canvas
9731
+
9732
+
9721
9733
  // copy work canvas to texture
9722
9734
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
9723
9735
  }
@@ -10822,7 +10834,7 @@ class UISystemPlugin
10822
10834
  }
10823
10835
 
10824
10836
  /** Get other axis navigation direction from gamepad or keyboard
10825
- * @return {Vector2} */
10837
+ * @return {number} */
10826
10838
  getNavigationOtherDirection()
10827
10839
  {
10828
10840
  if (uiSystem.navigationDirection === 2)
@@ -10911,6 +10923,7 @@ class UISystemPlugin
10911
10923
  uiSystem.navigationDirection = savedNavigationDirection;
10912
10924
  inputClear();
10913
10925
  }
10926
+ return confirmMenu;
10914
10927
  }
10915
10928
  }
10916
10929
 
@@ -11344,7 +11357,7 @@ class UITextInput extends UIObject
11344
11357
  this.onClick();
11345
11358
  }
11346
11359
 
11347
- /** Stop editing the text edited */
11360
+ /** Stop editing the text */
11348
11361
  stopEditing()
11349
11362
  {
11350
11363
  if (!this.isKeyInputObject())
@@ -11507,7 +11520,7 @@ class UICheckbox extends UIObject
11507
11520
  ASSERT(isStringLike(text), 'ui checkbox must be a string');
11508
11521
  ASSERT(isColor(color), 'ui checkbox color must be a color');
11509
11522
 
11510
- /** @property {boolean} - Current percentage value of this slider 0-1 */
11523
+ /** @property {boolean} - Is the checkbox currently checked? */
11511
11524
  this.checked = checked;
11512
11525
  // set properties
11513
11526
  this.text = text;
@@ -12272,7 +12285,7 @@ class Box2dObject extends EngineObject
12272
12285
  shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
12273
12286
  const f = this.addShape(shape, density, friction, restitution, isSensor);
12274
12287
  fixtures.push(f);
12275
- i < points.length && edgePoints.push(points[i].copy());
12288
+ edgePoints.push(points[i].copy());
12276
12289
  }
12277
12290
  this.edgeLoops.push(edgePoints);
12278
12291
  return fixtures;
@@ -12338,7 +12351,7 @@ class Box2dObject extends EngineObject
12338
12351
  /** Sets the position
12339
12352
  * @param {Vector2} pos */
12340
12353
  setPosition(pos)
12341
- { this.setTransform(pos, this.body.GetAngle()); }
12354
+ { this.setTransform(pos, -this.body.GetAngle()); }
12342
12355
 
12343
12356
  /** Sets the angle
12344
12357
  * @param {number} angle */
@@ -12435,6 +12448,7 @@ class Box2dObject extends EngineObject
12435
12448
  filter.set_categoryBits(categoryBits);
12436
12449
  filter.set_maskBits(0xffff & ~ignoreCategoryBits);
12437
12450
  filter.set_groupIndex(groupIndex);
12451
+ fixture.SetFilterData(filter); // applies and refilters contacts
12438
12452
  });
12439
12453
  }
12440
12454
 
@@ -12970,7 +12984,7 @@ class Box2dRevoluteJoint extends Box2dJoint
12970
12984
  jointDef.set_bodyB(objectB.body);
12971
12985
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
12972
12986
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
12973
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
12987
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
12974
12988
  jointDef.set_collideConnected(collide);
12975
12989
  super(jointDef);
12976
12990
  }
@@ -13117,14 +13131,14 @@ class Box2dPrismaticJoint extends Box2dJoint
13117
13131
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13118
13132
  const localAnchorA = objectA.worldToLocal(anchor);
13119
13133
  const localAnchorB = objectB.worldToLocal(anchor);
13120
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13134
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13121
13135
  const jointDef = new box2d.instance.b2PrismaticJointDef();
13122
13136
  jointDef.set_bodyA(objectA.body);
13123
13137
  jointDef.set_bodyB(objectB.body);
13124
13138
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13125
13139
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13126
13140
  jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
13127
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13141
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13128
13142
  jointDef.set_collideConnected(collide);
13129
13143
  super(jointDef);
13130
13144
  }
@@ -13227,7 +13241,7 @@ class Box2dWheelJoint extends Box2dJoint
13227
13241
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13228
13242
  const localAnchorA = objectA.worldToLocal(anchor);
13229
13243
  const localAnchorB = objectB.worldToLocal(anchor);
13230
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13244
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13231
13245
  const jointDef = new box2d.instance.b2WheelJointDef();
13232
13246
  jointDef.set_bodyA(objectA.body);
13233
13247
  jointDef.set_bodyB(objectB.body);
@@ -13327,7 +13341,7 @@ class Box2dWeldJoint extends Box2dJoint
13327
13341
  jointDef.set_bodyB(objectB.body);
13328
13342
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13329
13343
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13330
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13344
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13331
13345
  jointDef.set_collideConnected(collide);
13332
13346
  super(jointDef);
13333
13347
  }
@@ -13774,7 +13788,7 @@ class Box2dPlugin
13774
13788
  * @param {number} [lineWidth]
13775
13789
  * @param {boolean} [useWebGL=glEnable]
13776
13790
  * @param {CanvasRenderingContext2D} [context] */
13777
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
13791
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
13778
13792
  {
13779
13793
  const shape = box2d.castShapeObject(fixture.GetShape());
13780
13794
  switch (shape.GetType())
@@ -13784,20 +13798,20 @@ class Box2dPlugin
13784
13798
  let points = [];
13785
13799
  for (let i=shape.GetVertexCount(); i--;)
13786
13800
  points.push(box2d.vec2From(shape.GetVertex(i)));
13787
- drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
13801
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
13788
13802
  break;
13789
13803
  }
13790
13804
  case box2d.instance.b2Shape.e_circle:
13791
13805
  {
13792
13806
  const radius = shape.get_m_radius();
13793
- drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
13807
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
13794
13808
  break;
13795
13809
  }
13796
13810
  case box2d.instance.b2Shape.e_edge:
13797
13811
  {
13798
13812
  const v1 = box2d.vec2From(shape.get_m_vertex1());
13799
13813
  const v2 = box2d.vec2From(shape.get_m_vertex2());
13800
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
13814
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
13801
13815
  break;
13802
13816
  }
13803
13817
  }
@@ -14628,7 +14642,11 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
14628
14642
  const callback = (value) =>
14629
14643
  {
14630
14644
  let obj = target;
14631
- for (const k of parts) obj = obj[k];
14645
+ for (const k of parts)
14646
+ {
14647
+ obj = obj[k];
14648
+ ASSERT(obj != null, 'tweenProperty path does not resolve: ' + propertyPath);
14649
+ }
14632
14650
  obj[lastKey] = value;
14633
14651
  };
14634
14652
  return new Tween(callback, start, end, duration, options);
@@ -15511,3 +15529,157 @@ class PathFinder
15511
15529
  }
15512
15530
  }
15513
15531
 
15532
+ /**
15533
+ * LittleJS Three.js Plugin
15534
+ * - Renders a three.js scene on a canvas behind the LittleJS canvases
15535
+ * - The three.js module is passed in by the user, nothing is bundled
15536
+ * - Keep canvasClearColor transparent so the 3D scene shows through
15537
+ * - Aligned camera mode locks the 3D camera to the LittleJS 2D camera
15538
+ * - ThreeJSObject lets LittleJS physics drive a three.js mesh
15539
+ * - Call new ThreeJSPlugin(THREE) in gameInit to set up
15540
+ * @namespace ThreeJS
15541
+ */
15542
+
15543
+ ///////////////////////////////////////////////////////////////////////////////
15544
+
15545
+ /** Global ThreeJS plugin object
15546
+ * @type {ThreeJSPlugin}
15547
+ * @memberof ThreeJS */
15548
+ let threeJS;
15549
+
15550
+ ///////////////////////////////////////////////////////////////////////////////
15551
+ /**
15552
+ * ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas
15553
+ * @example
15554
+ * // in gameInit, with three.js loaded by the user
15555
+ * new ThreeJSPlugin(THREE);
15556
+ * threeJS.scene.add(new THREE.AmbientLight);
15557
+ * @memberof ThreeJS
15558
+ */
15559
+ class ThreeJSPlugin
15560
+ {
15561
+ /** Set up the three.js rendering layer, call in gameInit
15562
+ * @param {Object} THREE - The three.js module, supplied by the user
15563
+ * @param {number} [cameraFOV] - Vertical field of view in degrees */
15564
+ constructor(THREE, cameraFOV=60)
15565
+ {
15566
+ ASSERT(!threeJS, 'ThreeJS plugin already initialized');
15567
+ threeJS = this;
15568
+ if (headlessMode) return;
15569
+ ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit');
15570
+ ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in');
15571
+
15572
+ /** @property {Object} - The three.js module passed into the constructor */
15573
+ this.THREE = THREE;
15574
+ /** @property {Object} - The three.js renderer */
15575
+ this.renderer = new THREE.WebGLRenderer({antialias: true});
15576
+ /** @property {Object} - The three.js scene, add lights and meshes here */
15577
+ this.scene = new THREE.Scene();
15578
+ /** @property {Object} - The three.js perspective camera */
15579
+ this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3);
15580
+ /** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */
15581
+ this.cameraAlign2D = true;
15582
+
15583
+ // insert the canvas below the engine canvases and match the layout
15584
+ const threeCanvas = this.renderer.domElement;
15585
+ const rootElement = mainCanvas.parentElement;
15586
+ rootElement.insertBefore(threeCanvas, rootElement.firstChild);
15587
+ threeCanvas.style.cssText = mainCanvas.style.cssText;
15588
+
15589
+ // render automatically each frame after the engine renders
15590
+ engineAddPlugin(undefined, ()=> this.render());
15591
+ }
15592
+
15593
+ /** Position the camera so the z=0 plane exactly matches LittleJS world space,
15594
+ * called automatically when cameraAlign2D is set */
15595
+ alignCamera2D()
15596
+ {
15597
+ const halfHeight = mainCanvasSize.y / 2 / cameraScale; // half visible height in world units
15598
+ const distance = halfHeight / tan(this.camera.fov/2 * PI/180);
15599
+ this.camera.position.set(cameraPos.x, cameraPos.y, distance);
15600
+ // reset all axes in case a free camera was used, littlejs angles are clockwise
15601
+ this.camera.rotation.set(0, 0, -cameraAngle);
15602
+ }
15603
+
15604
+ /** Sync the canvas layout and render the scene, called automatically each frame */
15605
+ render()
15606
+ {
15607
+ if (!this.renderer) return; // headless mode
15608
+
15609
+ // keep renderer size and css in sync with the LittleJS canvas
15610
+ const threeCanvas = this.renderer.domElement;
15611
+ if (threeCanvas.width != mainCanvasSize.x || threeCanvas.height != mainCanvasSize.y)
15612
+ {
15613
+ this.renderer.setSize(mainCanvasSize.x, mainCanvasSize.y, false);
15614
+ this.camera.aspect = mainCanvasSize.x / mainCanvasSize.y;
15615
+ this.camera.updateProjectionMatrix();
15616
+ }
15617
+ if (threeCanvas.style.cssText != mainCanvas.style.cssText)
15618
+ threeCanvas.style.cssText = mainCanvas.style.cssText;
15619
+
15620
+ if (this.cameraAlign2D)
15621
+ this.alignCamera2D();
15622
+ this.renderer.render(this.scene, this.camera);
15623
+ }
15624
+ }
15625
+
15626
+ ///////////////////////////////////////////////////////////////////////////////
15627
+ /**
15628
+ * ThreeJS Object - EngineObject that drives a three.js mesh
15629
+ * - LittleJS physics moves the object and the mesh follows automatically
15630
+ * - Destroying the object removes the mesh from the scene
15631
+ * @extends EngineObject
15632
+ * @memberof ThreeJS
15633
+ */
15634
+ class ThreeJSObject extends EngineObject
15635
+ {
15636
+ /** Create an engine object that drives a three.js mesh
15637
+ * @param {Vector2} [pos] - World space position
15638
+ * @param {Vector2} [size] - World space size
15639
+ * @param {Object} [mesh] - The three.js object3d to drive
15640
+ * @param {number} [z] - Mesh height above the 2D plane */
15641
+ constructor(pos, size, mesh, z=0)
15642
+ {
15643
+ super(pos, size);
15644
+ ASSERT(threeJS, 'ThreeJS plugin must be initialized first');
15645
+
15646
+ /** @property {Object} - The three.js object3d this object drives */
15647
+ this.mesh = mesh;
15648
+ /** @property {number} - Mesh height above the 2D plane */
15649
+ this.z = z;
15650
+ if (mesh)
15651
+ {
15652
+ threeJS.scene.add(mesh);
15653
+ this.syncMesh();
15654
+ }
15655
+ }
15656
+
15657
+ /** Update the object and sync the mesh to its transform */
15658
+ update()
15659
+ {
15660
+ super.update();
15661
+ this.syncMesh();
15662
+ }
15663
+
15664
+ /** Copy this object's transform to the mesh */
15665
+ syncMesh()
15666
+ {
15667
+ if (!this.mesh) return;
15668
+ this.mesh.position.set(this.pos.x, this.pos.y, this.z);
15669
+ this.mesh.rotation.z = -this.angle; // littlejs angles are clockwise
15670
+ }
15671
+
15672
+ /** The mesh is this object's visual, the default 2D rendering is skipped */
15673
+ render() {}
15674
+
15675
+ /** Destroy this object and remove its mesh from the scene
15676
+ * @param {boolean} [immediate] */
15677
+ destroy(immediate)
15678
+ {
15679
+ if (this.destroyed) return;
15680
+ // note: frequently destroyed objects should also dispose geometry and material
15681
+ this.mesh && threeJS.scene.remove(this.mesh);
15682
+ super.destroy(immediate);
15683
+ }
15684
+ }
15685
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.18",
3
+ "version": "1.18.21",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
package/plugins/box2d.js CHANGED
@@ -345,7 +345,7 @@ class Box2dObject extends EngineObject
345
345
  shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
346
346
  const f = this.addShape(shape, density, friction, restitution, isSensor);
347
347
  fixtures.push(f);
348
- i < points.length && edgePoints.push(points[i].copy());
348
+ edgePoints.push(points[i].copy());
349
349
  }
350
350
  this.edgeLoops.push(edgePoints);
351
351
  return fixtures;
@@ -411,7 +411,7 @@ class Box2dObject extends EngineObject
411
411
  /** Sets the position
412
412
  * @param {Vector2} pos */
413
413
  setPosition(pos)
414
- { this.setTransform(pos, this.body.GetAngle()); }
414
+ { this.setTransform(pos, -this.body.GetAngle()); }
415
415
 
416
416
  /** Sets the angle
417
417
  * @param {number} angle */
@@ -508,6 +508,7 @@ class Box2dObject extends EngineObject
508
508
  filter.set_categoryBits(categoryBits);
509
509
  filter.set_maskBits(0xffff & ~ignoreCategoryBits);
510
510
  filter.set_groupIndex(groupIndex);
511
+ fixture.SetFilterData(filter); // applies and refilters contacts
511
512
  });
512
513
  }
513
514
 
@@ -1043,7 +1044,7 @@ class Box2dRevoluteJoint extends Box2dJoint
1043
1044
  jointDef.set_bodyB(objectB.body);
1044
1045
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
1045
1046
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
1046
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
1047
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
1047
1048
  jointDef.set_collideConnected(collide);
1048
1049
  super(jointDef);
1049
1050
  }
@@ -1190,14 +1191,14 @@ class Box2dPrismaticJoint extends Box2dJoint
1190
1191
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
1191
1192
  const localAnchorA = objectA.worldToLocal(anchor);
1192
1193
  const localAnchorB = objectB.worldToLocal(anchor);
1193
- const localAxisA = objectB.worldToLocalVector(worldAxis);
1194
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
1194
1195
  const jointDef = new box2d.instance.b2PrismaticJointDef();
1195
1196
  jointDef.set_bodyA(objectA.body);
1196
1197
  jointDef.set_bodyB(objectB.body);
1197
1198
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
1198
1199
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
1199
1200
  jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
1200
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
1201
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
1201
1202
  jointDef.set_collideConnected(collide);
1202
1203
  super(jointDef);
1203
1204
  }
@@ -1300,7 +1301,7 @@ class Box2dWheelJoint extends Box2dJoint
1300
1301
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
1301
1302
  const localAnchorA = objectA.worldToLocal(anchor);
1302
1303
  const localAnchorB = objectB.worldToLocal(anchor);
1303
- const localAxisA = objectB.worldToLocalVector(worldAxis);
1304
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
1304
1305
  const jointDef = new box2d.instance.b2WheelJointDef();
1305
1306
  jointDef.set_bodyA(objectA.body);
1306
1307
  jointDef.set_bodyB(objectB.body);
@@ -1400,7 +1401,7 @@ class Box2dWeldJoint extends Box2dJoint
1400
1401
  jointDef.set_bodyB(objectB.body);
1401
1402
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
1402
1403
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
1403
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
1404
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
1404
1405
  jointDef.set_collideConnected(collide);
1405
1406
  super(jointDef);
1406
1407
  }
@@ -1847,7 +1848,7 @@ class Box2dPlugin
1847
1848
  * @param {number} [lineWidth]
1848
1849
  * @param {boolean} [useWebGL=glEnable]
1849
1850
  * @param {CanvasRenderingContext2D} [context] */
1850
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
1851
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
1851
1852
  {
1852
1853
  const shape = box2d.castShapeObject(fixture.GetShape());
1853
1854
  switch (shape.GetType())
@@ -1857,20 +1858,20 @@ class Box2dPlugin
1857
1858
  let points = [];
1858
1859
  for (let i=shape.GetVertexCount(); i--;)
1859
1860
  points.push(box2d.vec2From(shape.GetVertex(i)));
1860
- drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
1861
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
1861
1862
  break;
1862
1863
  }
1863
1864
  case box2d.instance.b2Shape.e_circle:
1864
1865
  {
1865
1866
  const radius = shape.get_m_radius();
1866
- drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
1867
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
1867
1868
  break;
1868
1869
  }
1869
1870
  case box2d.instance.b2Shape.e_edge:
1870
1871
  {
1871
1872
  const v1 = box2d.vec2From(shape.get_m_vertex1());
1872
1873
  const v2 = box2d.vec2From(shape.get_m_vertex2());
1873
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
1874
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
1874
1875
  break;
1875
1876
  }
1876
1877
  }
@@ -193,6 +193,7 @@ class NewgroundsPlugin
193
193
  return;
194
194
  }
195
195
  debugMedals && LOG(xmlHttp.responseText);
196
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
196
+ try { return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); }
197
+ catch(e) { debugMedals && LOG('newgrounds response is not valid JSON', e); }
197
198
  }
198
199
  }