littlejsengine 1.18.19 → 1.18.22

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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * LittleJS Three.js Plugin
3
+ * - Renders a three.js scene on a canvas behind the LittleJS canvases
4
+ * - The three.js module is passed in by the user, nothing is bundled
5
+ * - Keep canvasClearColor transparent so the 3D scene shows through
6
+ * - Aligned camera mode locks the 3D camera to the LittleJS 2D camera
7
+ * - ThreeJSObject lets LittleJS physics drive a three.js mesh
8
+ * - Call new ThreeJSPlugin(THREE) in gameInit to set up
9
+ * @namespace ThreeJS
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ ///////////////////////////////////////////////////////////////////////////////
15
+
16
+ /** Global ThreeJS plugin object
17
+ * @type {ThreeJSPlugin}
18
+ * @memberof ThreeJS */
19
+ let threeJS;
20
+
21
+ ///////////////////////////////////////////////////////////////////////////////
22
+ /**
23
+ * ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas
24
+ * @example
25
+ * // in gameInit, with three.js loaded by the user
26
+ * new ThreeJSPlugin(THREE);
27
+ * threeJS.scene.add(new THREE.AmbientLight);
28
+ * @memberof ThreeJS
29
+ */
30
+ class ThreeJSPlugin
31
+ {
32
+ /** Set up the three.js rendering layer, call in gameInit
33
+ * @param {Object} THREE - The three.js module, supplied by the user
34
+ * @param {number} [cameraFOV] - Vertical field of view in degrees */
35
+ constructor(THREE, cameraFOV=60)
36
+ {
37
+ ASSERT(!threeJS, 'ThreeJS plugin already initialized');
38
+ threeJS = this;
39
+ if (headlessMode) return;
40
+ ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit');
41
+ ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in');
42
+
43
+ /** @property {Object} - The three.js module passed into the constructor */
44
+ this.THREE = THREE;
45
+ /** @property {Object} - The three.js renderer */
46
+ this.renderer = new THREE.WebGLRenderer({antialias: true});
47
+ /** @property {Object} - The three.js scene, add lights and meshes here */
48
+ this.scene = new THREE.Scene();
49
+ /** @property {Object} - The three.js perspective camera */
50
+ this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3);
51
+ /** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */
52
+ this.cameraAlign2D = true;
53
+
54
+ // insert the canvas below the engine canvases and match the layout
55
+ const threeCanvas = this.renderer.domElement;
56
+ const rootElement = mainCanvas.parentElement;
57
+ rootElement.insertBefore(threeCanvas, rootElement.firstChild);
58
+ threeCanvas.style.cssText = mainCanvas.style.cssText;
59
+
60
+ // render automatically each frame after the engine renders
61
+ engineAddPlugin(undefined, ()=> this.render());
62
+ }
63
+
64
+ /** Position the camera so the z=0 plane exactly matches LittleJS world space,
65
+ * called automatically when cameraAlign2D is set */
66
+ alignCamera2D()
67
+ {
68
+ const halfHeight = mainCanvasSize.y / 2 / cameraScale; // half visible height in world units
69
+ const distance = halfHeight / tan(this.camera.fov/2 * PI/180);
70
+ this.camera.position.set(cameraPos.x, cameraPos.y, distance);
71
+ // reset all axes in case a free camera was used, littlejs angles are clockwise
72
+ this.camera.rotation.set(0, 0, -cameraAngle);
73
+ }
74
+
75
+ /** Sync the canvas layout and render the scene, called automatically each frame */
76
+ render()
77
+ {
78
+ if (!this.renderer) return; // headless mode
79
+
80
+ // keep renderer size and css in sync with the LittleJS canvas
81
+ const threeCanvas = this.renderer.domElement;
82
+ if (threeCanvas.width != mainCanvasSize.x || threeCanvas.height != mainCanvasSize.y)
83
+ {
84
+ this.renderer.setSize(mainCanvasSize.x, mainCanvasSize.y, false);
85
+ this.camera.aspect = mainCanvasSize.x / mainCanvasSize.y;
86
+ this.camera.updateProjectionMatrix();
87
+ }
88
+ if (threeCanvas.style.cssText != mainCanvas.style.cssText)
89
+ threeCanvas.style.cssText = mainCanvas.style.cssText;
90
+
91
+ if (this.cameraAlign2D)
92
+ this.alignCamera2D();
93
+ this.renderer.render(this.scene, this.camera);
94
+ }
95
+ }
96
+
97
+ ///////////////////////////////////////////////////////////////////////////////
98
+ /**
99
+ * ThreeJS Object - EngineObject that drives a three.js mesh
100
+ * - LittleJS physics moves the object and the mesh follows automatically
101
+ * - Destroying the object removes the mesh from the scene
102
+ * @extends EngineObject
103
+ * @memberof ThreeJS
104
+ */
105
+ class ThreeJSObject extends EngineObject
106
+ {
107
+ /** Create an engine object that drives a three.js mesh
108
+ * @param {Vector2} [pos] - World space position
109
+ * @param {Vector2} [size] - World space size
110
+ * @param {Object} [mesh] - The three.js object3d to drive
111
+ * @param {number} [z] - Mesh height above the 2D plane */
112
+ constructor(pos, size, mesh, z=0)
113
+ {
114
+ super(pos, size);
115
+ ASSERT(threeJS, 'ThreeJS plugin must be initialized first');
116
+
117
+ /** @property {Object} - The three.js object3d this object drives */
118
+ this.mesh = mesh;
119
+ /** @property {number} - Mesh height above the 2D plane */
120
+ this.z = z;
121
+ if (mesh)
122
+ {
123
+ threeJS.scene.add(mesh);
124
+ this.syncMesh();
125
+ }
126
+ }
127
+
128
+ /** Update the object and sync the mesh to its transform */
129
+ update()
130
+ {
131
+ super.update();
132
+ this.syncMesh();
133
+ }
134
+
135
+ /** Copy this object's transform to the mesh */
136
+ syncMesh()
137
+ {
138
+ if (!this.mesh) return;
139
+ this.mesh.position.set(this.pos.x, this.pos.y, this.z);
140
+ this.mesh.rotation.z = -this.angle; // littlejs angles are clockwise
141
+ }
142
+
143
+ /** The mesh is this object's visual, the default 2D rendering is skipped */
144
+ render() {}
145
+
146
+ /** Destroy this object and remove its mesh from the scene
147
+ * @param {boolean} [immediate] */
148
+ destroy(immediate)
149
+ {
150
+ if (this.destroyed) return;
151
+ // note: frequently destroyed objects should also dispose geometry and material
152
+ this.mesh && threeJS.scene.remove(this.mesh);
153
+ super.destroy(immediate);
154
+ }
155
+ }
@@ -426,7 +426,11 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
426
426
  const callback = (value) =>
