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.
- package/dist/littlejs.d.ts +73 -63
- package/dist/littlejs.esm.js +405 -245
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +403 -245
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +399 -240
- package/examples/breakout/index.html +3 -3
- package/examples/breakoutTutorial/index.html +2 -2
- package/examples/electron/index.html +2 -2
- package/examples/js13k/build.js +1 -0
- package/examples/js13k/index.html +13 -13
- package/examples/module/index.html +1 -1
- package/examples/particles/index.html +1 -1
- package/examples/platformer/gameEffects.js +2 -3
- package/examples/platformer/index.html +8 -8
- package/examples/puzzle/index.html +2 -2
- package/examples/starter/index.html +13 -13
- package/examples/stress/index.html +1 -1
- package/examples/typescript/index.html +1 -1
- package/package.json +1 -1
- package/reference.md +11 -11
- package/src/engine.js +127 -71
- package/src/engineAudio.js +20 -16
- package/src/engineBuild.js +4 -4
- package/src/engineDebug.js +4 -5
- package/src/engineDraw.js +13 -10
- package/src/engineExport.js +2 -0
- package/src/engineInput.js +80 -64
- package/src/engineObject.js +27 -18
- package/src/engineParticles.js +14 -13
- package/src/engineSettings.js +13 -2
- package/src/engineTileLayer.js +20 -6
- package/src/engineUtilities.js +65 -22
- package/src/engineWebGL.js +20 -18
package/src/engineAudio.js
CHANGED
|
@@ -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
|
-
|
|
49
|
-
zzfxSound[1]
|
|
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
|
|
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
|
|
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] -
|
|
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 =
|
|
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;
|
package/src/engineBuild.js
CHANGED
|
@@ -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
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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!'); }
|
package/src/engineDebug.js
CHANGED
|
@@ -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=
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
{
|
package/src/engineExport.js
CHANGED
|
@@ -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,
|
package/src/engineInput.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
374
|
-
function
|
|
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
|
|
427
|
-
|
|
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
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
469
|
-
i%2 &&
|
|
470
|
-
i==1 &&
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
504
|
+
context.restore();
|
|
489
505
|
}
|
package/src/engineObject.js
CHANGED
|
@@ -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
|
|
110
|
-
|
|
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
|
-
|
|
117
|
-
this.
|
|
118
|
-
|
|
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)
|
|
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
|
-
|
|
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
|