littlejsengine 1.17.1 → 1.17.5

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.
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.17.1';
36
+ const engineVersion = '1.17.5';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -88,8 +88,9 @@ function getPaused() { return paused; }
88
88
  * @memberof Engine */
89
89
  function setPaused(isPaused=true) { paused = isPaused; }
90
90
 
91
- // Frame time tracking
91
+ // Engine internal variables
92
92
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
93
+ let showEngineVersion = true;
93
94
 
94
95
  ///////////////////////////////////////////////////////////////////////////////
95
96
  // plugin hooks
@@ -153,16 +154,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
153
154
  * @example
154
155
  * // Basic engine startup
155
156
  * engineInit(
156
- * () => { LOG('Game initialized!'); }, // gameInit
157
- * () => { updateGameLogic(); }, // gameUpdate
158
- * () => { updateUI(); }, // gameUpdatePost
159
- * () => { drawBackground(); }, // gameRender
160
- * () => { drawHUD(); }, // gameRenderPost
157
+ * ()=> { LOG('Game initialized!'); }, // gameInit
158
+ * ()=> { updateGameLogic(); }, // gameUpdate
159
+ * ()=> { updateUI(); }, // gameUpdatePost
160
+ * ()=> { drawBackground(); }, // gameRender
161
+ * ()=> { drawHUD(); }, // gameRenderPost
161
162
  * ['tiles.png', 'tilesLevel.png'] // images to load
162
163
  * );
163
164
  * @memberof Engine */