427
427
  {
428
428
  let obj = target;
429
- for (const k of parts) obj = obj[k];
429
+ for (const k of parts)
430
+ {
431
+ obj = obj[k];
432
+ ASSERT(obj != null, 'tweenProperty path does not resolve: ' + propertyPath);
433
+ }
430
434
  obj[lastKey] = value;
431
435
  };
432
436
  return new Tween(callback, start, end, duration, options);
@@ -580,7 +580,7 @@ class UISystemPlugin
580
580
  }
581
581
 
582
582
  /** Get other axis navigation direction from gamepad or keyboard
583
- * @return {Vector2} */
583
+ * @return {number} */
584
584
  getNavigationOtherDirection()
585
585
  {
586
586
  if (uiSystem.navigationDirection === 2)
@@ -669,6 +669,7 @@ class UISystemPlugin
669
669
  uiSystem.navigationDirection = savedNavigationDirection;
670
670
  inputClear();
671
671
  }
672
+ return confirmMenu;
672
673
  }
673
674
  }
674
675
 
@@ -1102,7 +1103,7 @@ class UITextInput extends UIObject
1102
1103
  this.onClick();
1103
1104
  }
1104
1105
 
1105
- /** Stop editing the text edited */
1106
+ /** Stop editing the text */
1106
1107
  stopEditing()
