littlejsengine 1.9.4 → 1.9.6

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.
@@ -31,7 +31,7 @@ class Sound
31
31
  */
32
32
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
33
33
  {
34
- if (!soundEnable) return;
34
+ if (!soundEnable || headlessMode) return;
35
35
 
36
36
  /** @property {Number} - World space max range of sound, will not play if camera is farther away */
37
37
  this.range = range;
@@ -45,8 +45,8 @@ class Sound
45
45
  if (zzfxSound)
46
46
  {
47
47
  // generate zzfx sound now for fast playback
48
- this.randomness = zzfxSound[1] || 0;
49
- zzfxSound[1] = 0; // generate without randomness
48
+ const defaultRandomness = .05;
49
+ this.randomness = zzfxSound[1] || defaultRandomness;
50
50
  this.sampleChannels = [zzfxG(...zzfxSound)];
51
51
  this.sampleRate = zzfxR;
52
52
  }
@@ -62,7 +62,7 @@ class Sound
62
62
  */
63
63
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
64
64
  {
65
- if (!soundEnable || !this.sampleChannels) return;
65
+ if (!soundEnable || !this.sampleChannels || headlessMode) return;
66
66
 
67
67
  let pan;
68
68
  if (pos)
@@ -145,7 +145,9 @@ class SoundWave extends Sound
145
145
  super(undefined, range, taper);
146
146
  this.randomness = randomness;
147
147
 
148
- if (!soundEnable) return;
148
+ if (!soundEnable || headlessMode) return;
149
+ if (!audioContext)
150
+ audioContext = new AudioContext; // create audio context
149
151
 
150
152
  fetch(filename)
151
153
  .then(response => response.arrayBuffer())
@@ -199,7 +201,7 @@ class Music extends Sound
199
201
  {
200
202
  super(undefined);
201
203
 
202
- if (!soundEnable) return;
204
+ if (!soundEnable || headlessMode) return;
203
205
  this.randomness = 0;
204
206
  this.sampleChannels = zzfxM(...zzfxMusic);
205
207
  this.sampleRate = zzfxR;
@@ -222,7 +224,7 @@ class Music extends Sound
222
224
  * @memberof Audio */
223
225
  function playAudioFile(filename, volume=1, loop=false)
224
226
  {
225
- if (!soundEnable) return;
227
+ if (!soundEnable || headlessMode) return;
226
228
 
227
229
  const audio = new Audio(filename);
228
230
  audio.volume = soundVolume * volume;
@@ -241,7 +243,7 @@ function playAudioFile(filename, volume=1, loop=false)
241
243
  * @memberof Audio */
242
244
  function speak(text, language='', volume=1, rate=1, pitch=1)
243
245
  {
244
- if (!soundEnable || !speechSynthesis) return;
246
+ if (!soundEnable || !speechSynthesis || headlessMode) return;
245
247
 
246
248
  // common languages (not supported by all browsers)
247
249
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -274,7 +276,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
274
276
  /** Audio context used by the engine
275
277
  * @type {AudioContext}
276
278
  * @memberof Audio */
277
- let audioContext = new AudioContext;
279
+ let audioContext;
278
280
 
279
281
  /** Keep track if audio was suspended when last sound was played
280
282
  * @type {Boolean}
@@ -292,7 +294,9 @@ let audioSuspended = false;
292
294
  * @memberof Audio */
293
295
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
294
296
  {
295
- if (!soundEnable) return;
297
+ if (!soundEnable || headlessMode) return;
298
+ if (!audioContext)
299
+ audioContext = new AudioContext; // create audio context
296
300
 
297
301
  // prevent sounds from building up if they can't be played
298
302
  const audioWasSuspended = audioSuspended;
@@ -338,7 +342,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
338
342
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
339
343
  * @return {AudioBufferSourceNode} - The audio node of the sound played
340
344
  * @memberof Audio */
341
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
345
+ function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
342
346
 
343
347
  /** Sample rate used for all ZzFX sounds
344
348
  * @default 44100
@@ -347,7 +351,7 @@ const zzfxR = 44100;
347
351
 
348
352
  /** Generate samples for a ZzFX sound
349
353
  * @param {Number} [volume] - Volume scale (percent)
350
- * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
354
+ * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
351
355
  * @param {Number} [frequency] - Frequency of sound (Hz)
352
356
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
353
357
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -373,17 +377,18 @@ const zzfxR = 44100;
373
377
  function zzfxG
374
378
  (
375
379
  // parameters
376
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
380
+ volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
377
381
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
378
382
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
379
383
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
380
384
  )
381
385
  {
386
+ // LJS Note: ZZFX modded so randomness is handled by Sound class
387
+
382
388
  // init parameters
383
389
  let PI2 = PI*2, sampleRate = zzfxR,
384
390
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
385
- startFrequency = frequency *=
386
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
391
+ startFrequency = frequency *= PI2 / sampleRate,
387
392
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
388
393
 
389
394
  // biquad LP/HP filter
@@ -405,7 +410,6 @@ function zzfxG
405
410
  pitchJump *= PI2 / sampleRate;
406
411
  pitchJumpTime *= sampleRate;
407
412
  repeatTime = repeatTime * sampleRate | 0;
408
- volume *= soundVolume;
409
413
 
410
414
  // generate waveform
411
415
  for(length = attack + decay + sustain + release + delay | 0;
@@ -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!'); }
@@ -149,7 +149,7 @@ function debugClear() { debugPrimitives = []; }
149
149
  * @param {String} [filename]
150
150
  * @param {String} [type]
151
151
  * @memberof Debug */
152
- function debugSaveCanvas(canvas, filename=engineName, type='image/png')
152
+ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
153
153
  { debugSaveDataURL(canvas.toDataURL(type), filename); }
154
154
 
155
155
  /** Save a text file to disk
@@ -157,7 +157,7 @@ function debugSaveCanvas(canvas, filename=engineName, type='image/png')
157
157
  * @param {String} [filename]
158
158
  * @param {String} [type]
159
159
  * @memberof Debug */
160
- function debugSaveText(text, filename=engineName, type='text/plain')
160
+ function debugSaveText(text, filename='text', type='text/plain')
161
161
  { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
162
162
 
163
163
  /** Save a data url to disk
@@ -177,8 +177,7 @@ function debugSaveDataURL(dataURL, filename)
177
177
  function debugInit()
178
178
  {
179
179
  // create link for saving screenshots
180
- document.body.appendChild(downloadLink = document.createElement('a'));
181
- downloadLink.style.display = 'none';
180
+ downloadLink = document.createElement('a');
182
181
  }
183
182
 
184
183
  function debugUpdate()
@@ -353,7 +352,7 @@ function debugRender()
353
352
  overlayContext.restore();
354
353
  });
355
354
 
356
- // remove expired pritives
355
+ // remove expired primitives
357
356
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
358
357
  }
359
358
 
package/src/engineDraw.js CHANGED
@@ -74,6 +74,9 @@ let drawCount;
74
74
  */
75
75
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
76
76
  {
77
+ if (headlessMode)
78
+ return new TileInfo;
79
+
77
80
  // if size is a number, make it a vector
78
81
  if (typeof size === 'number')
79
82
  {
@@ -107,9 +110,9 @@ class TileInfo
107
110
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
108
111
  {
109
112
  /** @property {Vector2} - Top left corner of tile in pixels */
110
- this.pos = pos;
113
+ this.pos = pos.copy();
111
114
  /** @property {Vector2} - Size of tile in pixels */
112
- this.size = size;
115
+ this.size = size.copy();
113
116
  /** @property {Number} - Texture index to use */
114
117
  this.textureIndex = textureIndex;
115
118
  }
@@ -201,7 +204,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
201
204
  * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
202
205
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
203
206
  * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
204
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
207
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
205
208
  * @memberof Draw */
206
209
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
207
210
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
@@ -274,7 +277,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
274
277
  * @param {Number} [angle]
275
278
  * @param {Boolean} [useWebGL=glEnable]
276
279
  * @param {Boolean} [screenSpace=false]
277
- * @param {CanvasRenderingContext2D} [context]
280
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
278
281
  * @memberof Draw */
279
282
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
280
283
  {
@@ -288,7 +291,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
288
291
  * @param {Color} [color=(1,1,1,1)]
289
292
  * @param {Boolean} [useWebGL=glEnable]
290
293
  * @param {Boolean} [screenSpace=false]
291
- * @param {CanvasRenderingContext2D} [context]
294
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
292
295
  * @memberof Draw */
293
296
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
294
297
  {
@@ -304,7 +307,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
304
307
  * @param {Boolean} mirror
305
308
  * @param {Function} drawFunction
306
309
  * @param {Boolean} [screenSpace=false]
307
- * @param {CanvasRenderingContext2D} [context=mainContext]
310
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
308
311
  * @memberof Draw */
309
312
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
310
313
  {
@@ -325,7 +328,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
325
328
  /** Enable normal or additive blend mode
326
329
  * @param {Boolean} [additive]
327
330
  * @param {Boolean} [useWebGL=glEnable]
328
- * @param {CanvasRenderingContext2D} [context=mainContext]
331
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
329
332
  * @memberof Draw */
330
333
  function setBlendMode(additive, useWebGL=glEnable, context)
331
334
  {
@@ -350,7 +353,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
350
353
  * @param {Color} [lineColor=(0,0,0,1)]
351
354
  * @param {CanvasTextAlign} [textAlign='center']
352
355
  * @param {String} [font=fontDefault]
353
- * @param {CanvasRenderingContext2D} [context=overlayContext]
356
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
354
357
  * @memberof Draw */
355
358
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
356
359
  {
@@ -367,7 +370,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
367
370
  * @param {Color} [lineColor=(0,0,0,1)]
368
371
  * @param {CanvasTextAlign} [textAlign]
369
372
  * @param {String} [font=fontDefault]
370
- * @param {CanvasRenderingContext2D} [context=overlayContext]
373
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
371
374
  * @memberof Draw */
372
375
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
373
376
  {
@@ -410,7 +413,7 @@ class FontImage
410
413
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
411
414
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
412
415
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
413
- * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
416
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
414
417
  */
415
418
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
416
419
  {
@@ -45,6 +45,7 @@ export {
45
45
  canvasPixelated,
46
46
  fontDefault,
47
47
  showSplashScreen,
48
+ headlessMode,
48
49
  tileSizeDefault,
49
50
  tileFixBleedScale,
50
51
  enablePhysicsSolver,
@@ -83,6 +84,7 @@ export {
83
84
  setCanvasPixelated,
84
85
  setFontDefault,
85
86
  setShowSplashScreen,
87
+ setHeadlessMode,
86
88
  setGlEnable,
87
89
  setGlOverlay,
88
90
  setTileSizeDefault,
@@ -122,7 +122,7 @@ function gamepadWasReleased(button, gamepad=0)
122
122
  * @return {Vector2}
123
123
  * @memberof Input */
124
124
  function gamepadStick(stick, gamepad=0)
125
- { return stickData[gamepad] ? stickData[gamepad][stick] || vec2() : vec2(); }
125
+ { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
126
126
 
127
127
  ///////////////////////////////////////////////////////////////////////////////
128
128
  // Input update called by engine
@@ -133,6 +133,8 @@ let inputData = [[]];
133
133
 
134
134
  function inputUpdate()
135
135
  {
136
+ if (headlessMode) return;
137
+
136
138
  // clear input when lost focus (prevent stuck keys)
137
139
  isTouchDevice || document.hasFocus() || clearInput();
138
140
 
@@ -145,6 +147,8 @@ function inputUpdate()
145
147
 
146
148
  function inputUpdatePost()
147
149
  {
150
+ if (headlessMode) return;
151
+
148
152
  // clear input to prepare for next frame
149
153
  for (const deviceInputData of inputData)
150
154
  for (const i in deviceInputData)
@@ -153,9 +157,12 @@ function inputUpdatePost()
153
157
  }
154
158
 
155
159
  ///////////////////////////////////////////////////////////////////////////////
156
- // Keyboard event handlers
160
+ // Input event handlers
157
161
 
162
+ function inputInit()
158
163
  {
164
+ if (headlessMode) return;
165
+
159
166
  onkeydown = (e)=>
160
167
  {
161
168
  if (debug && e.target != document.body) return;
@@ -186,21 +193,29 @@ function inputUpdatePost()
186
193
  c == 'KeyA' ? 'ArrowLeft' :
187
194
  c == 'KeyD' ? 'ArrowRight' : c : c;
188
195
  }
196
+
197
+ // mouse event handlers
198
+ onmousedown = (e)=>
199
+ {
200
+ isUsingGamepad = false;
201
+ inputData[0][e.button] = 3;
202
+ mousePosScreen = mouseToScreen(e);
203
+ e.button && e.preventDefault();
204
+ }
205
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
206
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
207
+ onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
208
+ oncontextmenu = (e)=> false; // prevent right click menu
209
+
210
+ // init touch input
211
+ if (isTouchDevice)
212
+ touchInputInit();
189
213
  }
190
214
 
191
- ///////////////////////////////////////////////////////////////////////////////
192
- // Mouse event handlers
193
-
194
- onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
195
- onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
196
- onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
197
- onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
198
- oncontextmenu = (e)=> false; // prevent right click menu
199
-
200
215
  // convert a mouse or touch event position to screen space
201
216
  function mouseToScreen(mousePos)
202
217
  {
203
- if (!mainCanvas)
218
+ if (!mainCanvas || headlessMode)
204
219
  return vec2(); // fix bug that can occur if user clicks before page loads
205
220
 
206
221
  const rect = mainCanvas.getBoundingClientRect();
@@ -212,7 +227,7 @@ function mouseToScreen(mousePos)
212
227
  // Gamepad input
213
228
 
214
229
  // gamepad internal variables
215
- const stickData = [];
230
+ const gamepadStickData = [];
216
231
 
217
232
  // gamepads are updated by engine every frame automatically
218
233
  function gamepadsUpdate()
@@ -229,14 +244,11 @@ function gamepadsUpdate()
229
244
  // update touch gamepad if enabled
230
245
  if (touchGamepadEnable && isTouchDevice)
231
246
  {
232
- // create the touch gamepad if it doesn't exist
233
- if (!touchGamepadButtons)
234
- createTouchGamepad();
235
-
247
+ ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
236
248
  if (touchGamepadTimer.isSet())
237
249
  {
238
250
  // read virtual analog stick
239
- const sticks = stickData[0] || (stickData[0] = []);
251
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
240
252
  sticks[0] = vec2();
241
253
  if (touchGamepadAnalog)
242
254
  sticks[0] = applyDeadZones(touchGamepadStick);
@@ -253,7 +265,8 @@ function gamepadsUpdate()
253
265
  for (let i=10; i--;)
254
266
  {
255
267
  const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
256
- data[j] = touchGamepadButtons[i] ? gamepadIsDown(j,0) ? 1 : 3 : gamepadIsDown(j,0) ? 4 : 0;
268
+ const wasDown = gamepadIsDown(j,0);
269
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
257
270
  }
258
271
  }
259
272
  }
@@ -273,7 +286,7 @@ function gamepadsUpdate()
273
286
  // get or create gamepad data
274
287
  const gamepad = gamepads[i];
275
288
  const data = inputData[i+1] || (inputData[i+1] = []);
276
- const sticks = stickData[i] || (stickData[i] = []);
289
+ const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
277
290
 
278
291
  if (gamepad)
279
292
  {
@@ -312,28 +325,44 @@ function gamepadsUpdate()
312
325
  * @param {Number|Array} [pattern] - single value in ms or vibration interval array
313
326
  * @memberof Input */
314
327
  function vibrate(pattern=100)
315
- { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
328
+ { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
316
329
 
317
330
  /** Cancel any ongoing vibration
318
331
  * @memberof Input */
319
332
  function vibrateStop() { vibrate(0); }
320
333
 
321
334
  ///////////////////////////////////////////////////////////////////////////////
322
- // Touch input
335
+ // Touch input & virtual on screen gamepad
323
336
 
324
337
  /** True if a touch device has been detected
325
338
  * @memberof Input */
326
- const isTouchDevice = window.ontouchstart !== undefined;
339
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
340
+
341
+ // touch gamepad internal variables
342
+ let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
327
343
 
328
344
  // try to enable touch mouse
329
- if (isTouchDevice)
345
+ function touchInputInit()
330
346
  {
347
+ // add non passive touch event listeners
348
+ let handleTouch = handleTouchDefault;
349
+ if (touchGamepadEnable)
350
+ {
351
+ // touch input internal variables
352
+ handleTouch = handleTouchGamepad;
353
+ touchGamepadButtons = [];
354
+ touchGamepadStick = vec2();
355
+ }
356
+ document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
357
+ document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
358
+ document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
359
+
331
360
  // override mouse events
332
- let wasTouching;
333
361
  onmousedown = onmouseup = ()=> 0;
334
362
 
335
363
  // handle all touch events the same way
336
- ontouchstart = ontouchmove = ontouchend = (e)=>
364
+ let wasTouching;
365
+ function handleTouchDefault(e)
337
366
  {
338
367
  // fix stalled audio requiring user interaction
339
368
  if (soundEnable && audioContext && audioContext.state != 'running')
@@ -362,27 +391,14 @@ if (isTouchDevice)
362
391
  // must return true so the document will get focus
363
392
  return true;
364
393
  }
365
- }
366
-
367
- ///////////////////////////////////////////////////////////////////////////////
368
- // touch gamepad, virtual on screen gamepad emulator for touch devices
369
-
370
- // touch input internal variables
371
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
372
394
 
373
- // create the touch gamepad, called automatically by the engine
374
- function createTouchGamepad()
375
- {
376
- // touch input internal variables
377
- touchGamepadButtons = [];
378
- touchGamepadStick = vec2();
379
-
380
- const touchHandler = ontouchstart;
381
- ontouchstart = ontouchmove = ontouchend = (e)=>
395
+ // special handling for virtual gamepad mode
396
+ function handleTouchGamepad(e)
382
397
  {
383
398
  // clear touch gamepad input
384
399
  touchGamepadStick = vec2();
385
400
  touchGamepadButtons = [];
401
+ isUsingGamepad = true;
386
402
 
387
403
  const touching = e.touches.length;
388
404
  if (touching)
@@ -423,9 +439,8 @@ function createTouchGamepad()
423
439
  }
424
440
  }
425
441
 
426
- // call default touch handler and set to using gamepad
427
- touchHandler.bind(window)(e);
428
- isUsingGamepad = true;
442
+ // call default touch handler so normal touch events still work
443
+ handleTouchDefault(e);
429
444
 
430
445
  // must return true so the document will get focus
431
446
  return true;
@@ -444,32 +459,33 @@ function touchGamepadRender()
444
459
  return;
445
460
 
446
461
  // setup the canvas
447
- overlayContext.save();
448
- overlayContext.globalAlpha = alpha*touchGamepadAlpha;
449
- overlayContext.strokeStyle = '#fff';
450
- overlayContext.lineWidth = 3;
462
+ const context = overlayContext;
463
+ context.save();
464
+ context.globalAlpha = alpha*touchGamepadAlpha;
465
+ context.strokeStyle = '#fff';
466
+ context.lineWidth = 3;
451
467
 
452
468
  // draw left analog stick
453
- overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
454
- overlayContext.beginPath();
469
+ context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
470
+ context.beginPath();
455
471
 
456
472
  const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
457
473
  if (touchGamepadAnalog) // draw circle shaped gamepad
458
474
  {
459
- overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
460
- overlayContext.fill();
461
- overlayContext.stroke();
475
+ context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
476
+ context.fill();
477
+ context.stroke();
462
478
  }
463
479
  else // draw cross shaped gamepad
464
480
  {
465
481
  for(let i=10; i--;)
466
482
  {
467
483
  const angle = i*PI/4;
468
- overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
469
- i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
470
- i==1 && overlayContext.fill();
484
+ context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
485
+ i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
486
+ i==1 && context.fill();
471
487
  }
472
- overlayContext.stroke();
488
+ context.stroke();
473
489
  }
474
490
 
475
491
  // draw right face buttons
@@ -477,13 +493,13 @@ function touchGamepadRender()
477
493
  for (let i=4; i--;)
478
494
  {
479
495
  const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
480
- overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
481
- overlayContext.beginPath();
482
- overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
483
- overlayContext.fill();
484
- overlayContext.stroke();
496
+ context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
497
+ context.beginPath();
498
+ context.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
499
+ context.fill();
500
+ context.stroke();
485
501
  }
486
502
 
487
503
  // set canvas back to normal
488
- overlayContext.restore();
504
+ context.restore();
489
505
  }
@@ -101,23 +101,37 @@ 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);
107
109
  }
108
110
 
109
- /** Update the object transform and physics, called automatically by engine once each frame */
110
- update()
111
+ /** Update the object transform, called automatically by engine even when paused */
112
+ updateTransforms()
111
113
  {
112
114
  const parent = this.parent;
113
115
  if (parent)
114
116
  {
115
117
  // copy parent pos/angle
116
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
117
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
118
- return;
118
+ const mirror = parent.getMirrorSign();
119
+ this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(-parent.angle).add(parent.pos);
120
+ this.angle = mirror*this.localAngle + parent.angle;
119
121
  }
120
122
 
123
+ // update children
124
+ for (const child of this.children)
125
+ child.updateTransforms();
126
+ }
127
+
128
+ /** Update the object physics, called automatically by engine once each frame */
129
+ update()
130
+ {
131
+ // child objects do not have physics
132
+ if (this.parent)
133
+ return;
134
+
121
135
  // limit max speed to prevent missing collisions
122
136
  this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
123
137
  this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
@@ -132,8 +146,7 @@ class EngineObject
132
146
  // physics sanity checks
133
147
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
134
148
  ASSERT(this.damping >= 0 && this.damping <= 1);
135
-
136
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
149
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
137
150
  return;
138
151
 
139
152
  const wasMovingDown = this.velocity.y < 0;
@@ -305,13 +318,7 @@ class EngineObject
305
318
  * @param {Number} tileData - the value of the tile at the position
306
319
  * @param {Vector2} pos - tile where the collision occured
307
320
  * @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; }
321
+ collideWithTile(tileData, pos) { return tileData > 0; }
315
322
 
316
323
  /** Called to check if a object collision should be resolved
317
324
  * @param {EngineObject} object - the object to test against
@@ -358,16 +365,18 @@ class EngineObject
358
365
  }
359
366
 
360
367
  /** 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)
368
+ * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects?
369
+ * @param {Boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
370
+ * @param {Boolean} [collideTiles] - Does it collide with the tile collision?
371
+ * @param {Boolean} [collideRaycast] - Does it collide with raycasts? */
372
+ setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
365
373
  {
366
374
  ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
367
375
 
368
376
  this.collideSolidObjects = collideSolidObjects;
369
377
  this.isSolid = isSolid;
370
378
  this.collideTiles = collideTiles;
379
+ this.collideRaycast = collideRaycast;
371
380
  }
372
381
 
373
382
  /** Returns string containg info about this object for debugging