littlejsengine 1.9.3 → 1.9.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.
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {String}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.9.3';
33
+ const engineVersion = '1.9.5';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {Number}
@@ -97,6 +97,19 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
97
97
  {
98
98
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
99
99
 
100
+ // Called automatically by engine to setup render system
101
+ function enginePreRender()
102
+ {
103
+ // save canvas size
104
+ mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
105
+
106
+ // disable smoothing for pixel art
107
+ mainContext.imageSmoothingEnabled = !canvasPixelated;
108
+
109
+ // setup gl rendering if enabled
110
+ glEnable && glPreRender();
111
+ }
112
+
100
113
  // internal update loop for engine
101
114
  function engineUpdate(frameTimeMS=0)
102
115
  {
@@ -112,7 +125,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
112
125
  timeReal += frameTimeDeltaMS / 1e3;
113
126
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
114
127
  if (!debugSpeedUp)
115
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
128
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
116
129
  updateCanvas();
117
130
 
118
131
  if (paused)
@@ -254,7 +267,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
254
267
  }
255
268
  image.src = src;
256
269
  })
257
- )
270
+ );
258
271
 
259
272
  // draw splash screen
260
273
  showSplashScreen && promises.push(new Promise(resolve =>
@@ -279,19 +292,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
279
292
  });
280
293
  }
281
294
 
282
- // Called automatically by engine to setup render system
283
- function enginePreRender()
284
- {
285
- // save canvas size
286
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
287
-
288
- // disable smoothing for pixel art
289
- mainContext.imageSmoothingEnabled = !canvasPixelated;
290
-
291
- // setup gl rendering if enabled
292
- glEnable && glPreRender();
293
- }
294
-
295
295
  /** Update each engine object, remove destroyed objects, and update time
296
296
  * @memberof Engine */
297
297
  function engineObjectsUpdate()
@@ -325,30 +325,63 @@ function engineObjectsDestroy()
325
325
  engineObjects = engineObjects.filter(o=>!o.destroyed);
326
326
  }
327
327
 
328
- /** Triggers a callback for each object within a given area
329
- * @param {Vector2} [pos] - Center of test area
328
+ /** Collects all object within a given area
329
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
330
330
  * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
331
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
332
331
  * @param {Array} [objects=engineObjects] - List of objects to check
332
+ * @return {Array} - List of collected objects
333
333
  * @memberof Engine */
334
- function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
334
+ function engineObjectsCollect(pos, size, objects=engineObjects)
335
335
  {
336
+ const collectedObjects = [];
336
337
  if (!pos) // all objects
337
338
  {
338
339
  for (const o of objects)
339
- callbackFunction(o);
340
+ collectedObjects.push(o);
340
341
  }
341
- else if (typeof size === 'object') // bounding box test
342
+ else if (size instanceof Vector2) // bounding box test
342
343
  {
343
344
  for (const o of objects)
344
- isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
345
+ isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
345
346
  }
346
347
  else // circle test
347
348
  {
348
349
  const sizeSquared = size*size;
349
350
  for (const o of objects)
350
- pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
351
+ pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
352
+ }
353
+ return collectedObjects;
354
+ }
355
+
356
+ /** Triggers a callback for each object within a given area
357
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
358
+ * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
359
+ * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
360
+ * @param {Array} [objects=engineObjects] - List of objects to check
361
+ * @memberof Engine */
362
+ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
363
+ { engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
364
+
365
+ /** Return a list of objects intersecting a ray
366
+ * @param {Vector2} start
367
+ * @param {Vector2} end
368
+ * @param {Array} [objects=engineObjects] - List of objects to check
369
+ * @return {Array} - List of objects hit
370
+ * @memberof Engine */
371
+ function engineObjectsRaycast(start, end, objects=engineObjects)
372
+ {
373
+ const hitObjects = [];
374
+ for (const o of objects)
375
+ {
376
+ if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
377
+ {
378
+ debugRaycast && debugRect(o.pos, o.size, '#f00');
379
+ hitObjects.push(o);
380
+ }
351
381
  }
382
+
383
+ debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
384
+ return hitObjects;
352
385
  }
353
386
 
354
387
  ///////////////////////////////////////////////////////////////////////////////
@@ -436,16 +469,16 @@ function drawEngineSplashScreen(t)
436
469
  rect(37,14,9,6);
437
470
 
438
471
  // big stack
439
- rect(50,20,10,-10,color(0,1));
440
- rect(50,20,6.5,-10,color(0,2));
441
- rect(50,20,3.5,-10,color(0,3));
442
- rect(50,20,10,-10);
443
- circle(55,2,11.4,.5,PI-.5,color(3,3));
444
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
445
- circle(55,2,11.4,.5,PI-.5);
446
- rect(45,7,20,-7,color(0,2));
447
- rect(45,0,20,3,color(0,3));
448
- rect(45,0,20,7);
472
+ rect(50,20,10,-8,color(0,1))
473
+ rect(50,20,6.5,-8,color(0,2))
474
+ rect(50,20,3.5,-8,color(0,3))
475
+ rect(50,20,10,-8)
476
+ circle(55,2,11.4,.5,PI-.5,color(3,3))
477
+ circle(55,2,11.4,.5,PI/2,color(3,2),1)
478
+ circle(55,2,11.4,.5,PI-.5)
479
+ rect(45,7,20,-7,color(0,2))
480
+ rect(45,-1,20,4,color(0,3))
481
+ rect(45,-1,20,8)
449
482
 
450
483
  // engine
451
484
  for (let i=5; i--;)
@@ -178,7 +178,7 @@ class SoundWave extends Sound
178
178
  * 1, 0, 9, 1 // channel notes
179
179
  * ],