1107
1108
  {
1108
1109
  if (!this.isKeyInputObject())
@@ -1265,7 +1266,7 @@ class UICheckbox extends UIObject
1265
1266
  ASSERT(isStringLike(text), 'ui checkbox must be a string');
1266
1267
  ASSERT(isColor(color), 'ui checkbox color must be a color');
1267
1268
 
1268
- /** @property {boolean} - Current percentage value of this slider 0-1 */
1269
+ /** @property {boolean} - Is the checkbox currently checked? */
1269
1270
  this.checked = checked;
1270
1271
  // set properties
1271
1272
  this.text = text;
package/src/engine.js CHANGED
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
32
32
  * @type {string}
33
33
  * @default
34
34
  * @memberof Engine */
35
- const engineVersion = '1.18.19';
35
+ const engineVersion = '1.18.22';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -471,6 +471,11 @@ function engineObjectsUpdate()
471
471
  // get list of solid objects for physics optimization
472
472
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
473
473
 
474
+ // update physics before object update
475
+ for (const o of engineObjects)
476
+ if (!o.parent && !o.destroyed)
477
+ o.updatePhysics();
478
+
474
479
  // recursive object update
475
480
  function updateChildObject(o)
476
481
  {
@@ -486,7 +491,6 @@ function engineObjectsUpdate()
486
491
 
487
492
  // update top level objects
488
493
  o.update();
489
- o.updatePhysics();
490
494
  for (const child of o.children)
491
495
  updateChildObject(child);
492
496
  o.updateTransforms();
@@ -446,7 +446,7 @@ class SoundInstance
446
446
  * @param {number} [rate] - How quickly to speak
447
447
  * @param {number} [pitch] - How much to change the pitch by
448
448
  * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
449
- * @return {SpeechSynthesisUtterance} - The utterance that was spoken
449
+ * @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
450
450
  * @memberof Audio */
451
451
  function speak(text, volume=1, rate=1, pitch=1, language='')
452
452
  {
@@ -499,7 +499,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
499
499
  * @param {number} [pan] - How much to apply stereo panning
500
500
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
501
501
  * @param {number} [sampleRate=44100] - Sample rate for the sound
502
- * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
502
+ * @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
503
503
  * @param {number} [offset] - Offset in seconds to start playback from
504
504
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
505
505
  * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
@@ -12,7 +12,7 @@
12
12
  import fs from 'node:fs';
13
13
  import { execSync } from 'node:child_process';
14
14
  import { fileURLToPath } from 'node:url';
15
- import { dirname, join } from 'node:path';
15
+ import { basename, dirname, join } from 'node:path';
16
16
 
17
17
  const __filename = fileURLToPath(import.meta.url);
18
18
  const __dirname = dirname(__filename);
@@ -46,8 +46,10 @@ const enginePluginFiles =
46
46
  `${PLUGIN_FOLDER}/uiSystem.js`,
47
47
  `${PLUGIN_FOLDER}/box2d.js`,
48
48
  `${PLUGIN_FOLDER}/drawUtilities.js`,
49
+ `${PLUGIN_FOLDER}/textureSheet.js`,
49
50
  `${PLUGIN_FOLDER}/tweenSystem.js`,
50
51
  `${PLUGIN_FOLDER}/pathFinder.js`,
52
+ `${PLUGIN_FOLDER}/threejs.js`,
51
53
  ];
52
54
  const engineExtraFiles =
53
55
  [
@@ -76,7 +78,7 @@ try
76
78
 
77
79
  // copy extra files to build folder
78
80
  for (const file of engineExtraFiles)
79
- fs.copyFileSync(file, `${BUILD_FOLDER}/${file.substring(file.lastIndexOf('/')+1)}`);
81
+ fs.copyFileSync(file, join(BUILD_FOLDER, basename(file)));
80
82
 
81
83
  }
82
84
  catch (e) { handleError(e, 'Failed to create build folder!'); }
@@ -659,6 +659,7 @@ function debugVideoCaptureStart()
659
659
  {
660
660
  LOG('Video capture not supported in this browser!');
661
661
  silentAudioSource?.stop();
662
+ audioStreamDestination && audioMasterGain.disconnect(audioStreamDestination);
662
663
  return;
663
664
  }
664
665
 
@@ -687,6 +688,8 @@ function debugVideoCaptureStop()
687
688
  debugVideoCapture.silentAudioSource?.stop();
688
689
  debugVideoCapture.mediaRecorder?.stop();
689
690
  debugVideoCapture.videoTrack?.stop();
691
+ if (debugVideoCapture.audioStreamDestination)
692
+ audioMasterGain.disconnect(debugVideoCapture.audioStreamDestination);
690
693
  debugVideoCapture = undefined;
691
694
  }
692
695
 
package/src/engineDraw.js CHANGED
@@ -155,8 +155,9 @@ class TileInfo
155
155
  * @param {TextureInfo} [textureInfo] - Texture info to use
156
156
  * @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
157
157
  * @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
158
+ * @param {number} [columns] - How many frames per row for frame(), 0 to keep frames on a single row
158
159
  */
159
- constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
160
+ constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed, columns=0)
160
161
  {
161
162
  /** @property {Vector2} - Top left corner of tile in pixels */
162
163
  this.pos = pos.copy();
@@ -168,6 +169,8 @@ class TileInfo
168
169
  this.textureInfo = textureInfo;
169
170
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
170
171
  this.bleed = bleed;
172
+ /** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
173
+ this.columns = columns;
171
174
  }
172
175
 
173
176
  /** Returns a copy of this tile offset by a vector
@@ -175,9 +178,10 @@ class TileInfo
175
178
  * @return {TileInfo}
176
179
  */
177
180
  offset(offset)
178
- { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
181
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed, this.columns); }
179
182
 
