littlejsengine 1.18.19 → 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.19';
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 */
@@ -3506,6 +3511,14 @@ class TileInfo
3506
3511
  return this.offset(new Vector2(x));
3507
3512
  }
3508
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
+
3509
3522
  /**
3510
3523
  * Set this tile to use a full image in a texture info
3511
3524
  * @param {TextureInfo} [textureInfo]
@@ -3519,14 +3532,6 @@ class TileInfo
3519
3532
  this.bleed = this.padding = 0;
3520
3533
  return this;
3521
3534
  }
3522
-
3523
- /**
3524
- * Returns a tile info for an index using this tile as reference
3525
- * @param {Vector2|number} [index=0]
3526
- * @return {TileInfo}
3527
- */
3528
- tile(index)
3529
- { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3530
3535
  }
3531
3536
 
3532
3537
  /**
@@ -5224,7 +5229,10 @@ function inputInit()
5224
5229
  {
5225
5230
  inputData[0][e.code] = (inputData[0][e.code]&2) | 4;
5226
5231
  if (inputWASDEmulateDirection)
5227
- inputData[0][remapKey(e.code)] = 4;
5232
+ {
5233
+ const remap = remapKey(e.code);
5234
+ inputData[0][remap] = (inputData[0][remap]&2) | 4;
5235
+ }
5228
5236
  }
5229
5237
  function remapKey(k)
5230
5238
  {
@@ -5299,7 +5307,7 @@ function inputInit()
5299
5307
  document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
5300
5308
 
5301
5309
  // handle all touch events the same way
5302
- let wasTouching;
5310
+ let wasTouching, touchIdentifier;
5303
5311
  function handleTouch(e)
5304
5312
  {
5305
5313
  if (!touchInputEnable) return;
@@ -5330,10 +5338,11 @@ function inputInit()
5330
5338
  const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
5331
5339
  const mousePosScreenLast = mousePosScreen;
5332
5340
  mousePosScreen = mouseEventToScreen(pos);
5333
- if (wasTouching)
5341
+ if (wasTouching && gameTouches[0].identifier === touchIdentifier)
5334
5342
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
5335
- else
5343
+ else if (!wasTouching)
5336
5344
  inputData[0][button] = 3;
5345
+ touchIdentifier = gameTouches[0].identifier;
5337
5346
  }
5338
5347
  else if (wasTouching)
5339
5348
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -6491,7 +6500,7 @@ class SoundInstance
6491
6500
  * @param {number} [rate] - How quickly to speak
6492
6501
  * @param {number} [pitch] - How much to change the pitch by
6493
6502
  * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
6494
- * @return {SpeechSynthesisUtterance} - The utterance that was spoken
6503
+ * @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
6495
6504
  * @memberof Audio */
6496
6505
  function speak(text, volume=1, rate=1, pitch=1, language='')
6497
6506
  {
@@ -6544,7 +6553,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
6544
6553
  * @param {number} [pan] - How much to apply stereo panning
6545
6554
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
6546
6555
  * @param {number} [sampleRate=44100] - Sample rate for the sound
6547
- * @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)
6548
6557
  * @param {number} [offset] - Offset in seconds to start playback from
6549
6558
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
6550
6559
  * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
@@ -6786,10 +6795,14 @@ function tileCollisionGetData(pos, solidOnly=true)
6786
6795
  // check all tile collision layers
6787
6796
  for (const layer of tileCollisionLayers)
6788
6797
  if (!solidOnly || layer.isSolid)
6789
- if (pos.arrayCheck(layer.size))
6790
6798
  {
6791
- const data = layer.getCollisionData(pos);
6792
- 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
+ }
6793
6806
  }
6794
6807
  return 0;
6795
6808
  }
@@ -7064,7 +7077,7 @@ class TileLayer extends CanvasLayer
7064
7077
 
7065
7078
  /** @property {TileInfo} - Default tile info for layer */
7066
7079
  this.tileInfo = undefined;
7067
- /** @property {Array<TileLayerData>} - Default tile info for layer */
7080
+ /** @property {Array<TileLayerData>} - Array of tile data for the layer */
7068
7081
  this.data = [];
7069
7082
  /** @property {boolean} - Is this layer using a webgl texture? */
7070
7083
  this.isUsingWebGL = false;
@@ -7149,7 +7162,7 @@ class TileLayer extends CanvasLayer
7149
7162
 
7150
7163
  const size = this.drawSize || this.size;
7151
7164
  const pos = this.pos.add(size.scale(.5));
7152
- 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);
7153
7166
  }
7154
7167
 
7155
7168
  /** Called after this layer is redrawn, does nothing by default */
@@ -7238,7 +7251,7 @@ class TileLayer extends CanvasLayer
7238
7251
  const d = this.getData(layerPos);