180
180
  * [ // channel 1
181
- * 0, 1, // instrument 1, right speaker
181
+ * 0, 1, // instrument 0, right speaker
182
182
  * 0, 12, 17, -1 // channel notes
183
183
  * ]
184
184
  * ],
@@ -161,10 +161,10 @@ function typeScriptBuildStep(filename)
161
161
  const tsFilename = `${BUILD_FOLDER}/${ENGINE_NAME}.d.ts`
162
162
  child_process.execSync(`npx tsc ${filename} --declaration --allowJs --emitDeclarationOnly --outFile ${tsFilename}`);
163
163
 
164
- // Remove declare module part
165
- //let fileContent = fs.readFileSync(tsFilename, 'utf8');
166
- //const r = new RegExp(`declare module "${ENGINE_NAME}\.esm" \{([\\s\\S]*?)\}`);
167
- //fs.writeFileSync(tsFilename, fileContent.replace(r, '$1'));
164
+ // Make declare module part use the package name "littlejsengine"
165
+ let fileContent = fs.readFileSync(tsFilename, 'utf8');
166
+ fileContent = fileContent.replace(`${ENGINE_NAME}\.esm`, 'littlejsengine')
167
+ fs.writeFileSync(tsFilename, fileContent);
168
168
 
169
169
  }
170
170
  catch (e) { handleError(e, 'Failed to run TypeScript build step!'); }
@@ -260,6 +260,10 @@ function debugRender()
260
260
  {
261
261
  const saveContext = mainContext;
262
262
  mainContext = overlayContext;
263
+
264
+ // draw red rectangle around screen
265
+ const cameraSize = getCameraSize();
266
+ debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
263
267
 
264
268
  // mouse pick
265
269
  let bestDistance = Infinity, bestObject;
@@ -349,7 +353,7 @@ function debugRender()
349
353
  overlayContext.restore();
350
354
  });
351
355
 
352
- // remove expired pritives
356
+ // remove expired primitives
353
357
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
354
358
  }
355
359
 
package/src/engineDraw.js CHANGED
@@ -114,13 +114,23 @@ class TileInfo
114
114
  this.textureIndex = textureIndex;
115
115
  }
116
116
 
117
- /** Returns an offset copy of this tile, useful for animation
117
+ /** Returns a copy of this tile offset by a vector
118
118
  * @param {Vector2} offset - Offset to apply in pixels
119
119
  * @return {TileInfo}
120
120
  */
121
121
  offset(offset)
122
122
  { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex); }
123
123
 
124
+ /** Returns a copy of this tile offset by a number of animation frames
125
+ * @param {Number} frame - Offset to apply in animation frames
126
+ * @return {TileInfo}
127
+ */
128
+ frame(frame)
129
+ {
130
+ ASSERT(typeof frame == 'number');
131
+ return this.offset(vec2(frame*this.size.x, 0));
132
+ }
133
+
124
134
  /** Returns the texture info for this tile
125
135
  * @return {TextureInfo}
126
136
  */
@@ -271,21 +281,6 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
271
281
  drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
272
282
  }
273
283
 
