littlejsengine 1.24.2 → 1.25.0
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/CHANGELOG.md +55 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +6 -0
- package/REFERENCE.md +86 -41
- package/dist/littlejs.d.ts +600 -323
- package/dist/littlejs.esm.js +2412 -1797
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +2410 -1797
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +2393 -1826
- package/package.json +1 -1
- package/plugins/box2d.js +41 -28
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +331 -55
- package/plugins/lightSystem.js +2 -2
- package/plugins/math3d.js +1013 -1014
- package/plugins/newgrounds.js +1 -4
- package/plugins/particleEffects.js +3 -4
- package/plugins/pathFinder.js +52 -28
- package/plugins/pluginExport.js +2 -0
- package/plugins/postProcess.js +196 -65
- package/plugins/render3d.js +8 -6
- package/plugins/render3dEditor.js +3 -3
- package/plugins/render3dExtras.js +31 -10
- package/plugins/render3dLevel.js +15 -4
- package/plugins/textureSheet.js +15 -7
- package/plugins/tweenSystem.js +43 -38
- package/plugins/uiSystem.js +25 -20
- package/src/engine.js +39 -21
- package/src/engineAudio.js +13 -9
- package/src/engineBuild.mjs +49 -2
- package/src/engineDebug.js +3 -2
- package/src/engineDraw.js +20 -18
- package/src/engineEditor.js +75 -23
- package/src/engineInput.js +10 -10
- package/src/engineMath.js +3 -6
- package/src/engineObject.js +8 -9
- package/src/engineParticles.js +8 -7
- package/src/engineRelease.js +17 -10
- package/src/engineTileLayer.js +33 -14
- package/src/engineUtilities.js +15 -2
- package/src/engineWebGL.js +34 -12
package/src/engineAudio.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* @memberof Audio */
|
|
21
21
|
let audioContext = typeof AudioContext == 'undefined' ? undefined : new AudioContext;
|
|
22
22
|
|
|
23
|
+
// resume the audio, a refusal caught so it is not an uncaught error in the console, the next gesture tries again
|
|
24
|
+
function audioResume() { audioContext?.resume()?.catch?.(()=> {}); }
|
|
25
|
+
|
|
23
26
|
/** Master gain node for all audio to pass through, made at load so effects can connect to it any time
|
|
24
27
|
* @type {GainNode}
|
|
25
28
|
* @memberof Audio */
|
|
@@ -76,6 +79,7 @@ function audioSetSession()
|
|
|
76
79
|
let audioSuspendedWhenHidden = false;
|
|
77
80
|
function audioVisibilityChange()
|
|
78
81
|
{
|
|
82
|
+
if (!audioContext) return; // a browser with no audio has none to pause
|
|
79
83
|
if (document.hidden)
|
|
80
84
|
{
|
|
81
85
|
if (!soundPauseWhenHidden || audioContext.state != 'running') return;
|
|
@@ -85,7 +89,7 @@ function audioVisibilityChange()
|
|
|
85
89
|
else if (audioSuspendedWhenHidden)
|
|
86
90
|
{
|
|
87
91
|
audioSuspendedWhenHidden = false;
|
|
88
|
-
|
|
92
|
+
audioResume();
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -224,7 +228,7 @@ function audioParamRamp(param, value, fadeTime=0)
|
|
|
224
228
|
class Sound
|
|
225
229
|
{
|
|
226
230
|
/** Create a sound object and cache the audio for later use
|
|
227
|
-
* @param {string|URL|Array} [asset] - Filename or URL of an audio file, or a zzfx array
|
|
231
|
+
* @param {string|URL|Array<number|undefined>} [asset] - Filename or URL of an audio file, or a zzfx array
|
|
228
232
|
* @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds it overrides the array's own randomness, which is used if undefined
|
|
229
233
|
* @param {number} [range=soundDefaultRange] - World space max range of sound
|
|
230
234
|
* @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
|
|
@@ -232,7 +236,7 @@ class Sound
|
|
|
232
236
|
*/
|
|
233
237
|
constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
|
|
234
238
|
{
|
|
235
|
-
if (!soundEnable || headlessMode)
|
|
239
|
+
if (!soundEnable || headlessMode || !audioContext) // a browser with no audio makes none either
|
|
236
240
|
{
|
|
237
241
|
// no sound is made: it counts as loaded, so a game that waits for its sounds goes on
|
|
238
242
|
this.loadedPercent = 1;
|
|
@@ -302,8 +306,8 @@ class Sound
|
|
|
302
306
|
// load the audio file, a URL object as bundlers give works like its string;
|
|
303
307
|
// report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
|
|
304
308
|
const filename = asset + '';
|
|
305
|
-
engineAddLoad(this.loadSound(filename).catch(e=>
|
|
306
|
-
|
|
309
|
+
engineAddLoad(this.loadSound(filename).catch(e=> // startup waits for it, and a release build says why too
|
|
310
|
+
console.warn('Sound load failed for', filename, '-', e?.message ?? e)));
|
|
307
311
|
}
|
|
308
312
|
}
|
|
309
313
|
|
|
@@ -795,7 +799,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
795
799
|
* @param {number} [rate] - The playback rate to use
|
|
796
800
|
* @param {number} [pan] - How much to apply stereo panning
|
|
797
801
|
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
798
|
-
* @param {number} [sampleRate=
|
|
802
|
+
* @param {number} [sampleRate=audioDefaultSampleRate] - Sample rate for the sound
|
|
799
803
|
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
800
804
|
* @param {number} [offset] - Where to start in the sound, in its own seconds whatever the rate
|
|
801
805
|
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
@@ -812,7 +816,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
812
816
|
// fix stalled audio, don't build a buffer that can't be played;
|
|
813
817
|
// but a context suspended because the page is hidden stays suspended until it shows
|
|
814
818
|
if (!audioSuspendedWhenHidden)
|
|
815
|
-
|
|
819
|
+
audioResume();
|
|
816
820
|
return;
|
|
817
821
|
}
|
|
818
822
|
|
|
@@ -822,7 +826,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
822
826
|
|
|
823
827
|
/** Copy arrays of samples into a new audio buffer
|
|
824
828
|
* @param {Array} sampleChannels - Array of arrays of samples (for stereo playback)
|
|
825
|
-
* @param {number} [sampleRate=
|
|
829
|
+
* @param {number} [sampleRate=audioDefaultSampleRate] - Sample rate for the sound
|
|
826
830
|
* @return {AudioBuffer} - The audio buffer holding the samples
|
|
827
831
|
* @memberof Audio */
|
|
828
832
|
function createAudioBuffer(sampleChannels, sampleRate=audioDefaultSampleRate)
|
|
@@ -857,7 +861,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
857
861
|
// fix stalled audio, this sound won't be able to play;
|
|
858
862
|
// but a context suspended because the page is hidden stays suspended until it shows
|
|
859
863
|
if (!audioSuspendedWhenHidden)
|
|
860
|
-
|
|
864
|
+
audioResume();
|
|
861
865
|
return;
|
|
862
866
|
}
|
|
863
867
|
|
package/src/engineBuild.mjs
CHANGED
|
@@ -289,17 +289,64 @@ function typeScriptBuildStep(filename)
|
|
|
289
289
|
try
|
|
290
290
|
{
|
|
291
291
|
const tsFilename = join(BUILD_FOLDER, `${ENGINE_NAME}.d.ts`);
|
|
292
|
-
|
|
292
|
+
// strictNullChecks keeps undefined in the types, where tsc would drop it from accessors and object types
|
|
293
|
+
// whatever the JSDoc says, so a game in strict mode can set render3D.shader = undefined and is told a
|
|
294
|
+
// flare may be missing; a game not in strict mode reads the same types as before
|
|
295
|
+
execSync(`npx -p typescript tsc "${filename}" --declaration --allowJs --emitDeclarationOnly --strictNullChecks --outFile "${tsFilename}"`);
|
|
293
296
|
|
|
294
297
|
// Make declare module part use the package name littlejsengine
|
|
295
298
|
let fileContent = fs.readFileSync(tsFilename, 'utf8');
|
|
296
299
|
fileContent = fileContent.replace(`${ENGINE_NAME}\.esm`, 'littlejsengine')
|
|
297
|
-
fs.writeFileSync(tsFilename, fileContent);
|
|
300
|
+
fs.writeFileSync(tsFilename, typeScriptOptionalTidy(fileContent));
|
|
298
301
|
|
|
299
302
|
}
|
|
300
303
|
catch (e) { handleError(e, 'Failed to run TypeScript build step!'); }
|
|
301
304
|
};
|
|
302
305
|
|
|
306
|
+
// strictNullChecks writes an optional parameter or field as x?: T | undefined, which is the same as x?: T, so the
|
|
307
|
+
// redundant | undefined is taken off: each ?: is followed to the end of its type, the first , ; ) or line end outside
|
|
308
|
+
// brackets, and only that type is changed; a required x: T | undefined keeps it
|
|
309
|
+
function typeScriptOptionalTidy(text)
|
|
310
|
+
{
|
|
311
|
+
let out = '', i = 0;
|
|
312
|
+
for (let at; (at = text.indexOf('?: ', i)) >= 0;)
|
|
313
|
+
{
|
|
314
|
+
const start = at + 3;
|
|
315
|
+
let end = start, depth = 0;
|
|
316
|
+
for (; end < text.length; ++end)
|
|
317
|
+
{
|
|
318
|
+
const c = text[end];
|
|
319
|
+
if (c === '=' && text[end + 1] === '>') { ++end; continue; } // an arrow is not a closing bracket
|
|
320
|
+
if ('([{<'.includes(c)) ++depth;
|
|
321
|
+
else if (')]}>'.includes(c) && depth) --depth;
|
|
322
|
+
else if (!depth && (',;)\n'.includes(c))) break;
|
|
323
|
+
}
|
|
324
|
+
let type = typeScriptOptionalTidy(text.slice(start, end)); // and the optional fields of a record type in it
|
|
325
|
+
if (type.endsWith(' | undefined'))
|
|
326
|
+
{
|
|
327
|
+
type = type.slice(0, -12);
|
|
328
|
+
// a function type was wrapped for the union, (() => void) | undefined, and needs it no more
|
|
329
|
+
if (type[0] === '(' && typeScriptCloseOf(type, 0) === type.length - 1)
|
|
330
|
+
type = type.slice(1, -1);
|
|
331
|
+
}
|
|
332
|
+
out += text.slice(i, start) + type;
|
|
333
|
+
i = end;
|
|
334
|
+
}
|
|
335
|
+
return out + text.slice(i);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// the index of the bracket that closes the one at start
|
|
339
|
+
function typeScriptCloseOf(text, start)
|
|
340
|
+
{
|
|
341
|
+
let depth = 0;
|
|
342
|
+
for (let k = start; k < text.length; ++k)
|
|
343
|
+
{
|
|
344
|
+
if (text[k] === '(') ++depth;
|
|
345
|
+
else if (text[k] === ')' && !--depth) return k;
|
|
346
|
+
}
|
|
347
|
+
return -1;
|
|
348
|
+
}
|
|
349
|
+
|
|
303
350
|
// display the error and exit
|
|
304
351
|
function handleError(e,message)
|
|
305
352
|
{
|
package/src/engineDebug.js
CHANGED
|
@@ -94,7 +94,7 @@ function LOG(...output) { console.log(...output); }
|
|
|
94
94
|
|
|
95
95
|
/** Draw a debug rectangle in world space, or on the screen with screenSpace
|
|
96
96
|
* @param {Vector2} pos
|
|
97
|
-
* @param {Vector2} [size=vec2(
|
|
97
|
+
* @param {Vector2} [size=vec2()]
|
|
98
98
|
* @param {Color|string} [color]
|
|
99
99
|
* @param {number} [time]
|
|
100
100
|
* @param {number} [angle]
|
|
@@ -766,7 +766,8 @@ function debugRenderPost() { debugVideoCaptureIsActive() && debugVideoCaptureUpd
|
|
|
766
766
|
let debugVideoCapture, debugVideoCaptureIcon;
|
|
767
767
|
|
|
768
768
|
/** Check if video capture is active
|
|
769
|
-
* @memberof Debug
|
|
769
|
+
* @memberof Debug
|
|
770
|
+
* @return {boolean} */
|
|
770
771
|
function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
|
|
771
772
|
|
|
772
773
|
/** Start capturing video
|
package/src/engineDraw.js
CHANGED
|
@@ -105,7 +105,7 @@ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
|
105
105
|
* Create a tile info object using a grid based system
|
|
106
106
|
* - This can take vecs or floats for easier use and conversion
|
|
107
107
|
* - If an index is passed in, the tile size and index will determine the position
|
|
108
|
-
* @param {Vector2|number} [index
|
|
108
|
+
* @param {Vector2|number} [index] - Index of the tile in 1d or 2d form
|
|
109
109
|
* @param {Vector2|number} [size] - Size of tile in pixels
|
|
110
110
|
* @param {TextureInfo|number} [texture] - Texture index or info to use
|
|
111
111
|
* @param {number} [padding] - How many pixels padding around tiles
|
|
@@ -134,10 +134,13 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
|
|
|
134
134
|
// create tile info object
|
|
135
135
|
const textureInfo = typeof texture === 'number' ?
|
|
136
136
|
textureInfos[texture] : texture;
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
if (!textureInfo?.size.x)
|
|
138
|
+
{
|
|
139
|
+
// no image: headless loads none and keeps the size; an image that failed to load, whose warning named it, gives
|
|
140
|
+
// a tile of no size, which draws nothing; a slot never given an image is a mistake a debug build points out
|
|
141
|
+
ASSERT(headlessMode || textureInfo instanceof TextureInfo, 'tile texture is not loaded', texture);
|
|
142
|
+
return new TileInfo(new Vector2, headlessMode ? size.copy() : new Vector2, textureInfo, padding, bleed);
|
|
143
|
+
}
|
|
141
144
|
|
|
142
145
|
// get the position of the tile
|
|
143
146
|
const sizePaddedX = size.x + padding*2;
|
|
@@ -586,10 +589,10 @@ class Shader
|
|
|
586
589
|
* @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
|
|
587
590
|
* @param {Color} [color=WHITE] - Color to modulate with
|
|
588
591
|
* @param {number} [angle] - Angle to rotate by
|
|
589
|
-
* @param {boolean} [mirror] - Is image flipped
|
|
592
|
+
* @param {boolean} [mirror] - Is the image flipped left to right?
|
|
590
593
|
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
591
594
|
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
592
|
-
* @param {boolean} [screenSpace=drawScreenSpace] - Are the pos and size
|
|
595
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - Are the pos and size in screen space?
|
|
593
596
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
594
597
|
* @memberof Draw */
|
|
595
598
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
@@ -605,8 +608,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
605
608
|
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
606
609
|
|
|
607
610
|
const textureInfo = tileInfo?.textureInfo;
|
|
608
|
-
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
|
|
609
|
-
return; // a tile with no area draws nothing, like a sprite still loading
|
|
611
|
+
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y && textureInfo.size.x))
|
|
612
|
+
return; // a tile with no area draws nothing, like a sprite still loading, and nor does an image that failed
|
|
610
613
|
const bleed = tileInfo?.bleed ?? 0;
|
|
611
614
|
if (useWebGL && glEnable)
|
|
612
615
|
{
|
|
@@ -700,7 +703,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
700
703
|
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
701
704
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
702
705
|
* @memberof Draw */
|
|
703
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
706
|
+
function drawRectGradient(pos, size=vec2(1), colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
704
707
|
{
|
|
705
708
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
706
709
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -759,9 +762,9 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, an
|
|
|
759
762
|
* @param {Vector2} pos - Center of the rect in world space
|
|
760
763
|
* @param {Vector2} size - Size of the rect in world space
|
|
761
764
|
* @param {Vector2} wrapCount - How many times the texture repeats (x, y)
|
|
762
|
-
* @param {TextureInfo|number} [texture
|
|
765
|
+
* @param {TextureInfo|number} [texture] - TextureInfo or texture index into textureInfos
|
|
763
766
|
* @param {Color} [color=WHITE] - Color to modulate with
|
|
764
|
-
* @param {number} [angle
|
|
767
|
+
* @param {number} [angle] - Angle to rotate by
|
|
765
768
|
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
766
769
|
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
767
770
|
* @param {boolean} [screenSpace=drawScreenSpace] - Are pos and size in screen space?
|
|
@@ -1103,9 +1106,9 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
1103
1106
|
|
|
1104
1107
|
/** Draw colored circle using passed in point
|
|
1105
1108
|
* @param {Vector2} pos
|
|
1106
|
-
* @param {number} [size
|
|
1109
|
+
* @param {number} [size] - Diameter
|
|
1107
1110
|
* @param {Color} [color=WHITE]
|
|
1108
|
-
* @param {number} [lineWidth
|
|
1111
|
+
* @param {number} [lineWidth]
|
|
1109
1112
|
* @param {Color} [lineColor=BLACK]
|
|
1110
1113
|
* @param {boolean} [useWebGL=glEnable]
|
|
1111
1114
|
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
@@ -1200,7 +1203,7 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
|
|
|
1200
1203
|
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
1201
1204
|
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
1202
1205
|
* @param {Vector2} pos
|
|
1203
|
-
* @param {number} [size
|
|
1206
|
+
* @param {number} [size] - Diameter
|
|
1204
1207
|
* @param {Color} [colorInner=WHITE]
|
|
1205
1208
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
1206
1209
|
* @param {boolean} [useWebGL=glEnable]
|
|
@@ -1265,7 +1268,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
1265
1268
|
* @param {Color} [color=WHITE]
|
|
1266
1269
|
* @param {number} [lineWidth]
|
|
1267
1270
|
* @param {Color} [lineColor=BLACK]
|
|
1268
|
-
* @param {'left'|'center'|'right'} [textAlign
|
|
1271
|
+
* @param {'left'|'center'|'right'} [textAlign]
|
|
1269
1272
|
* @param {string} [font=fontDefault]
|
|
1270
1273
|
* @param {string} [fontStyle]
|
|
1271
1274
|
* @param {number} [maxWidth]
|
|
@@ -1695,8 +1698,7 @@ function bakeTintedImage(image, color, additiveColor)
|
|
|
1695
1698
|
* @param {number} dHeight
|
|
1696
1699
|
* @param {Color} color
|
|
1697
1700
|
* @param {Color} [additiveColor]
|
|
1698
|
-
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
|
|
1699
|
-
* @memberof Draw */
|
|
1701
|
+
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding */
|
|
1700
1702
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
1701
1703
|
{
|
|
1702
1704
|
const sx2 = bleed;
|
package/src/engineEditor.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @callback EditorPlayFromCallback - Puts the player at a world position, for the level editor's Play from mouse
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {any} pos - Where to start playing, a Vector2 from the 2D editor and a Vector3 from the 3D one
|
|
18
18
|
* @memberof Editor
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -32,25 +32,29 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @
|
|
36
|
-
* @
|
|
35
|
+
* @template [P=Vector2]
|
|
36
|
+
* @typedef {Object} EditorToolAt - What the level editor gives the callbacks of a tool of the game's own; P is
|
|
37
|
+
* the type of pos, a Vector2 for a 2D tool, the default, and Vector3|undefined for a 3D one
|
|
38
|
+
* @property {P} pos - Where the mouse is in the level: a Vector2 in the 2D editor; in the 3D one a Vector3 on
|
|
37
39
|
* the level or the ground, undefined when the mouse is over the panel
|
|
38
40
|
* @property {Vector2|undefined} cell - 2D: the tile cell under the mouse on the selected tile layer
|
|
39
|
-
* @property {
|
|
41
|
+
* @property {Ray3D|undefined} ray - 3D: the mouse's Ray3D
|
|
40
42
|
* @property {boolean} shift - Is Shift held
|
|
41
43
|
* @property {boolean} ctrl - Is Ctrl held
|
|
42
44
|
* @memberof Editor
|
|
43
45
|
*/
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* @
|
|
48
|
+
* @template [P=Vector2]
|
|
49
|
+
* @typedef {Object} EditorTool - A tool of the game's own for the level editor, see LevelEditor.addTool; P is the
|
|
50
|
+
* type of its pos, as EditorToolAt has it
|
|
47
51
|
* @property {string} [key] - The key that picks it, as addKey spells one
|
|
48
52
|
* @property {string} [hint] - The hint line while it is on
|
|
49
|
-
* @property {function(EditorToolAt): any} [onPress] - The left button went down in the level; returning false
|
|
53
|
+
* @property {function(EditorToolAt<P>): any} [onPress] - The left button went down in the level; returning false
|
|
50
54
|
* says the press was not the tool's
|
|
51
|
-
* @property {function(EditorToolAt): any} [onDrag] - Each frame it is held
|
|
52
|
-
* @property {function(EditorToolAt): any} [onRelease] - It was let go
|
|
53
|
-
* @property {function(EditorToolAt): any} [onDraw] - Each frame the tool is on, to draw its cursor or preview
|
|
55
|
+
* @property {function(EditorToolAt<P>): any} [onDrag] - Each frame it is held
|
|
56
|
+
* @property {function(EditorToolAt<P>): any} [onRelease] - It was let go
|
|
57
|
+
* @property {function(EditorToolAt<P>): any} [onDraw] - Each frame the tool is on, to draw its cursor or preview
|
|
54
58
|
* @memberof Editor
|
|
55
59
|
*/
|
|
56
60
|
|
|
@@ -143,7 +147,7 @@ class LevelEditor
|
|
|
143
147
|
this.keys = {};
|
|
144
148
|
/** @type {Array<{label: string, onClick: Function, title: string}>} */
|
|
145
149
|
this.buttons = [];
|
|
146
|
-
/** @type {Object<string, EditorTool
|
|
150
|
+
/** @type {Object<string, EditorTool<any>>} */
|
|
147
151
|
this.tools = {};
|
|
148
152
|
}
|
|
149
153
|
|
|
@@ -161,7 +165,7 @@ class LevelEditor
|
|
|
161
165
|
|
|
162
166
|
/** Put the player at a position, a Vector2 in the 2D editor and a Vector3 in the 3D one; set it or override it
|
|
163
167
|
* and the editor has Play from mouse, which starts play there
|
|
164
|
-
* @param {any} pos */
|
|
168
|
+
* @param {any} pos - A Vector2 from the 2D editor, a Vector3 from the 3D one */
|
|
165
169
|
onPlayFrom(pos) {}
|
|
166
170
|
|
|
167
171
|
/** Called when the editor opens, to set or override */
|
|
@@ -244,8 +248,9 @@ class LevelEditor
|
|
|
244
248
|
* in 3D
|
|
245
249
|
* - What the callbacks change through edit2D or edit3D is one undo: the editor ends the stroke at the release,
|
|
246
250
|
* and takes it back when the right button or Escape ends the press
|
|
251
|
+
* @template [P=Vector2]
|
|
247
252
|
* @param {string} name
|
|
248
|
-
* @param {EditorTool} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
|
|
253
|
+
* @param {EditorTool<P>} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
|
|
249
254
|
* false says the press was not the tool's */
|
|
250
255
|
addTool(name, tool)
|
|
251
256
|
{
|
|
@@ -260,7 +265,10 @@ class LevelEditor
|
|
|
260
265
|
open()
|
|
261
266
|
{
|
|
262
267
|
if (editorOther?.wanted())
|
|
263
|
-
|
|
268
|
+
{
|
|
269
|
+
editorOther.open();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
264
272
|
if (!editorSession)
|
|
265
273
|
editorCameraScale = cameraScale; // a new session starts at the game's zoom, a return keeps the editor's
|
|
266
274
|
editorSession = true;
|
|
@@ -272,7 +280,10 @@ class LevelEditor
|
|
|
272
280
|
close()
|
|
273
281
|
{
|
|
274
282
|
if (editorOther?.active())
|
|
275
|
-
|
|
283
|
+
{
|
|
284
|
+
editorOther.close();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
276
287
|
editorSession = false;
|
|
277
288
|
editorSetOpen(false);
|
|
278
289
|
}
|
|
@@ -1084,6 +1095,8 @@ let editorStroke;
|
|
|
1084
1095
|
|
|
1085
1096
|
// during a big edit, the layers whose cell by cell redraws are held off, with the redraw each had of its own
|
|
1086
1097
|
let editorHeldRedraws;
|
|
1098
|
+
// the live layers a change can only show by drawing them again whole, drawn when it is done
|
|
1099
|
+
const editorRedrawWhole = new Set;
|
|
1087
1100
|
|
|
1088
1101
|
// make a big edit, a fill, clear, undo, apply or revert, with each layer's cell by cell redraws held off, a game's
|
|
1089
1102
|
// tile callback's included; the stroke's end draws each layer it touched again whole
|
|
@@ -1110,6 +1123,11 @@ function editorSetCell(layer, pos, gid)
|
|
|
1110
1123
|
source.data[index] = gid;
|
|
1111
1124
|
if (live.destroyed) return before; // a layer the game let go of, the map still has the change
|
|
1112
1125
|
|
|
1126
|
+
// a plain cell draws itself here; held draws, a game's onTile, which may change the cells around, and its own
|
|
1127
|
+
// drawing over the layer show only when the whole layer draws again, once the change is done
|
|
1128
|
+
if (editorHeldRedraws || editorHas('onTile') || live.onRedraw !== TileLayer.prototype.onRedraw)
|
|
1129
|
+
editorRedrawWhole.add(live);
|
|
1130
|
+
|
|
1113
1131
|
if (editorHeldRedraws && !editorHeldRedraws.has(live))
|
|
1114
1132
|
{
|
|
1115
1133
|
editorHeldRedraws.set(live, live.hasOwnProperty('redrawTileData') ? live.redrawTileData : undefined);
|
|
@@ -1187,8 +1205,7 @@ function editorUndo(redo=false)
|
|
|
1187
1205
|
editorChanged(stroke);
|
|
1188
1206
|
}
|
|
1189
1207
|
|
|
1190
|
-
// after a change the tile layers it touched draw again whole,
|
|
1191
|
-
// and the maps it touched are autosaved; a resized map is made again by the game, with the editor staying open
|
|
1208
|
+
// after a change the tile layers it touched that need it draw again whole, and the maps it touched are autosaved; a resized map is made again by the game, with the editor staying open
|
|
1192
1209
|
function editorChanged(stroke)
|
|
1193
1210
|
{
|
|
1194
1211
|
editorRedraw(stroke);
|
|
@@ -1197,11 +1214,11 @@ function editorChanged(stroke)
|
|
|
1197
1214
|
stroke.some((entry)=> entry.resize) && editorHas('onRestart') && levelEditor.onRestart();
|
|
1198
1215
|
}
|
|
1199
1216
|
|
|
1200
|
-
// the tile layers a stroke touched draw again whole
|
|
1217
|
+
// the tile layers a stroke touched that need it draw again whole
|
|
1201
1218
|
function editorRedraw(stroke)
|
|
1202
1219
|
{
|
|
1203
1220
|
for (const layer of new Set(stroke.filter((entry)=> entry.layer).map((entry)=> entry.layer)))
|
|
1204
|
-
layer.live.destroyed
|
|
1221
|
+
editorRedrawWhole.delete(layer.live) && !layer.live.destroyed && layer.live.redraw();
|
|
1205
1222
|
}
|
|
1206
1223
|
|
|
1207
1224
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -1708,6 +1725,40 @@ function editorGidMirror(gid)
|
|
|
1708
1725
|
return t ? editorTileToGid(t.tile, (4 - t.direction) % 4, !t.mirror) : gid;
|
|
1709
1726
|
}
|
|
1710
1727
|
|
|
1728
|
+
// a stamp's object turned a quarter clockwise with it: its place by the stamp, its body by its own rotation, which
|
|
1729
|
+
// Tiled turns clockwise about the object's place, so the place needs no other move
|
|
1730
|
+
function editorStampObjectTurn(entry, width)
|
|
1731
|
+
{
|
|
1732
|
+
const offset = vec2(entry.offset.y, width - entry.offset.x), object = entry.object;
|
|
1733
|
+
return {...entry, offset, ...(object && {object: {...object, rotation: ((object.rotation || 0) + 90) % 360}})};
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// a stamp's object mirrored left to right with it: its rotation the other way, a polygon's points mirrored about its
|
|
1737
|
+
// place, a tile object's image flipped, and a box, ellipse, text or tile, placed by a corner, placed by the corner
|
|
1738
|
+
// that becomes it, its width along its turn from the mirrored place; mirrored twice it is as it was
|
|
1739
|
+
function editorStampObjectMirror(entry, width)
|
|
1740
|
+
{
|
|
1741
|
+
const offset = vec2(width - entry.offset.x, entry.offset.y), object = entry.object;
|
|
1742
|
+
if (!object) return {...entry, offset};
|
|
1743
|
+
const mirrored = {...object, rotation: object.rotation ? -object.rotation : 0};
|
|
1744
|
+
const points = (list)=> list.map((p)=> ({...p, x: -p.x}));
|
|
1745
|
+
if (object.polygon || object.polyline)
|
|
1746
|
+
{
|
|
1747
|
+
object.polygon && (mirrored.polygon = points(object.polygon));
|
|
1748
|
+
object.polyline && (mirrored.polyline = points(object.polyline));
|
|
1749
|
+
}
|
|
1750
|
+
else
|
|
1751
|
+
{
|
|
1752
|
+
// Tiled's pixels run down, the stamp's cells up
|
|
1753
|
+
const angle = (object.rotation || 0) * PI / 180, w = object.width || 0, {x: tw, y: th} = entry.tileSize ?? vec2(1);
|
|
1754
|
+
offset.x -= w * cos(angle) / tw;
|
|
1755
|
+
offset.y -= w * sin(angle) / th;
|
|
1756
|
+
}
|
|
1757
|
+
if (object.gid)
|
|
1758
|
+
mirrored.gid = (object.gid ^ 0x80000000) >>> 0; // Tiled's flip across
|
|
1759
|
+
return {...entry, offset, object: mirrored};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1711
1762
|
// a stamp turned a quarter turn clockwise on screen, or back, its cells, their tiles and its objects together; cell
|
|
1712
1763
|
// (x, y) goes to (y, width - 1 - x), so the bottom row becomes the left column, its left end at the top
|
|
1713
1764
|
function editorStampTurn(stamp, back=false)
|
|
@@ -1715,7 +1766,7 @@ function editorStampTurn(stamp, back=false)
|
|
|
1715
1766
|
for (let turns = back ? 3 : 1; turns--;)
|
|
1716
1767
|
{
|
|
1717
1768
|
const {width, height} = stamp;
|
|
1718
|
-
const objects = stamp.objects?.map((
|
|
1769
|
+
const objects = stamp.objects?.map((entry)=> editorStampObjectTurn(entry, width));
|
|
1719
1770
|
stamp = {width: height, height: width, grids: stamp.grids.map((grid)=>
|
|
1720
1771
|
{
|
|
1721
1772
|
const turned = [];
|
|
@@ -1732,7 +1783,7 @@ function editorStampTurn(stamp, back=false)
|
|
|
1732
1783
|
function editorStampMirror(stamp)
|
|
1733
1784
|
{
|
|
1734
1785
|
const {width, height} = stamp;
|
|
1735
|
-
const objects = stamp.objects?.map((
|
|
1786
|
+
const objects = stamp.objects?.map((entry)=> editorStampObjectMirror(entry, width));
|
|
1736
1787
|
return {width, height, grids: stamp.grids.map((grid)=>
|
|
1737
1788
|
{
|
|
1738
1789
|
const mirrored = [];
|
|
@@ -1864,8 +1915,9 @@ function editorClear()
|
|
|
1864
1915
|
editorStrokeEnd();
|
|
1865
1916
|
}
|
|
1866
1917
|
|
|
1867
|
-
// the objects of a map's object layers inside a tile area, each {group, type, offset, object}: the index of
|
|
1868
|
-
// object layer, its offset from the area's bottom left corner,
|
|
1918
|
+
// the objects of a map's object layers inside a tile area, each {group, type, offset, object, tileSize}: the index of
|
|
1919
|
+
// its object layer, its offset from the area's bottom left corner, a copy of it, and the map's tile size in pixels,
|
|
1920
|
+
// which a mirror reads its width in cells by, as a stamp keeps them
|
|
1869
1921
|
function editorAreaObjects(layer, area)
|
|
1870
1922
|
{
|
|
1871
1923
|
const corner = layer.live.pos.add(area.min), far = layer.live.pos.add(area.max).add(vec2(1));
|
|
@@ -1874,7 +1926,7 @@ function editorAreaObjects(layer, area)
|
|
|
1874
1926
|
const ids = new Set(editorObjectsIn(objectLayer, corner, far));
|
|
1875
1927
|
return (objectLayer.group?.objects ?? []).filter((object)=> ids.has(object.id)).map((object)=>
|
|
1876
1928
|
({group, type: object.type || object.class, offset: editorObjectPos(layer.record, object).subtract(corner),
|
|
1877
|
-
object: editorObjectsCopy(object)}));
|
|
1929
|
+
object: editorObjectsCopy(object), tileSize: vec2(layer.record.map.tilewidth || 1, layer.record.map.tileheight || 1)}));
|
|
1878
1930
|
});
|
|
1879
1931
|
}
|
|
1880
1932
|
|
package/src/engineInput.js
CHANGED
|
@@ -248,7 +248,6 @@ function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='A
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
/** Returns true if mouse button is down
|
|
251
|
-
* @function
|
|
252
251
|
* @param {number} button
|
|
253
252
|
* @return {boolean}
|
|
254
253
|
* @memberof Input */
|
|
@@ -259,7 +258,6 @@ function mouseIsDown(button)
|
|
|
259
258
|
}
|
|
260
259
|
|
|
261
260
|
/** Returns true if mouse button was pressed
|
|
262
|
-
* @function
|
|
263
261
|
* @param {number} button
|
|
264
262
|
* @return {boolean}
|
|
265
263
|
* @memberof Input */
|
|
@@ -270,7 +268,6 @@ function mouseWasPressed(button)
|
|
|
270
268
|
}
|
|
271
269
|
|
|
272
270
|
/** Returns true if mouse button was released
|
|
273
|
-
* @function
|
|
274
271
|
* @param {number} button
|
|
275
272
|
* @return {boolean}
|
|
276
273
|
* @memberof Input */
|
|
@@ -498,7 +495,7 @@ function inputInit()
|
|
|
498
495
|
{
|
|
499
496
|
// fix stalled audio requiring user interaction, a keyboard only game has no other gesture
|
|
500
497
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
501
|
-
|
|
498
|
+
audioResume();
|
|
502
499
|
|
|
503
500
|
// keys typed into an html text field are the player's typing, not game input;
|
|
504
501
|
// a key already down still releases on keyup, which only lets go of keys that are down
|
|
@@ -584,10 +581,11 @@ function inputInit()
|
|
|
584
581
|
|
|
585
582
|
// fix stalled audio requiring user interaction
|
|
586
583
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
587
|
-
|
|
584
|
+
audioResume();
|
|
588
585
|
|
|
589
|
-
// a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves
|
|
590
|
-
|
|
586
|
+
// a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves; under
|
|
587
|
+
// pointer lock the mouse stays where the lock began, which may be in a bar, and every click is the game's
|
|
588
|
+
if (!pointerLockIsActive() && !inCanvas(e.x, e.y))
|
|
591
589
|
return;
|
|
592
590
|
inputData[0][e.button] = 3;
|
|
593
591
|
|
|
@@ -683,7 +681,7 @@ function inputInit()
|
|
|
683
681
|
|
|
684
682
|
// fix stalled audio requiring user interaction
|
|
685
683
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
686
|
-
|
|
684
|
+
audioResume();
|
|
687
685
|
|
|
688
686
|
// when the touch gamepad is enabled it owns touch input: suppress the
|
|
689
687
|
// touch->mouse passthrough entirely unless touchGamepadPassthrough is set
|
|
@@ -1004,9 +1002,11 @@ function inputUpdate()
|
|
|
1004
1002
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
1005
1003
|
}
|
|
1006
1004
|
|
|
1007
|
-
// copy dpad to left analog stick when pressed
|
|
1005
|
+
// copy dpad to left analog stick when pressed; a gamepad with no sticks has that one, at rest when not
|
|
1008
1006
|
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
|
1009
1007
|
sticks[0] = dpad.clampLength();
|
|
1008
|
+
else if (gamepadDirectionEmulateStick && !sticks.length)
|
|
1009
|
+
sticks[0] = vec2();
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
1012
1012
|
// disable touch gamepad if using real gamepad
|
|
@@ -1446,7 +1446,7 @@ function touchGamepadPointerDown(e, zone)
|
|
|
1446
1446
|
|
|
1447
1447
|
// resume audio on first interaction
|
|
1448
1448
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
1449
|
-
|
|
1449
|
+
audioResume();
|
|
1450
1450
|
|
|
1451
1451
|
// while paused, any touch is the start button; a control belongs to the first finger on it until that
|
|
1452
1452
|
// finger lifts, so a second finger landing on the same one neither takes it over nor lets it go
|
package/src/engineMath.js
CHANGED
|
@@ -413,8 +413,10 @@ function lineTest(posStart, posEnd, testFunction, normal)
|
|
|
413
413
|
const totalLength = (dx*dx + dy*dy)**.5;
|
|
414
414
|
if (!totalLength) return;
|
|
415
415
|
|
|
416
|
-
// current integer cell we are in
|
|
416
|
+
// current integer cell we are in, the one below a whole start heading down the grid, which it goes into
|
|
417
417
|
const pos = posStart.floor();
|
|
418
|
+
if (dx < 0 && pos.x === posStart.x) --pos.x;
|
|
419
|
+
if (dy < 0 && pos.y === posStart.y) --pos.y;
|
|
418
420
|
|
|
419
421
|
// normalize ray direction
|
|
420
422
|
const dirX = dx / totalLength;
|
|
@@ -687,11 +689,6 @@ function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
|
|
|
687
689
|
// vector2 asserts
|
|
688
690
|
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
|
|
689
691
|
function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
|
|
690
|
-
function ASSERT_VECTOR2_NORMAL(v)
|
|
691
|
-
{
|
|
692
|
-
ASSERT_VECTOR2_VALID(v);
|
|
693
|
-
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
694
|
-
}
|
|
695
692
|
|
|
696
693
|
/**
|
|
697
694
|
* 2D Vector object with vector math library
|
package/src/engineObject.js
CHANGED
|
@@ -483,19 +483,23 @@ class EngineObject
|
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
/** Convert from local space to world space
|
|
486
|
-
* @param {Vector2} pos - local space point
|
|
486
|
+
* @param {Vector2} pos - local space point
|
|
487
|
+
* @return {Vector2} */
|
|
487
488
|
localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
|
|
488
489
|
|
|
489
490
|
/** Convert from world space to local space
|
|
490
|
-
* @param {Vector2} pos - world space point
|
|
491
|
+
* @param {Vector2} pos - world space point
|
|
492
|
+
* @return {Vector2} */
|
|
491
493
|
worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
|
|
492
494
|
|
|
493
495
|
/** Convert from local space to world space for a vector (rotation only)
|
|
494
|
-
* @param {Vector2} vec - local space vector
|
|
496
|
+
* @param {Vector2} vec - local space vector
|
|
497
|
+
* @return {Vector2} */
|
|
495
498
|
localToWorldVector(vec) { return vec.rotate(this.angle); }
|
|
496
499
|
|
|
497
500
|
/** Convert from world space to local space for a vector (rotation only)
|
|
498
|
-
* @param {Vector2} vec - world space vector
|
|
501
|
+
* @param {Vector2} vec - world space vector
|
|
502
|
+
* @return {Vector2} */
|
|
499
503
|
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
500
504
|
|
|
501
505
|
/** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
|
|
@@ -634,11 +638,6 @@ class EngineObject
|
|
|
634
638
|
this.collideRaycast = collideRaycast;
|
|
635
639
|
}
|
|
636
640
|
|
|
637
|
-
/** @deprecated since 1.20, use collideLevel
|
|
638
|
-
* @type {boolean} */
|
|
639
|
-
get collideTiles() { return this.collideLevel; }
|
|
640
|
-
set collideTiles(collide) { this.collideLevel = collide; }
|
|
641
|
-
|
|
642
641
|
/** Returns string containing info about this object for debugging
|
|
643
642
|
* @return {string} */
|
|
644
643
|
toString()
|