7239
7252
  if (!d || !d.tile) return;
7240
7253
 
7241
- const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
7254
+ const tileInfo = this.tileInfo && this.tileInfo.index(d.tile);
7242
7255
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
7243
7256
  }
7244
7257
 
@@ -7532,8 +7545,8 @@ class TileCollisionLayer extends TileLayer
7532
7545
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
7533
7546
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
7534
7547
  * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
7535
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
7536
- * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
7548
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
7549
+ * .5, 1 // randomness, collide
7537
7550
  * );
7538
7551
  */
7539
7552
  class ParticleEmitter extends EngineObject
@@ -7925,13 +7938,12 @@ class Particle
7925
7938
  const hitLayer = tileCollisionTest(this.pos);
7926
7939
  if (!testCollision(oldPos))
7927
7940
  {
7928
- // testCollision already invoked collideCallback with the
7929
- // correct (this, data, pos) args; no need to re-check here.
7930
7941
  // test which side we bounced off (or both if a corner)
7931
7942
  const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
7932
7943
  const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
7933
- const hitRestitution = max(restitution, hitLayer.restitution);
7934
- 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;
7935
7947
  if (isBlockedX)
7936
7948
  {
7937
7949
  // move to previous X position and bounce
@@ -9580,7 +9592,8 @@ class NewgroundsPlugin
9580
9592
  return;
9581
9593
  }
9582
9594
  debugMedals && LOG(xmlHttp.responseText);
9583
- 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); }
9584
9597
  }
9585
9598
  }
9586
9599
  /**
@@ -9599,8 +9612,8 @@ class NewgroundsPlugin
9599
9612
  let postProcess;
9600
9613
 
9601
9614
  /////////////////////////////////////////////////////////////////////////
9602
- /**
9603
- * UI System Global Object
9615
+ /**
9616
+ * Post Process Plugin - Applies a full screen shader to the rendered output
9604
9617
  * @memberof PostProcess
9605
9618
  */
9606
9619
  class PostProcessPlugin
@@ -9714,8 +9727,9 @@ class PostProcessPlugin
9714
9727
  workCanvas.height = mainCanvasSize.y;
9715
9728
  glCopyToContext(workContext);
9716
9729
  workContext.drawImage(mainCanvas, 0, 0);
9717
- mainCanvas.width |= 0
9718
-
9730
+ mainCanvas.width |= 0; // setting size clears the main canvas
9731
+
9732
+
9719
9733
  // copy work canvas to texture
9720
9734
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
9721
9735
  }
@@ -10820,7 +10834,7 @@ class UISystemPlugin
10820
10834
  }
10821
10835
 
10822
10836
  /** Get other axis navigation direction from gamepad or keyboard
10823
- * @return {Vector2} */
10837
+ * @return {number} */
10824
10838
  getNavigationOtherDirection()
10825
10839
  {
10826
10840
  if (uiSystem.navigationDirection === 2)
@@ -10909,6 +10923,7 @@ class UISystemPlugin
10909
10923
  uiSystem.navigationDirection = savedNavigationDirection;
10910
10924
  inputClear();
10911
10925
  }
10926
+ return confirmMenu;
10912
10927
  }
10913
10928
  }
10914
10929
 
@@ -11342,7 +11357,7 @@ class UITextInput extends UIObject
11342
11357
  this.onClick();
11343
11358
  }
11344
11359
 
11345
- /** Stop editing the text edited */
11360
+ /** Stop editing the text */
11346
11361
  stopEditing()
11347
11362
  {
11348
11363
  if (!this.isKeyInputObject())
@@ -11505,7 +11520,7 @@ class UICheckbox extends UIObject
11505
11520
  ASSERT(isStringLike(text), 'ui checkbox must be a string');
11506
11521
  ASSERT(isColor(color), 'ui checkbox color must be a color');
11507
11522
 
11508
- /** @property {boolean} - Current percentage value of this slider 0-1 */
11523
+ /** @property {boolean} - Is the checkbox currently checked? */
11509
11524
  this.checked = checked;
11510
11525
  // set properties
11511
11526
  this.text = text;
@@ -12270,7 +12285,7 @@ class Box2dObject extends EngineObject
12270
12285
  shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
12271
12286
  const f = this.addShape(shape, density, friction, restitution, isSensor);
12272
12287
  fixtures.push(f);
12273
- i < points.length && edgePoints.push(points[i].copy());
12288
+ edgePoints.push(points[i].copy());
12274
12289
  }
12275
12290
  this.edgeLoops.push(edgePoints);
12276
12291
  return fixtures;
@@ -12336,7 +12351,7 @@ class Box2dObject extends EngineObject
12336
12351
  /** Sets the position
12337
12352
  * @param {Vector2} pos */