274
- /** Draw colored polygon using passed in points
275
- * @param {Array} points - Array of Vector2 points
276
- * @param {Color} [color=(1,1,1,1)]
277
- * @param {Boolean} [screenSpace=false]
278
- * @param {CanvasRenderingContext2D} [context=mainContext]
279
- * @memberof Draw */
280
- function drawPoly(points, color=new Color, screenSpace, context=mainContext)
281
- {
282
- context.fillStyle = color.toString();
283
- context.beginPath();
284
- for (const point of screenSpace ? points : points.map(worldToScreen))
285
- context.lineTo(point.x, point.y);
286
- context.fill();
287
- }
288
-
289
284
  /** Draw colored line between two points
290
285
  * @param {Vector2} posA
291
286
  * @param {Vector2} posB
@@ -211,9 +211,21 @@ function mouseToScreen(mousePos)
211
211
  ///////////////////////////////////////////////////////////////////////////////
212
212
  // Gamepad input
213
213
 
214
+ // gamepad internal variables
214
215
  const stickData = [];
216
+
217
+ // gamepads are updated by engine every frame automatically
215
218
  function gamepadsUpdate()
216
219
  {
220
+ const applyDeadZones = (v)=>
221
+ {
222
+ const min=.3, max=.8;
223
+ const deadZone = (v)=>
224
+ v > min ? percent( v, min, max) :
225
+ v < -min ? -percent(-v, min, max) : 0;
226
+ return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
227
+ }
228
+
217
229
  // update touch gamepad if enabled
218
230
  if (touchGamepadEnable && isTouchDevice)
219
231
  {
@@ -225,7 +237,16 @@ function gamepadsUpdate()
225
237
  {
226
238
  // read virtual analog stick
227
239
  const sticks = stickData[0] || (stickData[0] = []);
228
- sticks[0] = vec2(touchGamepadStick.x, -touchGamepadStick.y); // flip vertical
240
+ sticks[0] = vec2();
241
+ if (touchGamepadAnalog)
242
+ sticks[0] = applyDeadZones(touchGamepadStick);
243
+ else if (touchGamepadStick.lengthSquared() > .3)
244
+ {
245
+ // convert to 8 way dpad
246
+ sticks[0].x = Math.round(touchGamepadStick.x);
247
+ sticks[0].y = -Math.round(touchGamepadStick.y);
248
+ sticks[0] = sticks[0].clampLength();
249
+ }
229
250
 
230
251
  // read virtual gamepad buttons
231
252
  const data = inputData[1] || (inputData[1] = []);
@@ -237,7 +258,12 @@ function gamepadsUpdate()
237
258
  }
238
259
  }
239
260
 
240
- if (!gamepadsEnable || !navigator || !navigator.getGamepads || !document.hasFocus() && !debug)
261
+ // return if gamepads are disabled or not supported
262
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads)
263
+ return;
264
+
265
+ // only poll gamepads when focused or in debug mode
266
+ if (!debug && !document.hasFocus())
241
267
  return;
242
268
 
243
269
  // poll gamepads
@@ -251,14 +277,9 @@ function gamepadsUpdate()
251
277
 
252
278
  if (gamepad)
253
279
  {
254
- // read clamp dead zone of analog sticks
255
- const deadZone = .3, deadZoneMax = .8, applyDeadZone = (v)=>
256
- v > deadZone ? percent( v, deadZone, deadZoneMax) :
257
- v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
258
-
259
280
  // read analog sticks
260
281
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
261
- sticks[j>>1] = vec2(applyDeadZone(gamepad.axes[j]), applyDeadZone(-gamepad.axes[j+1])).clampLength();
282
+ sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
262
283
 
263
284
  // read buttons
264
285
  for (let j = gamepad.buttons.length; j--;)
@@ -387,14 +408,7 @@ function createTouchGamepad()
387
408
  if (touchPos.distance(stickCenter) < touchGamepadSize)
388
409
  {
389
410
  // virtual analog stick
390
- if (touchGamepadAnalog)
391
- touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
392
- else
393
- {
394
- // 8 way dpad
395
- const angle = touchPos.subtract(stickCenter).angle();
396
- touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
397
- }
411
+ touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
398
412
  }
399
413
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
400
414
  {
@@ -101,6 +101,8 @@ class EngineObject
101
101
  this.collideSolidObjects = false;
102
102
  /** @property {Boolean} - Object collides with and blocks other objects */
103
103
  this.isSolid = false;
104
+ /** @property {Boolean} - Object collides with raycasts */
105
+ this.collideRaycast = false;
104
106
 
105
107
  // add to list of objects
106
108
  engineObjects.push(this);
@@ -305,13 +307,7 @@ class EngineObject
305
307
  * @param {Number} tileData - the value of the tile at the position
306
308
  * @param {Vector2} pos - tile where the collision occured
307
309
  * @return {Boolean} - true if the collision should be resolved */
308
- collideWithTile(tileData, pos) { return tileData > 0; }
309
-
310
- /** Called to check if a tile raycast hit
311
- * @param {Number} tileData - the value of the tile at the position
312
- * @param {Vector2} pos - tile where the raycast is
313
- * @return {Boolean} - true if the raycast should hit */
314
- collideWithTileRaycast(tileData, pos) { return tileData > 0; }
310
+ collideWithTile(tileData, pos) { return tileData > 0; }
315
311
 
