littlejsengine 1.18.12 → 1.18.17

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.
package/src/engineLogo.js CHANGED
@@ -65,7 +65,7 @@ function drawEngineLogo(t)
65
65
  x.closePath();
66
66
  gradient(0, Y, 0, Y+H,C);
67
67
  }
68
- const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
68
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%)`:'#000';
69
69
 
70
70
  // center and fit to screen
71
71
  const alpha = oscillate(1,1,t);
package/src/engineMath.js CHANGED
@@ -62,19 +62,19 @@ const max = Math.max;
62
62
  * @param {number} x
63
63
  * @return {number}
64
64
  * @memberof Math */
65
- const sign = Math.sign;
65
+ const sign = (x) => Math.sign(x);
66
66
 
67
67
  /** Returns hypotenuse of values passed in
68
68
  * @param {...number} values
69
69
  * @return {number}
70
70
  * @memberof Math */
71
- const hypot = Math.hypot;
71
+ const hypot = (...values) => Math.hypot(...values);
72
72
 
73
73
  /** Returns log2 of value passed in
74
74
  * @param {number} x
75
75
  * @return {number}
76
76
  * @memberof Math */
77
- const log2 = Math.log2;
77
+ const log2 = (x) => Math.log2(x);
78
78
 
79
79
  /** Returns sin of value passed in
80
80
  * @param {number} x
@@ -216,7 +216,8 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
216
216
  const dy = (posA.y - posB.y)*2;
217
217
  const sx = sizeA.x + sizeB.x;
218
218
  const sy = sizeA.y + sizeB.y;
219
- return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
219
+ // symmetric so isOverlapping(A,B) === isOverlapping(B,A) at touching edges
220
+ return abs(dx) < sx && abs(dy) < sy;
220
221
  }
221
222
 
222
223
  /** Returns true if a line segment is intersecting an axis aligned box
@@ -305,7 +306,7 @@ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'
305
306
  /**
306
307
  * Check if object is an array
307
308
  * @param {any} a
308
- * @return {boolean}
309
+ * @return {a is Array<any>}
309
310
  * @memberof Math */
310
311
  function isArray(a) { return Array.isArray(a); }
311
312
 
@@ -443,7 +444,13 @@ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length);
443
444
  * @return {Vector2}
444
445
  * @memberof Random */
445
446
  function randInCircle(radius=1, minRadius=0)
446
- { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
447
+ {
448
+ // r is uniform in area ⇒ r² uniform in [minRadius², radius²]
449
+ // (the squared inner bound is what makes minRadius the actual exclusion edge)
450
+ if (radius <= 0) return new Vector2;
451
+ const ratio = clamp(minRadius / radius);
452
+ return randVec2(radius * rand(ratio*ratio, 1)**.5);
453
+ }
447
454
 
448
455
  /** Returns a random color between the two passed in colors, combine components if linear
449
456
  * @param {Color} [colorA=WHITE]
@@ -513,7 +520,14 @@ class RandomGenerator
513
520
  * @param {number} [valueA]
514
521
  * @param {number} [valueB]
515
522
  * @return {number} */
516
- floatSign(valueA=1, valueB=0) { return this.float(valueA, valueB) * this.sign(); }
523
+ floatSign(valueA=1, valueB=0)
524
+ {
525
+ const lo = min(valueA, valueB);
526
+ const hi = max(valueA, valueB);
527
+ const d = hi - lo;
528
+ const e = this.float(d*2);
529
+ return e < d ? lo + e : d - lo - e;
530
+ }
517
531
 
518
532
  /** Returns a random angle between -PI and PI
519
533
  * @return {number} */
@@ -71,7 +71,7 @@ class EngineObject
71
71
  this.color = color.copy();
72
72
  /** @property {Color} - Additive color to apply when rendered */
73
73
  this.additiveColor = undefined;
74
- /** @property {boolean} - Should it flip along y axis when rendered */
74
+ /** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
75
75
  this.mirror = false;
76
76
  /** @property {boolean} - Has object been destroyed? */
77
77
  this.destroyed = false;
@@ -139,10 +139,10 @@ class EngineObject
139
139
  if (pa)
140
140
  {
141
141
  const c = cos(-pa), s = sin(-pa);
142
- this.pos = new Vector2(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
142
+ this.pos.set(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
143
143
  }
144
144
  else
145
- this.pos = new Vector2(lx + pp.x, ly + pp.y);
145
+ this.pos.set(lx + pp.x, ly + pp.y);
146
146
  this.angle = mirror*this.localAngle + pa;
147
147
  }
148
148
 
@@ -157,6 +157,9 @@ class EngineObject
157
157
  // child objects do not have physics
158
158
  ASSERT(!this.parent);
159
159
 
160
+ // bail if a collision callback destroyed us mid-frame
161
+ if (this.destroyed) return;
162
+
160
163
  if (this.clampSpeed)
161
164
  {
162
165
  // limit max speed to prevent missing collisions
@@ -212,6 +215,8 @@ class EngineObject
212
215
 
213
216
  // notify objects of collision and check if should be resolved
214
217
  const collide1 = this.collideWithObject(o);
218
+ // callback may have destroyed us; stop resolving against more objects
219
+ if (this.destroyed) return;
215
220
  const collide2 = o.collideWithObject(this);
216
221
  if (!collide1 || !collide2) continue;
217
222
 
@@ -307,13 +312,17 @@ class EngineObject
307
312
  const restitution = max(this.restitution, hitLayer.restitution);
308
313
  if (isBlockedX)
309
314
  {
310
- // try to move up a tiny bit
315
+ // try to step over a 1-tile bump (direction follows gravity sign
316
+ // so inverted gravity steps down off a ceiling bump instead of up;
317
+ // zero gravity defaults to the normal-gravity step-up direction)
311
318
  const epsilon = 1e-3;
312
- const maxMoveUp = .1;
313
- const y = floor(oldPos.y-this.size.y/2+1) +
314
- this.size.y/2 + epsilon;
315
- const delta = y - this.pos.y;
316
- if (delta < maxMoveUp)
319
+ const maxMove = .1;
320
+ const gravitySign = gravity.y > 0 ? -1 : 1;
321
+ const y = gravitySign > 0 ?
322
+ floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
323
+ ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
324
+ const delta = abs(y - this.pos.y);
325
+ if (delta < maxMove)
317
326
  if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
318
327
  {
319
328
  this.pos.y = y;
@@ -365,6 +374,9 @@ class EngineObject
365
374
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
366
375
  }
367
376
 
377
+ /** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
378
+ renderLight() {}
379
+
368
380
  /** Destroy this object, destroy its children, detach its parent, and mark it for removal
369
381
  * @param {boolean} [immediate] - should attached effects be allowed to die off? */
370
382
  destroy(immediate=false)
@@ -453,6 +465,8 @@ class EngineObject
453
465
  * @return {EngineObject} The child object added */
454
466
  addChild(child, localPos=vec2(), localAngle=0)
455
467
  {
468
+ ASSERT(!this.destroyed, 'cannot add child to destroyed object');
469
+ if (this.destroyed) return child;
456
470
  ASSERT(!child.parent && !this.children.includes(child));
457
471
  ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
458
472
  ASSERT(child !== this, 'cannot add self as child');
@@ -469,10 +483,7 @@ class EngineObject
469
483
  removeChild(child)
470
484
  {
471
485
  ASSERT(child.parent === this && this.children.includes(child));
472
- ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
473
- const index = this.children.indexOf(child);
474
- ASSERT(index >= 0, 'child not found in children array');
475
- index >= 0 && this.children.splice(index, 1);
486
+ this.children.splice(this.children.indexOf(child), 1);
476
487
  child.parent = undefined;
477
488
  }
478
489
 
@@ -62,13 +62,13 @@ class ParticleEmitter extends EngineObject
62
62
  * @param {number} [particleTime] - How long particles live
63
63
  * @param {number} [sizeStart] - How big are particles at start
64
64
  * @param {number} [sizeEnd] - How big are particles at end
65
- * @param {number} [speed] - How fast are particles when spawned
66
- * @param {number} [angleSpeed] - How fast are particles rotating
67
- * @param {number} [damping] - How much to dampen particle speed
68
- * @param {number} [angleDamping] - How much to dampen particle angular speed
65
+ * @param {number} [speed] - How fast are particles when spawned, in world units per frame (at 60fps, so multiply units/sec by 1/60)
66
+ * @param {number} [angleSpeed] - How fast are particles rotating, in radians per frame (at 60fps)
67
+ * @param {number} [damping] - How much to dampen particle speed, per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame)
68
+ * @param {number} [angleDamping] - How much to dampen particle angular speed, per-frame multiplier (1 = no damping)
69
69
  * @param {number} [gravityScale] - How much gravity effect particles
70
70
  * @param {number} [particleConeAngle] - Cone for start particle angle
71
- * @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
71
+ * @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
72
72
  * @param {number} [randomness] - Apply extra randomness percent
73
73
  * @param {boolean} [collideTiles] - Do particles collide against tiles
74
74
  * @param {boolean} [additive] - Should particles use additive blend
@@ -140,19 +140,19 @@ class ParticleEmitter extends EngineObject
140
140
  this.sizeStart = sizeStart;
141
141
  /** @property {number} - How big are particles at end */
142
142
  this.sizeEnd = sizeEnd;
143
- /** @property {number} - How fast are particles when spawned */
143
+ /** @property {number} - Particle speed when spawned, in world units per frame (at 60fps) */
144
144
  this.speed = speed;
145
- /** @property {number} - How fast are particles rotating */
145
+ /** @property {number} - Particle angular speed when spawned, in radians per frame (at 60fps) */
146
146
  this.angleSpeed = angleSpeed;
147
- /** @property {number} - How much to dampen particle speed */
147
+ /** @property {number} - Per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame) */
148
148
  this.damping = damping;
149
- /** @property {number} - How much to dampen particle angular speed */
149
+ /** @property {number} - Per-frame angular velocity multiplier (1 = no damping) */
150
150
  this.angleDamping = angleDamping;
151
151
  /** @property {number} - How much gravity affects particles */
152
152
  this.gravityScale = gravityScale;
153
153
  /** @property {number} - Cone for start particle angle */
154
154
  this.particleConeAngle = particleConeAngle;
155
- /** @property {number} - How quick to fade in particles at start/end in percent of life */
155
+ /** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
156
156
  this.fadeRate = fadeRate;
157
157
  /** @property {number} - Apply extra randomness percent */
158
158
  this.randomness = randomness;
@@ -215,12 +215,16 @@ class ParticleEmitter extends EngineObject
215
215
  else if (this.particles.length === 0)
216
216
  this.destroy(true);
217
217
 
218
- // update and remove destroyed particles
219
- this.particles = this.particles.filter((p)=>
218
+ // update and remove destroyed particles in place to avoid per-frame array allocation
219
+ const particles = this.particles;
220
+ let alive = 0;
221
+ for (let i = 0; i < particles.length; ++i)
220
222
  {
223
+ const p = particles[i];
221
224
  p.update();
222
- return !p.destroyed;
223
- });
225
+ if (!p.destroyed) particles[alive++] = p;
226
+ }
227
+ particles.length = alive;
224
228
 
225
229
  if (debugParticles)
226
230
  {
@@ -430,33 +434,32 @@ class Particle
430
434
  const hitLayer = tileCollisionTest(this.pos);
431
435
  if (!testCollision(oldPos))
432
436
  {
433
- if (!collideCallback || collideCallback?.(this, hitLayer))
437
+ // testCollision already invoked collideCallback with the
438
+ // correct (this, data, pos) args; no need to re-check here.
439
+ // test which side we bounced off (or both if a corner)
440
+ const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
441
+ const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
442
+ const hitRestitution = max(restitution, hitLayer.restitution);
443
+ const hitFriction = max(friction, hitLayer.friction);
444
+ if (isBlockedX)
445
+ {
446
+ // move to previous X position and bounce
447
+ this.pos.x = oldPos.x;
448
+ this.velocity.x *= -hitRestitution;
449
+ this.velocity.y *= hitFriction;
450
+ }
451
+ if (isBlockedY || !isBlockedX)
434
452
  {
435
- // test which side we bounced off (or both if a corner)
436
- const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
437
- const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
438
- const hitRestitution = max(restitution, hitLayer.restitution);
439
- const hitFriction = max(friction, hitLayer.friction);
440
- if (isBlockedX)
441
- {
442
- // move to previous X position and bounce
443
- this.pos.x = oldPos.x;
444
- this.velocity.x *= -hitRestitution;
445
- this.velocity.y *= hitFriction;
446
- }
447
- if (isBlockedY || !isBlockedX)
448
- {
449
- const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
450
- if (wasFalling)
451
- this.groundObject = hitLayer;
452
-
453
- // move to previous Y position and bounce
454
- this.pos.y = oldPos.y;
455
- this.velocity.y *= -hitRestitution;
456
- this.velocity.x *= hitFriction;
457
- }
458
- debugPhysics && debugRect(this.pos, this.size, '#f00');
453
+ const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
454
+ if (wasFalling)
455
+ this.groundObject = hitLayer;
456
+
457
+ // move to previous Y position and bounce
458
+ this.pos.y = oldPos.y;
459
+ this.velocity.y *= -hitRestitution;
460
+ this.velocity.x *= hitFriction;
459
461
  }
462
+ debugPhysics && debugRect(this.pos, this.size, '#f00');
460
463
  }
461
464
  }
462
465
  }
@@ -503,7 +506,7 @@ class Particle
503
506
  {
504
507
  // in local space of emitter
505
508
  const a = emitter.angle;
506
- const c = cos(a), s = sin(a);
509
+ const c = cos(-a), s = sin(-a);
507
510
  pos.set(emitter.pos.x + pos.x*c - pos.y*s,
508
511
  emitter.pos.y + pos.x*s + pos.y*c);
509
512
  angle += a;
@@ -511,8 +514,8 @@ class Particle
511
514
  if (trailScale)
512
515
  {
513
516
  // trail style particles
514
- const velocity = localSpace ?
515
- this.velocity.rotate(-emitter.angle) : this.velocity;
517
+ const velocity = localSpace ?
518
+ this.velocity.rotate(emitter.angle) : this.velocity;
516
519
  const speed = velocity.length();
517
520
  if (speed)
518
521
  {
@@ -18,6 +18,7 @@ const debugPhysics = 0;
18
18
  const debugParticles = 0;
19
19
  const debugRaycast = 0;
20
20
  const debugGamepads = 0;
21
+ const debugSound = 0;
21
22
  const debugPointSize = .5;
22
23
 
23
24
  // debug commands are automatically removed from the final build
@@ -51,7 +51,7 @@ let canvasColorTiles = true;
51
51
 
52
52
  /** Color to clear the canvas to before render, does not clear if alpha is 0
53
53
  * @type {Color}
54
- * @memberof Draw */
54
+ * @memberof Settings */
55
55
  let canvasClearColor = CLEAR_BLACK;
56
56
 
57
57
  /** The max size of the canvas, centered if window is larger
@@ -244,34 +244,78 @@ let touchInputEnable = true;
244
244
  * - Supports left analog stick, 4 face buttons and start button (button 9)
245
245
  * - setTouchGamepadButtonCount(1) to use face buttons as right analog stick
246
246
  * - Analog stick buttons 10 and 11 are also activated when virtual sticks are touched
247
-
247
+ * - Rendered as a full-viewport HTML/SVG overlay, so controls may sit outside the game canvas
248
248
  * @type {boolean}
249
249
  * @default
250
250
  * @memberof Settings */
251
251
  let touchGamepadEnable = false;
252
252
 
253
- /** True if touch gamepad should have start button in the center
254
- * - Prevents activating if overlappng with virtual stick or buttons if they are enabled
253
+ /** True if touches outside the gamepad controls should still drive mouse/touch input
254
+ * - When false (the default), enabling the touch gamepad suppresses touch-to-mouse input entirely
255
+ * - Set true to also pass touches outside the controls through to the game as mouse/touch input
256
+ * - Touches on the gamepad controls never drive the mouse regardless of this setting
257
+ * @type {boolean}
258
+ * @default
259
+ * @memberof Settings */
260
+ let touchGamepadPassthrough = false;
261
+
262
+ /** Size of center button if touch gamepad should have start button in the center
263
+ * - Prevents activating when pressed near virtual stick or face buttons
255
264
  * - When the game is paused, any touch will press the button
256
- * - Set size to enable the center button
265
+ * - Measured in viewport CSS pixels
257
266
  * @type {number}
258
267
  * @default
259
268
  * @memberof Settings */
260
- let touchGamepadCenterButtonSize = 300;
269
+ let touchGamepadCenterButtonSize = 0;
261
270
 
262
- /** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
271
+ /** Number of buttons on the right side of the touch gamepad (0-4), using gamepad buttons 0-3
272
+ * - A count of 1 is a single large button (the size of a stick)
273
+ * - Ignored when touchGamepadRightStick is set (the right side is a stick instead)
263
274
  * @type {number}
264
275
  * @default
265
276
  * @memberof Settings */
266
277
  let touchGamepadButtonCount = 4;
267
278
 
279
+ /** True if the touch gamepad should have a left analog stick (or dpad)
280
+ * - When false, the left side is face buttons (touchGamepadLeftButtonCount) or nothing
281
+ * @type {boolean}
282
+ * @default
283
+ * @memberof Settings */
284
+ let touchGamepadLeftStick = true;
285
+
286
+ /** Number of buttons on the left side of the touch gamepad (0-4), using gamepad buttons 4-7
287
+ * - Only used when touchGamepadLeftStick is false (otherwise the left side is a stick)
288
+ * - A count of 1 is a single large button (the size of a stick)
289
+ * @type {number}
290
+ * @default
291
+ * @memberof Settings */
292
+ let touchGamepadLeftButtonCount = 0;
293
+
294
+ /** True if the touch gamepad right side should be an analog stick (or dpad) instead of face buttons
295
+ * - When set, touchGamepadButtonCount is ignored and the right side is a stick
296
+ * - Uses an analog stick when touchGamepadAnalog is true, otherwise an 8 way dpad
297
+ * @type {boolean}
298
+ * @default
299
+ * @memberof Settings */
300
+ let touchGamepadRightStick = false;
301
+
268
302
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
269
303
  * @type {boolean}
270
304
  * @default
271
305
  * @memberof Settings */
272
306
  let touchGamepadAnalog = true;
273
307
 
274
- /** Size of virtual gamepad for touch devices in pixels
308
+ /** True if touch gamepad directional controls should float to where you press
309
+ * - Only affects analog sticks and dpads, not face buttons
310
+ * - Directional controls re-anchor to where you press within the bottom ~60% of their screen half; the top ~40% passes through to the game
311
+ * - The right side floats only when it acts as the right analog stick (touchGamepadRightStick is set)
312
+ * - A center button (touchGamepadCenterButtonSize) still works since it ignores touches near the sticks
313
+ * @type {boolean}
314
+ * @default
315
+ * @memberof Settings */
316
+ let touchGamepadFloating = false;
317
+
318
+ /** Size of virtual gamepad for touch devices in viewport CSS pixels
275
319
  * @type {number}
276
320
  * @default
277
321
  * @memberof Settings */
@@ -289,6 +333,13 @@ let touchGamepadAlpha = .3;
289
333
  * @memberof Settings */
290
334
  let touchGamepadDisplayTime = 3;
291
335
 
336
+ /** Duration in ms to vibrate when a touch gamepad face button or start button is pressed
337
+ * - Set to 0 to disable, also requires vibrateEnable and hardware support (ignored on iOS)
338
+ * @type {number}
339
+ * @default
340
+ * @memberof Settings */
341
+ let touchGamepadVibration = 0;
342
+
292
343
  /** Allow vibration hardware if it exists
293
344
  * @type {boolean}
294
345
  * @default
@@ -521,6 +572,11 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
521
572
  * @memberof Settings */
522
573
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
523
574
 
575
+ /** Set if touches outside the gamepad controls should still drive mouse/touch input
576
+ * @param {boolean} passthrough
577
+ * @memberof Settings */
578
+ function setTouchGamepadPassthrough(passthrough) { touchGamepadPassthrough = passthrough; }
579
+
524
580
  /** Set if touch gamepad should have start button in the center
525
581
  * - Set size to enable the center button
526
582
  * - When the game is paused, any touch will press the button
@@ -528,16 +584,57 @@ function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
528
584
  * @memberof Settings */
529
585
  function setTouchGamepadCenterButtonSize(size) { touchGamepadCenterButtonSize = size; }
530
586
 
531
- /** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
587
+ /** Set number of buttons on the right side of the touch gamepad (0-4, gamepad buttons 0-3)
588
+ * @param {number} count
589
+ * @memberof Settings */
590
+ function setTouchGamepadButtonCount(count)
591
+ {
592
+ touchGamepadButtonCount = count;
593
+ if (count > 0)
594
+ touchGamepadRightStick = false;
595
+ }
596
+
597
+ /** Set if the touch gamepad should have a left analog stick (or dpad)
598
+ * @param {boolean} enable
599
+ * @memberof Settings */
600
+ function setTouchGamepadLeftStick(enable)
601
+ {
602
+ touchGamepadLeftStick = enable;
603
+ if (enable)
604
+ touchGamepadLeftButtonCount = 0;
605
+ }
606
+
607
+ /** Set number of buttons on the left side of the touch gamepad (0-4, gamepad buttons 4-7)
608
+ * - Only used when touchGamepadLeftStick is false
532
609
  * @param {number} count
533
610
  * @memberof Settings */
534
- function setTouchGamepadButtonCount(count) { touchGamepadButtonCount = count; }
611
+ function setTouchGamepadLeftButtonCount(count)
612
+ {
613
+ touchGamepadLeftButtonCount = count;
614
+ if (count > 0)
615
+ touchGamepadLeftStick = false;
616
+ }
617
+
618
+ /** Set if the touch gamepad right side is an analog stick (or dpad) instead of face buttons
619
+ * @param {boolean} rightStick
620
+ * @memberof Settings */
621
+ function setTouchGamepadRightStick(rightStick)
622
+ {
623
+ touchGamepadRightStick = rightStick;
624
+ if (rightStick)
625
+ touchGamepadButtonCount = 0;
626
+ }
535
627
 
536
628
  /** Set if touch gamepad should be analog stick or 8 way dpad
537
629
  * @param {boolean} analog
538
630
  * @memberof Settings */
539
631
  function setTouchGamepadAnalog(analog) { touchGamepadAnalog = analog; }
540
632
 
633
+ /** Set if touch gamepad directional controls should float to where you press
634
+ * @param {boolean} floating
635
+ * @memberof Settings */
636
+ function setTouchGamepadFloating(floating) { touchGamepadFloating = floating; }
637
+
541
638
  /** Set size of virtual gamepad for touch devices in pixels
542
639
  * @param {number} size
543
640
  * @memberof Settings */
@@ -553,6 +650,11 @@ function setTouchGamepadAlpha(alpha) { touchGamepadAlpha = alpha; }
553
650
  * @memberof Settings */
554
651
  function setTouchGamepadDisplayTime(time) { touchGamepadDisplayTime = time; }
555
652
 
653
+ /** Set duration in ms to vibrate when a touch gamepad face or start button is pressed (0 disables)
654
+ * @param {number} ms
655
+ * @memberof Settings */
656
+ function setTouchGamepadVibration(ms) { touchGamepadVibration = ms; }
657
+
556
658
  /** Set to allow vibration hardware if it exists
557
659
  * @param {boolean} enable
558
660
  * @memberof Settings */