180
183
  /** Returns a copy of this tile offset by a number of animation frames
184
+ * Frames wrap down to the next row if columns is set
181
185
  * @param {number} frame - Offset to apply in animation frames
182
186
  * @return {TileInfo}
183
187
  */
@@ -185,11 +189,33 @@ class TileInfo
185
189
  {
186
190
  ASSERT(typeof frame === 'number');
187
191
  const w = this.size.x + this.padding*2;
188
- const x = frame*w;
189
- ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
190
- return this.offset(new Vector2(x));
192
+ const h = this.size.y + this.padding*2;
193
+ const x = (this.columns ? frame % this.columns : frame) * w;
194
+ const y = (this.columns ? frame / this.columns | 0 : 0) * h;
195
+ ASSERT(this.pos.x + x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
196
+ ASSERT(this.pos.y + y + this.size.y <= this.textureInfo.size.y, 'frame extends beyond texture height!');
197
+ return this.offset(new Vector2(x, y));
191
198
  }
192
199
 
200
+ /** Set how many frames per row this tile uses, so frame() can wrap
201
+ * @param {number} [columns] - Frames per row, 0 to keep frames on a single row
202
+ * @return {TileInfo}
203
+ */
204
+ setColumns(columns=0)
205
+ {
206
+ ASSERT(isNumber(columns) && columns >= 0, 'columns must be a number >= 0');
207
+ this.columns = columns;
208
+ return this;
209
+ }
210
+
211
+ /**
212
+ * Returns a tile info for an index using this tile as reference
213
+ * @param {Vector2|number} [index=0]
214
+ * @return {TileInfo}
215
+ */
216
+ index(index)
217
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed).setColumns(this.columns); }
218
+
193
219
  /**
194
220
  * Set this tile to use a full image in a texture info
195
221
  * @param {TextureInfo} [textureInfo]
@@ -200,17 +226,9 @@ class TileInfo
200
226
  this.textureInfo = textureInfo;
201
227
  this.pos = new Vector2;
202
228
  this.size = textureInfo.size.copy();
203
- this.bleed = this.padding = 0;
229
+ this.bleed = this.padding = this.columns = 0;
204
230
  return this;
205
231
  }
206
-
207
- /**
208
- * Returns a tile info for an index using this tile as reference
209
- * @param {Vector2|number} [index=0]
210
- * @return {TileInfo}
211
- */
212
- tile(index)
213
- { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
214
232
  }
215
233
 
216
234
  /**
@@ -438,7 +438,10 @@ function inputInit()
438
438
  {
439
439
  inputData[0][e.code] = (inputData[0][e.code]&2) | 4;
440
440
  if (inputWASDEmulateDirection)
441
- inputData[0][remapKey(e.code)] = 4;
441
+ {
442
+ const remap = remapKey(e.code);
443
+ inputData[0][remap] = (inputData[0][remap]&2) | 4;
444
+ }
442
445
  }
443
446
  function remapKey(k)
444
447
  {
@@ -513,7 +516,7 @@ function inputInit()
513
516
  document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
514
517
 
515
518
  // handle all touch events the same way
516
- let wasTouching;
519
+ let wasTouching, touchIdentifier;
517
520
  function handleTouch(e)
518
521
  {
519
522
  if (!touchInputEnable) return;
@@ -544,10 +547,11 @@ function inputInit()
544
547
  const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
545
548
  const mousePosScreenLast = mousePosScreen;
546
549
  mousePosScreen = mouseEventToScreen(pos);
547
- if (wasTouching)
550
+ if (wasTouching && gameTouches[0].identifier === touchIdentifier)
548
551
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
549
- else
552
+ else if (!wasTouching)
550
553
  inputData[0][button] = 3;
554
+ touchIdentifier = gameTouches[0].identifier;
551
555
  }
552
556
  else if (wasTouching)
553
557
  inputData[0][button] = inputData[0][button] & 2 | 4;
package/src/engineMath.js CHANGED
@@ -194,7 +194,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
194
194
  * @param {number} value
195
195
  * @return {boolean}
196
196
  * @memberof Math */