316
312
  /** Called to check if a object collision should be resolved
317
313
  * @param {EngineObject} object - the object to test against
@@ -358,16 +354,18 @@ class EngineObject
358
354
  }
359
355
 
360
356
  /** Set how this object collides
361
- * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects
362
- * @param {Boolean} [isSolid] - Does it collide with and block other objects (expensive in large numbers)
363
- * @param {Boolean} [collideTiles] - Does it collide with the tile collision */
364
- setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true)
357
+ * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects?
358
+ * @param {Boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
359
+ * @param {Boolean} [collideTiles] - Does it collide with the tile collision?
360
+ * @param {Boolean} [collideRaycast] - Does it collide with raycasts? */
361
+ setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
365
362
  {
366
363
  ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
367
364
 
368
365
  this.collideSolidObjects = collideSolidObjects;
369
366
  this.isSolid = isSolid;
370
367
  this.collideTiles = collideTiles;
368
+ this.collideRaycast = collideRaycast;
371
369
  }
372
370
 
373
371
  /** Returns string containg info about this object for debugging
@@ -72,7 +72,7 @@ function tileCollisionTest(pos, size=vec2(), object)
72
72
  }
73
73
  }
74
74
 
75
- /** Return the center of tile if any that is hit (does not return the exact intersection)
75
+ /** Return the center of first tile hit (does not return the exact intersection)
76
76
  * @param {Vector2} posStart
77
77
  * @param {Vector2} posEnd
78
78
  * @param {EngineObject} [object]
@@ -268,8 +268,11 @@ class TileLayer extends EngineObject
268
268
  mainCanvas.height = mainCanvasSize.y;
269
269
  }
270
270
 
271
- // begin a new render for the tile canvas
272
- enginePreRender();
271
+ // disable smoothing for pixel art
272
+ this.context.imageSmoothingEnabled = !canvasPixelated;
273
+
274
+ // setup gl rendering if enabled
275
+ glEnable && glPreRender();
273
276
  }
274
277
 
275
278
  /** Call to end the redraw process */
@@ -121,16 +121,57 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
121
121
  function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
122
122
 
123
123
  /** Returns true if two axis aligned bounding boxes are overlapping
124
- * @param {Vector2} pointA - Center of box A
125
- * @param {Vector2} sizeA - Size of box A
126
- * @param {Vector2} pointB - Center of box B
127
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
128
- * @return {Boolean} - True if overlapping
124
+ * @param {Vector2} posA - Center of box A
125
+ * @param {Vector2} sizeA - Size of box A
126
+ * @param {Vector2} posB - Center of box B
127
+ * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
128
+ * @return {Boolean} - True if overlapping
129
129
  * @memberof Utilities */
130
- function isOverlapping(pointA, sizeA, pointB, sizeB=vec2())
130
+ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
131
131
  {
132
- return abs(pointA.x - pointB.x)*2 < sizeA.x + sizeB.x
133
- && abs(pointA.y - pointB.y)*2 < sizeA.y + sizeB.y;
132
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
133
+ && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
134
+ }
135
+
136
+ /** Returns true if a line segment is intersecting an axis aligned box
137
+ * @param {Vector2} start - Start of raycast
138
+ * @param {Vector2} end - End of raycast
139
+ * @param {Vector2} pos - Center of box
140
+ * @param {Vector2} size - Size of box
141
+ * @return {Boolean} - True if intersecting
142
+ * @memberof Utilities */
143
+ function isIntersecting(start, end, pos, size)
144
+ {
145
+ // Liang-Barsky algorithm
146
+ const boxMin = pos.subtract(size.scale(.5));
147
+ const boxMax = boxMin.add(size);
148
+ const delta = end.subtract(start);
149
+ const a = start.subtract(boxMin);
150
+ const b = start.subtract(boxMax);
151
+ const p = [-delta.x, delta.x, -delta.y, delta.y];
152
+ const q = [a.x, -b.x, a.y, -b.y];
153
+ let tMin = 0, tMax = 1;
154
+ for (let i = 4; i--;)
155
+ {
156
+ if (p[i])
157
+ {
158
+ const t = q[i] / p[i];
159
+ if (p[i] < 0)
160
+ {
161
+ if (t > tMax) return false;
162
+ tMin = max(t, tMin);
163
+ }
164
+ else
165
+ {
166
+ if (t < tMin) return false;
167
+ tMax = min(t, tMax);
168
+ }
169
+ }
170
+ else if (q[i] < 0)
171
+ return false;
172
+ }
173
+
174
+ return true;
134
175
  }
135
176
 
136
177
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
@@ -630,7 +671,7 @@ class Color
630
671
 
631
672
  /** Returns this color expressed in hsla format
632
673
  * @return {Array} */
633
- getHSLA()
674
+ HSLA()
634
675
  {
635
676
  const r = clamp(this.r);
636
677
  const g = clamp(this.g);