12338
12353
  setPosition(pos)
12339
- { this.setTransform(pos, this.body.GetAngle()); }
12354
+ { this.setTransform(pos, -this.body.GetAngle()); }
12340
12355
 
12341
12356
  /** Sets the angle
12342
12357
  * @param {number} angle */
@@ -12433,6 +12448,7 @@ class Box2dObject extends EngineObject
12433
12448
  filter.set_categoryBits(categoryBits);
12434
12449
  filter.set_maskBits(0xffff & ~ignoreCategoryBits);
12435
12450
  filter.set_groupIndex(groupIndex);
12451
+ fixture.SetFilterData(filter); // applies and refilters contacts
12436
12452
  });
12437
12453
  }
12438
12454
 
@@ -12968,7 +12984,7 @@ class Box2dRevoluteJoint extends Box2dJoint
12968
12984
  jointDef.set_bodyB(objectB.body);
12969
12985
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
12970
12986
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
12971
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
12987
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
12972
12988
  jointDef.set_collideConnected(collide);
12973
12989
  super(jointDef);
12974
12990
  }
@@ -13115,14 +13131,14 @@ class Box2dPrismaticJoint extends Box2dJoint
13115
13131
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13116
13132
  const localAnchorA = objectA.worldToLocal(anchor);
13117
13133
  const localAnchorB = objectB.worldToLocal(anchor);
13118
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13134
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13119
13135
  const jointDef = new box2d.instance.b2PrismaticJointDef();
13120
13136
  jointDef.set_bodyA(objectA.body);
13121
13137
  jointDef.set_bodyB(objectB.body);
13122
13138
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13123
13139
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13124
13140
  jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
13125
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13141
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13126
13142
  jointDef.set_collideConnected(collide);
13127
13143
  super(jointDef);
13128
13144
  }
@@ -13225,7 +13241,7 @@ class Box2dWheelJoint extends Box2dJoint
13225
13241
  anchor ||= box2d.vec2From(objectB.body.GetPosition());
13226
13242
  const localAnchorA = objectA.worldToLocal(anchor);
13227
13243
  const localAnchorB = objectB.worldToLocal(anchor);
13228
- const localAxisA = objectB.worldToLocalVector(worldAxis);
13244
+ const localAxisA = objectA.worldToLocalVector(worldAxis);
13229
13245
  const jointDef = new box2d.instance.b2WheelJointDef();
13230
13246
  jointDef.set_bodyA(objectA.body);
13231
13247
  jointDef.set_bodyB(objectB.body);
@@ -13325,7 +13341,7 @@ class Box2dWeldJoint extends Box2dJoint
13325
13341
  jointDef.set_bodyB(objectB.body);
13326
13342
  jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
13327
13343
  jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
13328
- jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
13344
+ jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
13329
13345
  jointDef.set_collideConnected(collide);
13330
13346
  super(jointDef);
13331
13347
  }
@@ -13772,7 +13788,7 @@ class Box2dPlugin
13772
13788
  * @param {number} [lineWidth]
13773
13789
  * @param {boolean} [useWebGL=glEnable]
13774
13790
  * @param {CanvasRenderingContext2D} [context] */
13775
- 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)
13776
13792
  {
13777
13793
  const shape = box2d.castShapeObject(fixture.GetShape());
13778
13794
  switch (shape.GetType())
@@ -13782,20 +13798,20 @@ class Box2dPlugin
13782
13798
  let points = [];
13783
13799
  for (let i=shape.GetVertexCount(); i--;)
13784
13800
  points.push(box2d.vec2From(shape.GetVertex(i)));
13785
- drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
13801
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
13786
13802
  break;
13787
13803
  }
13788
13804
  case box2d.instance.b2Shape.e_circle:
13789
13805
  {
13790
13806
  const radius = shape.get_m_radius();
13791
- drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
13807
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
13792
13808
  break;
13793
13809
  }
13794
13810
  case box2d.instance.b2Shape.e_edge:
13795
13811
  {
13796
13812
  const v1 = box2d.vec2From(shape.get_m_vertex1());
13797
13813
  const v2 = box2d.vec2From(shape.get_m_vertex2());
13798
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
13814
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
13799
13815
  break;
13800
13816
  }
13801
13817
  }
@@ -14626,7 +14642,11 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
14626
14642
  const callback = (value) =>
14627
14643
  {
14628
14644
  let obj = target;
14629
- 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
+ }
14630
14650
  obj[lastKey] = value;
14631
14651
  };
14632
14652
  return new Tween(callback, start, end, duration, options);
@@ -15509,3 +15529,157 @@ class PathFinder
15509
15529
  }
15510
15530
  }
15511
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.19",
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
  }