197
- function isPowerOfTwo(value) { return !(value & (value - 1)); }
197
+ function isPowerOfTwo(value) { return value > 0 && !(value & (value - 1)); }
198
198
 
199
199
  /** Returns the nearest power of two not less than the value
200
200
  * @param {number} value
@@ -271,7 +271,7 @@ function isIntersecting(start, end, pos, size)
271
271
  * @memberof Math */
272
272
  function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
273
273
  {
274
- const phase = (offset + t*frequency) % 1;
274
+ const phase = mod(offset + t*frequency, 1);
275
275
  let value;
276
276
 
277
277
  if (type === 1) // triangle
@@ -41,8 +41,8 @@
41
41
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
42
42
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
43
43
  * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
44
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
45
- * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
44
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
45
+ * .5, 1 // randomness, collide
46
46
  * );
47
47
  */
48
48
  class ParticleEmitter extends EngineObject
@@ -434,13 +434,12 @@ class Particle
434
434
  const hitLayer = tileCollisionTest(this.pos);
435
435
  if (!testCollision(oldPos))
436
436
  {
437
- // testCollision already invoked collideCallback with the
438
- // correct (this, data, pos) args; no need to re-check here.
439
437
  // test which side we bounced off (or both if a corner)
440
438
  const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
441
439
  const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
442
- const hitRestitution = max(restitution, hitLayer.restitution);
443
- const hitFriction = max(friction, hitLayer.friction);
440
+ // collide callback may hit where the layer test does not, so hitLayer can be undefined
441
+ const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
442
+ const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
444
443
  if (isBlockedX)
445
444
  {
446
445
  // move to previous X position and bounce
@@ -358,6 +358,7 @@ let vibrateEnable = true;
358
358
  let soundEnable = true;
359
359
 
360
360
  /** Volume scale to apply to all sound, music and speech
361
+ * Use setSoundVolume to also update the audio master gain immediately
361
362
  * @type {number}
362
363
  * @default
363
364
  * @memberof Settings */
@@ -31,10 +31,14 @@ function tileCollisionGetData(pos, solidOnly=true)
31
31
  // check all tile collision layers
32
32
  for (const layer of tileCollisionLayers)
33
33
  if (!solidOnly || layer.isSolid)
34
- if (pos.arrayCheck(layer.size))
35
34
  {
36
- const data = layer.getCollisionData(pos);
37
- if (data) return data;
35
+ // convert world pos to layer local space
36
+ const layerPos = pos.subtract(layer.pos);
37
+ if (layerPos.arrayCheck(layer.size))
38
+ {
39
+ const data = layer.getCollisionData(layerPos);
40
+ if (data) return data;
41
+ }
38
42
  }
39
43
  return 0;
40
44
  }
@@ -309,7 +313,7 @@ class TileLayer extends CanvasLayer
309
313
 
310
314
  /** @property {TileInfo} - Default tile info for layer */
311
315
  this.tileInfo = undefined;
312
- /** @property {Array<TileLayerData>} - Default tile info for layer */
316
+ /** @property {Array<TileLayerData>} - Array of tile data for the layer */
313
317
  this.data = [];
314
318
  /** @property {boolean} - Is this layer using a webgl texture? */
315
319
  this.isUsingWebGL = false;
@@ -394,7 +398,7 @@ class TileLayer extends CanvasLayer
394
398
 
395
399
  const size = this.drawSize || this.size;
396
400
  const pos = this.pos.add(size.scale(.5));
397
- this.draw(pos, this.size, this.color, this.angle, this.mirror, this.additiveColor);
401
+ this.draw(pos, size, this.color, this.angle, this.mirror, this.additiveColor);
398
402
  }
399
403
 
400
404
  /** Called after this layer is redrawn, does nothing by default */
@@ -483,7 +487,7 @@ class TileLayer extends CanvasLayer
483
487
  const d = this.getData(layerPos);
484
488
  if (!d || !d.tile) return;
485
489
 
486
- const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
490
+ const tileInfo = this.tileInfo && this.tileInfo.index(d.tile);
487
491
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
488
492
  }
489
493
 
@@ -25,7 +25,7 @@
25
25
  class Timer
26
26
  {
27
27
  /** Create a timer object set time passed in
28
- * @param {number} [timeLeft] - How much time left before the timer
28
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
29
29
  * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
30
30
  constructor(timeLeft, useRealTime=false)
31
31
  {