164
165
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
165
166
  {
167
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
166
168
  ASSERT(!mainContext, 'engine already initialized');
167
169
  ASSERT(isArray(imageSources), 'pass in images as array');
168
170
 
@@ -431,7 +433,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
431
433
  promises.push(new Promise(resolve =>
432
434
  {
433
435
  let t = 0;
434
- console.log(`${engineName} Engine v${engineVersion}`);
435
436
  updateSplash();
436
437
  function updateSplash()
437
438
  {
@@ -608,7 +609,7 @@ function drawEngineLogo(t)
608
609
  x.fillStyle = C;
609
610
  C ? x.fill() : x.stroke();
610
611
  };
611
- const color = (c=0, l=0) =>
612
+ const color = (c=0, l=0)=>
612
613
  hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
613
614
  const alpha = wave(1,1,t);
614
615
  const p = percent(alpha, .1, .5);
@@ -777,7 +778,7 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
777
778
  /** Asserts if the expression is false, does nothing in release builds
778
779
  * Halts execution if the assert fails and throws an error
779
780
  * @param {boolean} assert
780
- * @param {...Object} [output] - error message output
781
+ * @param {...Object} output - error message output
781
782
  * @memberof Debug */
782
783
  function ASSERT(assert, ...output)
783
784
  {
@@ -787,7 +788,7 @@ function ASSERT(assert, ...output)
787
788
  }
788
789
 
789
790
  /** Log to console if debug is enabled, does nothing in release builds
790
- * @param {...Object} [output] - message output
791
+ * @param {...Object} output - message output
791
792
  * @memberof Debug */
792
793
  function LOG(...output) { console.log(...output); }
793
794
 
@@ -1434,8 +1435,8 @@ function debugProtectConstant(obj)
1434
1435
  props.forEach(prop =>
1435
1436
  {
1436
1437
  Object.defineProperty(obj, prop, {
1437
- get: () => values[prop],
1438
- set: (value) =>
1438
+ get: ()=> values[prop],
1439
+ set: (value)=>
1439
1440
  {
1440
1441
  ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1441
1442
  },
@@ -1463,25 +1464,25 @@ function debugProtectConstant(obj)
1463
1464
  const PI = Math.PI;
1464
1465
 
1465
1466
  /** Returns absolute value of value passed in
1466
- * @param {number} value
1467
+ * @param {number} x
1467
1468
  * @return {number}
1468
1469
  * @memberof Math */
1469
1470
  const abs = Math.abs;
1470
1471
 
1471
1472
  /** Returns floored value of value passed in
1472
- * @param {number} value
1473
+ * @param {number} x
1473
1474
  * @return {number}
1474
1475
  * @memberof Math */
1475
1476
  const floor = Math.floor;
1476
1477
 
1477
1478
  /** Returns ceiled value of value passed in
1478
- * @param {number} value
1479
+ * @param {number} x
1479
1480
  * @return {number}
1480
1481
  * @memberof Math */
1481
1482
  const ceil = Math.ceil;
1482
1483
 
1483
1484
  /** Returns rounded value passed in
1484
- * @param {number} value
1485
+ * @param {number} x
1485
1486
  * @return {number}
1486
1487
  * @memberof Math */
1487
1488
  const round = Math.round;
@@ -1499,7 +1500,7 @@ const min = Math.min;
1499
1500
  const max = Math.max;
1500
1501
 
1501
1502
  /** Returns the sign of value passed in
1502
- * @param {number} value
1503
+ * @param {number} x
1503
1504
  * @return {number}
1504
1505
  * @memberof Math */
1505
1506
  const sign = Math.sign;
@@ -1511,25 +1512,25 @@ const sign = Math.sign;
1511
1512
  const hypot = Math.hypot;
1512
1513
 
1513
1514
  /** Returns log2 of value passed in
1514
- * @param {number} value
1515
+ * @param {number} x
1515
1516
  * @return {number}
1516
1517
  * @memberof Math */
1517
1518
  const log2 = Math.log2;
1518
1519
 
1519
1520
  /** Returns sin of value passed in
1520
- * @param {number} value
1521
+ * @param {number} x
1521
1522
  * @return {number}
1522
1523
  * @memberof Math */
1523
1524
  const sin = Math.sin;
1524
1525
 
1525
1526
  /** Returns cos of value passed in
1526
- * @param {number} value
1527
+ * @param {number} x
1527
1528
  * @return {number}
1528
1529
  * @memberof Math */
1529
1530
  const cos = Math.cos;
1530
1531
 
1531
1532
  /** Returns tan of value passed in
1532
- * @param {number} value
1533
+ * @param {number} x
1533
1534
  * @return {number}
1534
1535
  * @memberof Math */
1535
1536
  const tan = Math.tan;
@@ -3601,7 +3602,7 @@ class EngineObject
3601
3602
  if (this.collideTiles)
3602
3603
  {
3603
3604
  // check collision against tiles
3604
- const hitLayer = tileCollisionTest(this.pos, this.size, this)
3605
+ const hitLayer = tileCollisionTest(this.pos, this.size, this);
3605
3606
  if (hitLayer)
3606
3607
  {
3607
3608
  // if already was stuck in collision, don't do anything
@@ -4105,7 +4106,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
4105
4106
  ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
4106
4107
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4107
4108
 
4108
- const textureInfo = tileInfo && tileInfo.textureInfo;
4109
+ const textureInfo = tileInfo?.textureInfo;
4109
4110
  const bleed = tileInfo?.bleed ?? 0;
4110
4111
  if (useWebGL && glEnable)
4111
4112
  {
@@ -4411,9 +4412,12 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
4411
4412
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
4412
4413
  ASSERT(isNumber(angle), 'angle must be a number');
4413
4414
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4414
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
4415
+ ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
4415
4416
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4416
4417
 
4418
+ // clamp line width to prevent artifacts
4419
+ lineWidth = clamp(lineWidth, 0, Math.min(size.x, size.y));
4420
+
4417
4421
  if (useWebGL && glEnable)
4418
4422
  {
4419
4423
  // draw as a regular polygon
@@ -5379,9 +5383,9 @@ function inputInit()
5379
5383
  function touchInputInit()
5380
5384
  {
5381
5385
  // add non passive touch event listeners
5382
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
5383
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
5384
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
5386
+ document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
5387
+ document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
5388
+ document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
5385
5389
 
5386
5390
  // handle all touch events the same way
5387
5391
  let wasTouching;
@@ -5587,7 +5591,7 @@ function inputUpdate()
5587
5591
  // poll gamepads
5588
5592
  const maxGamepads = 8;
5589
5593
  const gamepads = navigator.getGamepads();
5590
- const gamepadCount = min(maxGamepads, gamepads.length)
5594
+ const gamepadCount = min(maxGamepads, gamepads.length);
5591
5595
  for (let i=0; i<gamepadCount; ++i)
5592
5596
  {
5593
5597
  // get or create gamepad data
@@ -5792,29 +5796,46 @@ function audioInit()
5792
5796
  ///////////////////////////////////////////////////////////////////////////////
5793
5797
 
5794
5798
  /**
5795
- * Sound Object - Stores a sound for later use and can be played positionally
5799
+ * Sound Object - Stores a sound for later
5800
+ * - this can be used to load and play wave, mp3, and ogg files
5801
+ * - it can also create sounds using the ZzFX sound generator
5802
+ * - can attenuate and apply stereo panning to sounds
5803
+ * - sound instance control with pause/resume capability
5796
5804
  *
5797
5805
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5798
5806
  * @memberof Audio
5799
5807
  * @example
5800
- * // create a sound
5808
+ * // load an audio asset file
5809
+ * const sound_example = new Sound('sound.mp3');
5810
+ *
5811
+ * // create a zzfx sound
5801
5812
  * const sound_example = new Sound([.5,.5]);
5802
5813
  *
5803
- * // play the sound
5814
+ * // play a sound
5804
5815
  * sound_example.play();
5805
5816
  */
5806
5817
  class Sound
5807
5818
  {
5808
- /** Create a sound object and cache the zzfx samples for later use
5809
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
5819
+ /**
5820
+ * @callback SoundLoadCallback - Function called when sound is loaded
5821
+ * @param {Sound} sound
5822
+ * @memberof Audio
5823
+ */
5824
+
5825
+ /** Create a sound object and cache the audio for later use
5826
+ * @param {string|Array} [asset] - Filename of audio file or zzfx array
5827
+ * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
5810
5828
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5811
5829
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5830
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5812
5831
  */
5813
- constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
5832
+ constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
5814
5833
  {
5815
5834
  if (!soundEnable || headlessMode) return;
5816
5835
 
5817
- ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5836
+ ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
5837
+ ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
5838
+ ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
5818
5839
  ASSERT(isNumber(range), 'range must be a number');
5819
5840
  ASSERT(isNumber(taper), 'taper must be a number');
5820
5841
 
@@ -5823,23 +5844,35 @@ class Sound
5823
5844
  /** @property {number} - At what percentage of range should it start tapering */
5824
5845
  this.taper = taper;
5825
5846
  /** @property {number} - How much to randomize frequency each time sound plays */
5826
- this.randomness = 0;
5847
+ this.randomness = randomness ?? 0;
5827
5848
  /** @property {number} - Sample rate for this sound */
5828
5849
  this.sampleRate = audioDefaultSampleRate;
5829
5850
  /** @property {number} - Percentage of this sound currently loaded */
5830
5851
  this.loadedPercent = 0;
5852
+ /** @property {SoundLoadCallback} - function to call when sound is loaded */
5853
+ this.onloadCallback = onloadCallback;
5831
5854
 
5832
- // generate zzfx sound now for fast playback
5833
- if (zzfxSound)
5855
+ if (Array.isArray(asset))
5834
5856
  {
5857
+ // generate zzfx sound
5858
+ const zzfxSound = asset;
5859
+
5835
5860
  // remove randomness so it can be applied on playback
5836
- const randomnessIndex = 1, defaultRandomness = .05;
5861
+ const defaultRandomness = randomness ?? .05;
5862
+ const randomnessIndex = 1;
5837
5863
  this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5838
5864
  zzfxSound[randomnessIndex] = 0;
5839
5865
 
5840
5866
  // generate the zzfx samples
5841
5867
  this.sampleChannels = [zzfxG(...zzfxSound)];
5842
5868
  this.loadedPercent = 1;
5869
+ onloadCallback?.(this);
5870
+ }
5871
+ else if (typeof asset === 'string')
5872
+ {
5873
+ // load the audio file
5874
+ const filename = asset;
5875
+ this.loadSound(filename);
5843
5876
  }
5844
5877
  }
5845
5878
 
@@ -5891,7 +5924,7 @@ class Sound
5891
5924
  * @param {number} [volume] - Volume to play the music at
5892
5925
  * @param {boolean} [loop] - Should the music loop?
5893
5926
  * @param {boolean} [paused] - Should the music start paused
5894
- * @return {SoundInstance} - The audio source node
5927
+ * @return {SoundInstance} - The sound instance
5895
5928
  */
5896
5929
  playMusic(volume=1, loop=true, paused=false)
5897
5930
  { return this.play(undefined, volume, 1, 0, loop, paused); }
@@ -5901,7 +5934,7 @@ class Sound
5901
5934
  * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5902
5935
  * @param {Vector2} [pos] - World space position to play the sound if any
5903
5936
  * @param {number} [volume=1] - How much to scale volume by
5904
- * @return {SoundInstance} - The audio source node
5937
+ * @return {SoundInstance} - The sound instance
5905
5938
  */
5906
5939
  playNote(semitoneOffset=0, pos, volume)
5907
5940
  {
@@ -5914,57 +5947,14 @@ class Sound
5914
5947
  * @return {number} - How long the sound is in seconds (undefined if loading)
5915
5948
  */
5916
5949
  getDuration()
5917
- { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
5950
+ { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
5918
5951
 
5919
5952
  /** Check if sound is loaded, for sounds fetched from a url
5920
5953
  * @return {boolean} - True if sound is loaded and ready to play
5921
5954
  */
5922
5955
  isLoaded() { return this.loadedPercent === 1; }
5923
- }
5924
-
5925
- ///////////////////////////////////////////////////////////////////////////////
5926
-
5927
- /**
5928
- * Sound Wave Object - Loads and stores an audio file for later use
5929
- * - this can be used to load and play wave, mp3, and ogg files
5930
- * @extends Sound
5931
- * @memberof Audio
5932
- * @example
5933
- * // load an audio asset file
5934
- * const sound_example = new SoundWave('sound.mp3');
5935
- *
5936
- * // play the sound
5937
- * sound_example.play();
5938
- */
5939
- class SoundWave extends Sound
5940
- {
5941
- /**
5942
- * @callback SoundLoadCallback - Function called when sound is loaded
5943
- * @param {SoundWave} sound
5944
- * @memberof Audio
5945
- */
5946
5956
 
5947
- /** Create a sound object and cache the wave file for later use
5948
- * @param {string} filename - Filename of audio file to load
5949
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
5950
- * @param {number} [range=soundDefaultRange] - World space max range of sound
5951
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5952
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5953
- */
5954
- constructor(filename, randomness=0, range, taper, onloadCallback)
5955
- {
5956
- super(undefined, range, taper);
5957
- if (!soundEnable || headlessMode) return;
5958
- ASSERT(!filename || isString(filename), 'filename must be a string');
5959
- ASSERT(isNumber(randomness), 'randomness must be a number');
5960
-
5961
- /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5962
- this.onloadCallback = onloadCallback;
5963
- this.randomness = randomness;
5964
- filename && this.loadSound(filename);
5965
- }
5966
-
5967
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
5957
+ /** Loads a sound from a URL and decodes it into sample data.
5968
5958
  * @param {string} filename
5969
5959
  * @return {Promise<void>} */
5970
5960
  async loadSound(filename)
@@ -6619,17 +6609,17 @@ class TileLayerData
6619
6609
  class CanvasLayer extends EngineObject
6620
6610
  {
6621
6611
  /** Create a canvas layer object
6622
- * @param {Vector2} [position] - World space position of the layer
6612
+ * @param {Vector2} [pos] - World space position of the layer
6623
6613
  * @param {Vector2} [size] - World space size of the layer
6624
6614
  * @param {number} [angle] - Angle the layer is rotated by
6625
6615
  * @param {number} [renderOrder] - Objects sorted by renderOrder
6626
6616
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
6627
6617
  * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6628
6618
  */
6629
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=glEnable)
6619
+ constructor(pos, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=true)
6630
6620
  {
6631
6621
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
6632
- super(position, size, undefined, angle, WHITE, renderOrder);
6622
+ super(pos, size, undefined, angle, WHITE, renderOrder);
6633
6623
 
6634
6624
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
6635
6625
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
@@ -6733,16 +6723,16 @@ class CanvasLayer extends EngineObject
6733
6723
  class TileLayer extends CanvasLayer
6734
6724
  {
6735
6725
  /** Create a tile layer object
6736
- * @param {Vector2} position - World space position
6726
+ * @param {Vector2} pos - World space position
6737
6727
  * @param {Vector2} size - World space size
6738
6728
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6739
6729
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6730
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6740
6731
  */
6741
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6732
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6742
6733
  {
6743
6734
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6744
- const useWebGL = true;
6745
- super(position, size, 0, renderOrder, canvasSize, useWebGL);
6735
+ super(pos, size, 0, renderOrder, canvasSize, useWebGL);
6746
6736
 
6747
6737
  /** @property {TileInfo} - Default tile info for layer */
6748
6738
  this.tileInfo = undefined;
@@ -6754,15 +6744,15 @@ class TileLayer extends CanvasLayer
6754
6744
  if (headlessMode)
6755
6745
  {
6756
6746
  // disable rendering in headless mode
6757
- this.render = () => {};
6758
- this.redraw = () => {};
6759
- this.redrawStart = () => {};
6760
- this.redrawEnd = () => {};
6761
- this.drawTileData = () => {};
6762
- this.redrawTileData = () => {};
6763
- this.drawLayerTile = () => {};
6764
- this.drawLayerRect = () => {};
6765
- this.clearLayerRect = () => {};
6747
+ this.render = ()=> {};
6748
+ this.redraw = ()=> {};
6749
+ this.redrawStart = ()=> {};
6750
+ this.redrawEnd = ()=> {};
6751
+ this.drawTileData = ()=> {};
6752
+ this.redrawTileData = ()=> {};
6753
+ this.drawLayerTile = ()=> {};
6754
+ this.drawLayerRect = ()=> {};
6755
+ this.clearLayerRect = ()=> {};
6766
6756
  return;
6767
6757
  }
6768
6758
 
@@ -6796,6 +6786,12 @@ class TileLayer extends CanvasLayer
6796
6786
  isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6797
6787
  }
6798
6788
 
6789
+ /** Clear data at a given position in the array
6790
+ * @param {Vector2} layerPos - Local position in array
6791
+ * @param {boolean} [redraw] - Force the tile to redraw if true */
6792
+ clearData(layerPos, redraw=false)
6793
+ { this.setData(layerPos, new TileLayerData, redraw) }
6794
+
6799
6795
  /** Get data at a given position in the array
6800
6796
  * @param {Vector2} layerPos - Local position in array
6801
6797
  * @return {TileLayerData} */
@@ -6837,7 +6833,7 @@ class TileLayer extends CanvasLayer
6837
6833
  for (let x = this.size.x; x--;)
6838
6834
  for (let y = this.size.y; y--;)
6839
6835
  this.drawTileData(vec2(x,y), false);
6840
- this.hasWebGL() && glFlush();
6836
+ this.isUsingWebGL && glFlush();
6841
6837
  this.onRedraw();
6842
6838
  this.redrawEnd();
6843
6839
  }
@@ -6886,14 +6882,13 @@ class TileLayer extends CanvasLayer
6886
6882
  if (!this.context) return;
6887
6883
  ASSERT(drawContext === this.context);
6888
6884
 
6889
- if (glEnable && this.textureInfo.glTexture)
6890
- glSetRenderTarget();
6891
-
6892
6885
  // set stuff back to normal
6886
+ if (this.isUsingWebGL)
6887
+ glSetRenderTarget();
6893
6888
  [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6894
6889
  }
6895
6890
 
6896
- /** Draw the tile at a given position in the tile grid
6891
+ /** Draw the tile at a given position in the tile layer
6897
6892
  * This can be used to clear out tiles when they are destroyed
6898
6893
  * Tiles can also be redrawn if inside a redrawStart/End block
6899
6894
  * @param {Vector2} layerPos
@@ -6917,7 +6912,7 @@ class TileLayer extends CanvasLayer
6917
6912
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6918
6913
  }
6919
6914
 
6920
- /** Draw the tile at a given position in the tile grid
6915
+ /** Draw the tile at a given position in the tile layer
6921
6916
  * This can be used to clear tiles when they are destroyed
6922
6917
  * For better performance use drawTileData inside a redrawStart/End block
6923
6918
  * @param {Vector2} layerPos
@@ -6945,7 +6940,7 @@ class TileLayer extends CanvasLayer
6945
6940
  angle=0, mirror, additiveColor)
6946
6941
  {
6947
6942
  const drawPos = pos.add(size.scale(.5));
6948
- drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor);
6943
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor, this.isUsingWebGL);
6949
6944
  }
6950
6945
 
6951
6946
  /** Clear a rectangle in layer space
@@ -6985,14 +6980,15 @@ class TileLayer extends CanvasLayer
6985
6980
  class TileCollisionLayer extends TileLayer
6986
6981
  {
6987
6982
  /** Create a tile layer object
6988
- * @param {Vector2} position - World space position
6989
- * @param {Vector2} size - World space size
6990
- * @param {TileInfo} [tileInfo] - Tile info for layer
6983
+ * @param {Vector2} pos - World space position
6984
+ * @param {Vector2} size - World space size
6985
+ * @param {TileInfo} [tileInfo] - Tile info for layer
6991
6986
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6987
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6992
6988
  */
6993
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6989
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6994
6990
  {
6995
- super(position, size.floor(), tileInfo, renderOrder);
6991
+ super(pos, size.floor(), tileInfo, renderOrder, useWebGL);
6996
6992
 
6997
6993
  /** @property {Array<number>} - The tile collision grid */
6998
6994
  this.collisionData = [];
@@ -7028,24 +7024,29 @@ class TileCollisionLayer extends TileLayer
7028
7024
  this.collisionData.fill(0);
7029
7025
  }
7030
7026
 
7031
- /** Set tile collision data for a given cell in the grid
7032
- * @param {Vector2} gridPos
7027
+ /** Set tile collision data for a given cell in the layer
7028
+ * @param {Vector2} layerPos
7033
7029
  * @param {number} [data] */
7034
- setCollisionData(gridPos, data=1)
7030
+ setCollisionData(layerPos, data=1)
7035
7031
  {
7036
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
7037
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
7038
- gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
7032
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
7033
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
7034
+ layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
7039
7035
  }
7040
7036
 
7041
- /** Get tile collision data for a given cell in the grid
7042
- * @param {Vector2} gridPos
7037
+ /** Clear tile collision data for a given cell in the layer
7038
+ * @param {Vector2} layerPos */
7039
+ clearCollisionData(layerPos)
7040
+ { this.setCollisionData(layerPos, 0); }
7041
+
7042
+ /** Get tile collision data for a given cell in the layer
7043
+ * @param {Vector2} layerPos
7043
7044
  * @return {number} */
7044
- getCollisionData(gridPos)
7045
+ getCollisionData(layerPos)
7045
7046
  {
7046
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
7047
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
7048
- return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
7047
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
7048
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
7049
+ return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
7049
7050
  }
7050
7051
 
7051
7052
  /** Check if collision with another object should occur
@@ -7963,7 +7964,7 @@ function glPreRender(clear=true)
7963
7964
  p.x, p.y, 0, 1];
7964
7965
 
7965
7966
  // set the same transform matrix for both shaders
7966
- const initUniform = (program, uniform, value) =>
7967
+ const initUniform = (program, uniform, value)=>
7967
7968
  {
7968
7969
  glContext.useProgram(program);
7969
7970
  const location = glContext.getUniformLocation(program, uniform);
@@ -8357,7 +8358,7 @@ function glSetRenderTarget(texture, clear=false)
8357
8358
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
8358
8359
  glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
8359
8360
  glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
8360
- glPreRender(clear);
8361
+ glPreRender(clear);
8361
8362
  }
8362
8363
  else
8363
8364
  {
@@ -8976,7 +8977,7 @@ class ZzFXMusic extends Sound
8976
8977
  /** Play the music that loops by default
8977
8978
  * @param {number} [volume] - Volume to play the music at
8978
8979
  * @param {boolean} [loop] - Should the music loop?
8979
- * @return {AudioBufferSourceNode} - The audio source node
8980
+ * @return {SoundInstance} - The sound instance
8980
8981
  */
8981
8982
  playMusic(volume=1, loop=true)
8982
8983
  { return super.play(undefined, volume, 1, 0, loop); }
@@ -9023,7 +9024,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
9023
9024
  sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
9024
9025
 
9025
9026
  // for each pattern in sequence
9026
- sequence.forEach((patternIndex, sequenceIndex) => {
9027
+ sequence.forEach((patternIndex, sequenceIndex)=> {
9027
9028
  // get pattern for current channel, use empty 1 note pattern if none found
9028
9029
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
9029
9030
 
@@ -9116,7 +9117,7 @@ let uiDebug = 0;
9116
9117
 
9117
9118
  /** Enable UI system debug drawing
9118
9119
  * 0=off, 1=normal, 2=show invisible
9119
- * @param {number|boolean} enable
9120
+ * @param {number|boolean} debugMode
9120
9121
  * @memberof UISystem */
9121
9122
  function uiSetDebug(debugMode)
9122
9123
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
@@ -10317,8 +10318,8 @@ class UIScrollbar extends UIObject
10317
10318
  class UIVideo extends UIObject
10318
10319
  {
10319
10320
  /** Create a video player UI object
10320
- * @param {Vector2} [pos]
10321
- * @param {Vector2} [size]
10321
+ * @param {Vector2} pos
10322
+ * @param {Vector2} size
10322
10323
  * @param {string} src - Video file path or URL
10323
10324
  * @param {boolean} [autoplay=false] - Start playing immediately?
10324
10325
  * @param {boolean} [loop=false] - Loop the video?
@@ -10458,6 +10459,7 @@ class UIVideo extends UIObject
10458
10459
  * - Contact begin and end callbacks
10459
10460
  * - Wraps b2Vec2 type to/from Vector2
10460
10461
  * - Raycasting and querying
10462
+ * - Box2dTileLayer for grid based collision
10461
10463
  * - Every type of joint
10462
10464
  * - Debug physics drawing
10463
10465
  * @namespace Box2D
@@ -10507,21 +10509,24 @@ class Box2dObject extends EngineObject
10507
10509
  bodyDef.set_type(bodyType);
10508
10510
  bodyDef.set_position(box2d.vec2dTo(pos));
10509
10511
  bodyDef.set_angle(-angle);
10512
+
10513
+ /** @property {Object} - The Box2d body */
10510
10514
  this.body = box2d.world.CreateBody(bodyDef);
10511
- this.body.object = this;
10515
+ /** @property {Color} - Line color used for default box2d drawing */
10512
10516
  this.lineColor = BLACK;
10513
- box2d.objects.push(this);
10514
-
10515
- // edge lists and loops for drawing
10517
+ /** @property {Array<Object>} - List of all edges for default box2d drawing */
10516
10518
  this.edgeLists = [];
10519
+ /** @property {Array<Object>} - List of all edge loops for default box2d drawing */
10517
10520
  this.edgeLoops = [];
10521
+
10522
+ this.body.object = this; // link body to this object
10523
+ box2d.objects.push(this); // keep track of all box2d objects
10518
10524
  }
10519
10525
 
10520
10526
  /** Destroy this object and its physics body */
10521
10527
  destroy()
10522
10528
  {
10523
- if (this.destroyed)
10524
- return;
10529
+ if (this.destroyed) return;
10525
10530
 
10526
10531
  // destroy physics body, fixtures, and joints
10527
10532
  ASSERT(this.body, 'Box2dObject has no body to destroy');
@@ -10552,11 +10557,12 @@ class Box2dObject extends EngineObject
10552
10557
  }
10553
10558
 
10554
10559
  /** Draws all this object's fixtures
10555
- * @param {Color} [color]
10556
- * @param {Color} [lineColor]
10557
- * @param {number} [lineWidth]
10560
+ * @param {Color} [color]
10561
+ * @param {Color} [lineColor]
10562
+ * @param {number} [lineWidth]
10563
+ * @param {boolean} [useWebGL=glEnable]
10558
10564
  * @param {CanvasRenderingContext2D} [context] */
10559
- drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
10565
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
10560
10566
  {
10561
10567
  // draw non-edge fixtures
10562
10568
  this.getFixtureList().forEach((fixture)=>
@@ -10564,7 +10570,7 @@ class Box2dObject extends EngineObject
10564
10570
  const shape = box2d.castObjectType(fixture.GetShape());
10565
10571
  if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
10566
10572
  {
10567
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
10573
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, useWebGL, context);
10568
10574
  }
10569
10575
  });
10570
10576
 
@@ -10790,6 +10796,14 @@ class Box2dObject extends EngineObject
10790
10796
  return fixtures;
10791
10797
  }
10792
10798
 
10799
+ /** Destroy a fixture from the body
10800
+ * @param {Object} [fixture] */
10801
+ destroyFixture(fixture) { this.body.DestroyFixture(fixture); }
10802
+
10803
+ /** Destroy all fixture from the body */
10804
+ destroyAllFixtures()
10805
+ { this.getFixtureList().forEach(fixture=>this.destroyFixture(fixture)); }
10806
+
10793
10807
  ///////////////////////////////////////////////////////////////////////////////
10794
10808
  // physics get functions
10795
10809
 
@@ -11022,6 +11036,136 @@ class Box2dObject extends EngineObject
11022
11036
  }
11023
11037
  }
11024
11038
 
11039
+ ///////////////////////////////////////////////////////////////////////////////
11040
+ /**
11041
+ * Box2D Static Object - Box2d with a static physics body
11042
+ * @extends Box2dObject
11043
+ * @memberof Box2D
11044
+ */
11045
+ class Box2dStaticObject extends Box2dObject
11046
+ {
11047
+ /** Create a LittleJS object with Box2d physics
11048
+ * @param {Vector2} [pos]
11049
+ * @param {Vector2} [size]
11050
+ * @param {TileInfo} [tileInfo]
11051
+ * @param {number} [angle]
11052
+ * @param {Color} [color]
11053
+ * @param {number} [renderOrder] */
11054
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11055
+ {
11056
+ const bodyType = box2d.bodyTypeStatic;
11057
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11058
+ }
11059
+ }
11060
+
11061
+ ///////////////////////////////////////////////////////////////////////////////
11062
+ /**
11063
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
11064
+ * @extends Box2dObject
11065
+ * @memberof Box2D
11066
+ */
11067
+ class Box2dKiematicObject extends Box2dObject
11068
+ {
11069
+ /** Create a LittleJS object with Box2d physics
11070
+ * @param {Vector2} [pos]
11071
+ * @param {Vector2} [size]
11072
+ * @param {TileInfo} [tileInfo]
11073
+ * @param {number} [angle]
11074
+ * @param {Color} [color]
11075
+ * @param {number} [renderOrder] */
11076
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11077
+ {
11078
+ const bodyType = box2d.bodyTypeKinematic;
11079
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11080
+ }
11081
+ }
11082
+
11083
+ ///////////////////////////////////////////////////////////////////////////////
11084
+ /**
11085
+ * Box2d Tile Layer
11086
+ * - adds Box2d support to tile layers
11087
+ * - creates static box2d fixtures for solid tiles
11088
+ * @extends Box2dObject
11089
+ * @memberof Box2D
11090
+ */
11091
+ class Box2dTileLayer extends Box2dStaticObject
11092
+ {
11093
+ /** Create a Box2d tile layer object
11094
+ * @param {TileCollisionLayer} tileLayer - Tile layer for this object */
11095
+ constructor(tileLayer)
11096
+ {
11097
+ ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
11098
+ super(tileLayer.pos, tileLayer.size);
11099
+
11100
+ /** @property {TileLayer} - The tile layer */
11101
+ this.tileLayer = tileLayer;
11102
+ this.addChild(tileLayer);
11103
+ }
11104
+
11105
+ render()
11106
+ {
11107
+ // do not render fixtures, tile layer handles rendering
11108
+ }
11109
+
11110
+ /** Create box2d collision fixtures for solid tiles
11111
+ * @param {number} [friction]
11112
+ * @param {number} [restitution] */
11113
+ buildCollision(friction=.2, restitution=0)
11114
+ {
11115
+ // destroy all fixtures and create new ones
11116
+ this.destroyAllFixtures();
11117
+
11118
+ // create box2d object for this layer
11119
+ const size = this.tileLayer.size;
11120
+ this.pos = this.tileLayer.pos.copy();
11121
+ this.size = size.copy();
11122
+
11123
+ // track which tiles have been processed
11124
+ const processed = [];
11125
+ const getIndex = (x, y)=> x + y * size.x;
11126
+ const isSolidUnprocessed = (x, y)=>
11127
+ !processed[getIndex(x, y)] &&
11128
+ this.tileLayer.getCollisionData(vec2(x, y)) > 0;
11129
+
11130
+ // combine tiles into larger boxes
11131
+ for (let x = 0; x < size.x; ++x)
11132
+ for (let y = 0; y < size.y; ++y)
11133
+ {
11134
+ if (!isSolidUnprocessed(x, y)) continue;
11135
+
11136
+ // find max width by scanning right
11137
+ let width = 1, height = 1, canExpand = true;
11138
+ while (isSolidUnprocessed(x + width, y))
11139
+ ++width;
11140
+
11141
+ // find max height by scanning up, ensuring all rows have the same width
11142
+ while (canExpand)
11143
+ {
11144
+ for (let checkX = 0; checkX < width; ++checkX)
11145
+ {
11146
+ if (!isSolidUnprocessed(x + checkX, y + height))
11147
+ {
11148
+ canExpand = false;
11149
+ break;
11150
+ }
11151
+ }
11152
+ if (canExpand)
11153
+ ++height;
11154
+ }
11155
+
11156
+ // mark all tiles in this rectangle as processed
11157
+ for (let rectX = width; rectX--;)
11158
+ for (let rectY = height; rectY--;)
11159
+ processed[getIndex(x + rectX, y + rectY)] = true;
11160
+
11161
+ // create a single fixture for the entire rectangle
11162
+ const shapeSize = vec2(width, height);
11163
+ const offset = vec2(x + width/2, y + height/2);
11164
+ this.addBox(shapeSize, offset, 0, 0, friction, restitution);
11165
+ }
11166
+ }
11167
+ }
11168
+
11025
11169
  ///////////////////////////////////////////////////////////////////////////////
11026
11170
  /**
11027
11171
  * Box2D Raycast Result
@@ -11063,6 +11207,7 @@ class Box2dJoint
11063
11207
  * @param {Object} jointDef */
11064
11208
  constructor(jointDef)
11065
11209
  {
11210
+ /** @property {Object} - The Box2d joint */
11066
11211
  this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
11067
11212
  }
11068
11213
 
@@ -11409,7 +11554,7 @@ class Box2dGearJoint extends Box2dJoint
11409
11554
  * @param {Box2dObject} objectB
11410
11555
  * @param {Box2dJoint} joint1
11411
11556
  * @param {Box2dJoint} joint2
11412
- * @param {ratio} [ratio] */
11557
+ * @param {number} [ratio] */
11413
11558
  constructor(objectA, objectB, joint1, joint2, ratio=1)
11414
11559
  {
11415
11560
  const jointDef = new box2d.instance.b2GearJointDef();
@@ -11551,50 +11696,6 @@ class Box2dPrismaticJoint extends Box2dJoint
11551
11696
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
11552
11697
  }
11553
11698
 
11554
- ///////////////////////////////////////////////////////////////////////////////
11555
- /**
11556
- * Box2D Static Object - Box2d with a static physics body
11557
- * @extends Box2dObject
11558
- * @memberof Box2D
11559
- */
11560
- class Box2dStaticObject extends Box2dObject
11561
- {
11562
- /** Create a LittleJS object with Box2d physics
11563
- * @param {Vector2} [pos]
11564
- * @param {Vector2} [size]
11565
- * @param {TileInfo} [tileInfo]
11566
- * @param {number} [angle]
11567
- * @param {Color} [color]
11568
- * @param {number} [renderOrder] */
11569
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11570
- {
11571
- const bodyType = box2d.bodyTypeStatic;
11572
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11573
- }
11574
- }
11575
-
11576
- ///////////////////////////////////////////////////////////////////////////////
11577
- /**
11578
- * Box2D Kiematic Object - Box2d with a kinematic physics body
11579
- * @extends Box2dObject
11580
- * @memberof Box2D
11581
- */
11582
- class Box2dKiematicObject extends Box2dObject
11583
- {
11584
- /** Create a LittleJS object with Box2d physics
11585
- * @param {Vector2} [pos]
11586
- * @param {Vector2} [size]
11587
- * @param {TileInfo} [tileInfo]
11588
- * @param {number} [angle]
11589
- * @param {Color} [color]
11590
- * @param {number} [renderOrder] */
11591
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11592
- {
11593
- const bodyType = box2d.bodyTypeKinematic;
11594
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11595
- }
11596
- }
11597
-
11598
11699
  ///////////////////////////////////////////////////////////////////////////////
11599
11700
  /**
11600
11701
  * Box2D Wheel Joint
@@ -11955,10 +12056,13 @@ class Box2dPlugin
11955
12056
  {
11956
12057
  ASSERT(!box2d, 'Box2D already initialized');
11957
12058
  box2d = this;
12059
+
12060
+ /** @property {Object} - The Box2d instance */
11958
12061
  this.instance = instance;
12062
+ /** @property {Object} - The Box2d world */
11959
12063
  this.world = new box2d.instance.b2World();
12064
+ /** @property {Array<Box2dObject>} - List of all Box2d objects */
11960
12065
  this.objects = [];
11961
-
11962
12066
  /** @property {number} - Velocity iterations per update*/
11963
12067
  this.velocityIterations = 8;
11964
12068
  /** @property {number} - Position iterations per update*/
@@ -12157,8 +12261,9 @@ class Box2dPlugin
12157
12261
  * @param {Color} [color]
12158
12262
  * @param {Color} [lineColor]
12159
12263
  * @param {number} [lineWidth]
12264
+ * @param {boolean} [useWebGL=glEnable]
12160
12265
  * @param {CanvasRenderingContext2D} [context] */
12161
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
12266
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
12162
12267
  {
12163
12268
  const shape = box2d.castObjectType(fixture.GetShape());
12164
12269
  switch (shape.GetType())
@@ -12168,20 +12273,20 @@ class Box2dPlugin
12168
12273
  let points = [];
12169
12274
  for (let i=shape.GetVertexCount(); i--;)
12170
12275
  points.push(box2d.vec2From(shape.GetVertex(i)));
12171
- drawPoly(points, color, lineWidth, lineColor, pos, angle);
12276
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
12172
12277
  break;
12173
12278
  }
12174
12279
  case box2d.instance.b2Shape.e_circle:
12175
12280
  {
12176
12281
  const radius = shape.get_m_radius();
12177
- drawCircle(pos, radius*2, color, lineWidth, lineColor);
12282
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
12178
12283
  break;
12179
12284
  }
12180
12285
  case box2d.instance.b2Shape.e_edge:
12181
12286
  {
12182
12287
  const v1 = box2d.vec2From(shape.get_m_vertex1());
12183
12288
  const v2 = box2d.vec2From(shape.get_m_vertex2());
12184
- drawLine(v1, v2, lineWidth, lineColor, pos, angle);
12289
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
12185
12290
  break;
12186
12291
  }
12187
12292
  }
@@ -12199,7 +12304,7 @@ class Box2dPlugin
12199
12304
  }
12200
12305
 
12201
12306
  /** converts a box2d vec2 pointer to a Vector2
12202
- * @param {Object} v */
12307
+ * @param {Object} vp */
12203
12308
  vec2FromPointer(vp)
12204
12309
  {
12205
12310
  const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
@@ -12311,7 +12416,7 @@ async function box2dInit()
12311
12416
  const box2dColorPointer = (c)=>
12312
12417
  box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
12313
12418
  const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
12314
- const getPointsList = (vertices, vertexCount) =>
12419
+ const getPointsList = (vertices, vertexCount)=>
12315
12420
  {
12316
12421
  const points = [];
12317
12422
  for (let i=vertexCount; i--;)
@@ -12818,7 +12923,6 @@ export
12818
12923
  audioMasterGain,
12819
12924
  audioDefaultSampleRate,
12820
12925
  Sound,
12821
- SoundWave,
12822
12926
  SoundInstance,
12823
12927
  speak,
12824
12928
  speakStop,