littlejsengine 1.24.3 → 1.26.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 +123 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +55 -11
- package/README.md +6 -2
- package/REFERENCE.md +203 -57
- package/dist/littlejs.d.ts +933 -313
- package/dist/littlejs.esm.js +5143 -2419
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +5132 -2416
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +4905 -2232
- package/package.json +4 -4
- package/plugins/box2d.js +141 -56
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +383 -60
- package/plugins/lightSystem.js +412 -30
- package/plugins/math3d.js +1015 -1014
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +5 -6
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +47 -14
- package/plugins/pathFinder.js +60 -38
- package/plugins/pluginExport.js +11 -0
- package/plugins/postProcess.js +200 -67
- package/plugins/render3d.js +493 -2391
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +84 -9
- package/plugins/render3dExtras.js +122 -33
- package/plugins/render3dLevel.js +67 -8
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +117 -87
- package/plugins/uiSystem.js +62 -28
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +192 -40
- package/src/engineAudio.js +47 -27
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +23 -162
- package/src/engineDraw.js +148 -43
- package/src/engineEditor.js +153 -40
- package/src/engineExport.js +0 -3
- package/src/engineInput.js +31 -25
- package/src/engineMath.js +38 -20
- package/src/engineObject.js +68 -50
- package/src/engineParticles.js +66 -18
- package/src/engineRelease.js +18 -15
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +44 -18
- package/src/engineUtilities.js +56 -11
- package/src/engineWebGL.js +68 -22
package/plugins/medalSystem.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* - Medal class with name, description, icon, and unlock tracking
|
|
5
5
|
* - Automatic saving to local storage, unless a service like Newgrounds holds the medal (see Medal.isLocal)
|
|
6
6
|
* - Visual display queue with slide-in notifications
|
|
7
|
-
* - The Newgrounds plugin extends it with NewgroundsMedal, held on the server while logged in
|
|
8
|
-
* - Setting debugMedals = true in the game code before medalsInit skips the load and the save, and in the debug build logs the Newgrounds traffic; it is not exported, so only a script tag build can set it
|
|
7
|
+
* - The Newgrounds plugin extends it with NewgroundsMedal, held on the server while logged in, and the Wavedash plugin with WavedashMedal, saved here and sent to Wavedash
|
|
8
|
+
* - Setting debugMedals = true in the game code before medalsInit skips the load and the save, sends nothing to Wavedash, and in the debug build logs the Newgrounds traffic; it is not exported, so only a script tag build can set it
|
|
9
9
|
* @namespace Medals
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -54,13 +54,25 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast, medalsRender
|
|
|
54
54
|
* - Call this after creating all medals
|
|
55
55
|
* - Loads which medals are unlocked from the save, and writes the catalog back
|
|
56
56
|
* - A medal a service like Newgrounds holds is left as it is, see Medal.isLocal
|
|
57
|
+
* - A medal unlocked before the first call stays unlocked and is saved by it
|
|
57
58
|
* @param {string} saveName - The localStorage key the medals are kept under, a different one from the game's own
|
|
58
59
|
* readSaveData and writeSaveData, or each would overwrite the other
|
|
59
60
|
* @memberof Medals */
|
|
60
61
|
function medalsInit(saveName)
|
|
61
62
|
{
|
|
63
|
+
// unlocks from before the first call, which had nowhere to be saved, are kept: medals favour the player; a call
|
|
64
|
+
// after that, another player's save, starts from its own
|
|
65
|
+
const early = [];
|
|
66
|
+
medalsSaveName || medalsForEach((medal)=> medal.unlocked && medal.isLocal() && early.push(medal));
|
|
62
67
|
medalsSaveName = saveName;
|
|
63
68
|
medalsLoad();
|
|
69
|
+
if (early.length)
|
|
70
|
+
{
|
|
71
|
+
early.forEach((medal)=> medal.unlocked = true);
|
|
72
|
+
medalsSave();
|
|
73
|
+
}
|
|
74
|
+
// the saved unlocks of Wavedash medals go to Wavedash, when that plugin is in the build and started
|
|
75
|
+
typeof wavedash != 'undefined' && wavedash?.sendAchievements();
|
|
64
76
|
|
|
65
77
|
// add the medal display once, however often this is called
|
|
66
78
|
if (!medalsRenderAdded)
|
|
@@ -130,10 +142,11 @@ function medalsReset()
|
|
|
130
142
|
medalsForEach(medal=> medal.isLocal() && (medal.unlocked = false));
|
|
131
143
|
if (medalsSaveName && !debugMedals)
|
|
132
144
|
{
|
|
133
|
-
// the saved unlocks of medals not made yet are cleared too,
|
|
145
|
+
// the saved unlocks of the local medals and of medals not made yet are cleared too, since a save keeps an
|
|
146
|
+
// unlock another tab wrote, and those not made yet read theirs when they are made
|
|
134
147
|
const saved = readSaveData(medalsSaveName);
|
|
135
148
|
for (const id in saved)
|
|
136
|
-
if (!medals[id] && saved[id] && typeof saved[id] === 'object')
|
|
149
|
+
if ((!medals[id] || medals[id].isLocal()) && saved[id] && typeof saved[id] === 'object')
|
|
137
150
|
saved[id].unlocked = false;
|
|
138
151
|
writeSaveData(medalsSaveName, saved);
|
|
139
152
|
}
|
|
@@ -159,7 +172,7 @@ function medalsSave()
|
|
|
159
172
|
name: medal.name,
|
|
160
173
|
description: medal.description,
|
|
161
174
|
icon: medal.icon,
|
|
162
|
-
unlocked: medal.unlocked,
|
|
175
|
+
unlocked: medal.unlocked || !!saved[medal.id]?.unlocked, // another tab may have unlocked it since
|
|
163
176
|
};
|
|
164
177
|
if (medal.image) entry.src = medal.image.src;
|
|
165
178
|
data[medal.id] = entry;
|
|
@@ -229,8 +242,7 @@ class Medal
|
|
|
229
242
|
{
|
|
230
243
|
if (!medalsPreventUnlock && !this.unlocked)
|
|
231
244
|
{
|
|
232
|
-
|
|
233
|
-
this.unlocked = true;
|
|
245
|
+
this.unlocked = true; // saved now, or by medalsInit when it comes
|
|
234
246
|
medalsSave();
|
|
235
247
|
medalsDisplayQueue.push(this);
|
|
236
248
|
}
|
package/plugins/newgrounds.js
CHANGED
|
@@ -25,7 +25,7 @@ let newgrounds;
|
|
|
25
25
|
const newgroundsUnlocksToResend = new Set; // pending medals whose request did not reach the server
|
|
26
26
|
const newgroundsUnlocksRefused = new Set; // medals the server refused this visit, asked again they answer no unsent
|
|
27
27
|
const newgroundsSecureComponents = ['Medal.unlock', 'ScoreBoard.postScore']; // the calls encrypted with a cipher
|
|
28
|
-
const newgroundsSessionErrors = [104, 110, 111]; // expired session, login required, session
|
|
28
|
+
const newgroundsSessionErrors = [104, 110, 111]; // expired session, login required, session canceled
|
|
29
29
|
const newgroundsTimeoutMS = 15e3; // how long a request may take before it fails
|
|
30
30
|
|
|
31
31
|
// whether the server answered that the session is gone, as opposed to a request that failed on the way
|
|
@@ -157,9 +157,11 @@ class NewgroundsPlugin
|
|
|
157
157
|
const hasLocation = typeof location != 'undefined';
|
|
158
158
|
/** @property {string} - Hostname sent with the view the plugin logs when it starts */
|
|
159
159
|
this.host = hasLocation ? location.hostname : '';
|
|
160
|
-
/** @property {Array} - Medals fetched from Newgrounds, empty until ready, with the unlocks only when logged in
|
|
160
|
+
/** @property {Array<Object>} - Medals fetched from Newgrounds, empty until ready, with the unlocks only when logged in
|
|
161
|
+
* @type {Array<Object>} */
|
|
161
162
|
this.medals = [];
|
|
162
|
-
/** @property {Array} - Scoreboards fetched from Newgrounds, empty until ready
|
|
163
|
+
/** @property {Array<Object>} - Scoreboards fetched from Newgrounds, empty until ready
|
|
164
|
+
* @type {Array<Object>} */
|
|
163
165
|
this.scoreboards = [];
|
|
164
166
|
/** @property {{id: number, name: string, url: string, supporter: boolean}|null} - The logged in player once ready, null when not logged in
|
|
165
167
|
* @type {{id: number, name: string, url: string, supporter: boolean}|null} */
|
|
@@ -181,9 +183,6 @@ class NewgroundsPlugin
|
|
|
181
183
|
this.ready = this.init();
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
/** @deprecated since 1.20, the view is logged when the plugin starts, so this does nothing */
|
|
185
|
-
logView() {}
|
|
186
|
-
|
|
187
186
|
/** Log the view, check the session, fetch the medals and scoreboards, then keep the session alive; the constructor runs it once
|
|
188
187
|
* @private */
|
|
189
188
|
async init()
|
package/plugins/parallax.js
CHANGED
|
@@ -47,6 +47,7 @@ class ParallaxLayer extends CanvasLayer
|
|
|
47
47
|
ASSERT(isNumber(parallax) || isVector2(parallax), 'parallax must be a number or a Vector2');
|
|
48
48
|
ASSERT(typeof drawFunction == 'function', 'drawFunction must be a function');
|
|
49
49
|
super(pos, size, 0, renderOrder, canvasSize);
|
|
50
|
+
this.castShadow = false; // a backdrop, it would throw huge shadows in the light system, a sun's above all
|
|
50
51
|
|
|
51
52
|
/** @property {Vector2} - How much of the camera's movement it follows on each axis, 0 stays with the world
|
|
52
53
|
* and 1 with the screen */
|
|
@@ -85,9 +85,9 @@ particleEffectAddSetting('randomColorLinear', 'checkbox', true, 0, 0, 0,
|
|
|
85
85
|
|
|
86
86
|
particleEffectSettingGroup = 'Motion';
|
|
87
87
|
particleEffectAddSetting('speed', 'number', .1, 0, .5, .005,
|
|
88
|
-
'Start speed, world units per frame',
|
|
88
|
+
'Start speed, world units per frame', -1e9, 1e9); // negative goes back against the cone, as the emitter takes it
|
|
89
89
|
particleEffectAddSetting('angleSpeed', 'number', .05, 0, .5, .005,
|
|
90
|
-
'Spin speed, radians per frame',
|
|
90
|
+
'Spin speed, radians per frame', -1e9, 1e9);
|
|
91
91
|
particleEffectAddSetting('damping', 'number', 1, .8, 1, .001,
|
|
92
92
|
'Speed kept each frame, 1 keeps it all', 0, 1);
|
|
93
93
|
particleEffectAddSetting('angleDamping', 'number', 1, .8, 1, .001,
|
|
@@ -107,7 +107,7 @@ particleEffectAddSetting('collideLevel', 'checkbox', false, 0, 0, 0,
|
|
|
107
107
|
particleEffectAddSetting('restitution', 'number', 0, 0, 1, .01,
|
|
108
108
|
'Bounce when hitting tiles');
|
|
109
109
|
particleEffectAddSetting('friction', 'number', .8, 0, 1, .01,
|
|
110
|
-
'Speed kept sliding along tiles');
|
|
110
|
+
'Speed kept sliding along tiles, the larger of this and the friction of the layer, as for an object');
|
|
111
111
|
|
|
112
112
|
particleEffectSettingGroup = 'Texture';
|
|
113
113
|
particleEffectAddSetting('shape', 'shape', 'soft', 0, 0, 0,
|
|
@@ -160,9 +160,9 @@ function particleEffectSanitize(raw)
|
|
|
160
160
|
{
|
|
161
161
|
// one line of text, line breaks and control characters made spaces
|
|
162
162
|
const text = typeof raw?.name === 'string' ? raw.name.replace(/[\x00-\x1f\x7f]+/g, ' ').trim() : '';
|
|
163
|
-
|
|
163
|
+
// whole characters, and no space left where it was cut
|
|
164
|
+
const name = text ? textGraphemes(text).slice(0, 60).join('').trim() : 'Effect';
|
|
164
165
|
const input = raw?.settings && typeof raw.settings === 'object' ? {...raw.settings} : {};
|
|
165
|
-
input.collideLevel ??= input.collideTiles; // its name before 1.20
|
|
166
166
|
// a library saved before shapes names its tile and no shape, it keeps its tile
|
|
167
167
|
if (input.shape === undefined && isNumber(input.tileIndex))
|
|
168
168
|
input.shape = '';
|
|
@@ -212,10 +212,10 @@ function particleEffectSanitize(raw)
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/** A copy of an effect with its four colors turned around the color wheel and their saturation scaled; lightness and
|
|
215
|
-
* alpha stay, and
|
|
215
|
+
* alpha stay, and gray and white have no hue to turn
|
|
216
216
|
* @param {Object} effect
|
|
217
217
|
* @param {number} [hue] - How far around the wheel, 1 is all the way
|
|
218
|
-
* @param {number} [saturation] - Multiplies the saturation, 0 is
|
|
218
|
+
* @param {number} [saturation] - Multiplies the saturation, 0 is gray, clamped to 1
|
|
219
219
|
* @return {Object}
|
|
220
220
|
* @memberof ParticleEffects */
|
|
221
221
|
function particleEffectRecolor(effect, hue=0, saturation=1)
|
|
@@ -415,7 +415,7 @@ function particleEffectShapeTile(name)
|
|
|
415
415
|
* @param {string} name
|
|
416
416
|
* @param {function(Particle, number): void} update - Pushes a 2D particle, given the strength
|
|
417
417
|
* @param {function(Particle3D, number): void} [update3D] - The same for a 3D particle, none leaves 3D alone
|
|
418
|
-
* @param {number} [min] - Strength range the designer offers
|
|
418
|
+
* @param {number} [min] - Strength range the designer offers, and the range a strength is clamped to
|
|
419
419
|
* @param {number} [max]
|
|
420
420
|
* @param {number} [value] - Strength when first added
|
|
421
421
|
* @param {string} [description]
|
|
@@ -479,17 +479,45 @@ function particleEffectSet(emitter, effect)
|
|
|
479
479
|
emitter.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors);
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
// the options a play takes besides the settings
|
|
483
|
+
const PARTICLE_EFFECT_OPTIONS = ['hue', 'saturation', 'scale', 'angle', 'tileInfo', 'flatten'];
|
|
484
|
+
|
|
485
|
+
// whether an option is a value its setting takes, as particleEffectSanitize keeps one
|
|
486
|
+
function particleEffectOptionValid(setting, value)
|
|
487
|
+
{
|
|
488
|
+
return setting.kind === 'checkbox' ? typeof value === 'boolean' || value === 0 || value === 1 :
|
|
489
|
+
setting.kind === 'color' ? !!particleEffectColor(value) :
|
|
490
|
+
setting.kind === 'shape' ? value === '' || particleEffectShapes.includes(value) : isNumber(value);
|
|
491
|
+
}
|
|
492
|
+
|
|
482
493
|
// an effect from a name or an effect, recolored by the options
|
|
483
494
|
function particleEffectResolve(nameOrEffect, options)
|
|
484
495
|
{
|
|
485
496
|
const found = typeof nameOrEffect == 'string' ? particleEffectsGet(nameOrEffect) : nameOrEffect;
|
|
486
497
|
ASSERT(!!found, 'no particle effect named ' + nameOrEffect);
|
|
487
498
|
if (!found) return;
|
|
488
|
-
//
|
|
499
|
+
// a vec2 emitSize is a rectangle, as ParticleEmitter takes one, unless emitRect is false, which makes a circle as
|
|
500
|
+
// wide as its x
|
|
501
|
+
if (isVector2(options.emitSize))
|
|
502
|
+
options = {...options, emitSize: options.emitSize.x, emitHeight: options.emitSize.y,
|
|
503
|
+
emitRect: options.emitRect ?? true};
|
|
504
|
+
if (debug)
|
|
505
|
+
for (const key in options)
|
|
506
|
+
ASSERT(PARTICLE_EFFECT_OPTIONS.includes(key) || particleEffectSettings.some((setting)=> setting.name === key),
|
|
507
|
+
'particleEffect: no option named ' + key);
|
|
508
|
+
// any setting in the options replaces the effect's own for this play, emitTime for a burst or a loop; one that is
|
|
509
|
+
// not a value of its kind keeps the effect's own, where the sanitizing would put the library's default
|
|
489
510
|
const settings = {...found.settings};
|
|
490
511
|
for (const setting of particleEffectSettings)
|
|
491
|
-
|
|
492
|
-
|
|
512
|
+
{
|
|
513
|
+
const value = options[setting.name];
|
|
514
|
+
if (value === undefined) continue;
|
|
515
|
+
const valid = particleEffectOptionValid(setting, value);
|
|
516
|
+
ASSERT(valid, `particleEffect: ${setting.name} must be a ${setting.kind === 'checkbox' ? 'boolean' : setting.kind}`,
|
|
517
|
+
value);
|
|
518
|
+
if (valid)
|
|
519
|
+
settings[setting.name] = value;
|
|
520
|
+
}
|
|
493
521
|
// sanitized each time, an effect may be written by hand or changed after it was added
|
|
494
522
|
const effect = particleEffectSanitize({...found, settings}), {hue=0, saturation=1} = options;
|
|
495
523
|
return hue || saturation != 1 ? particleEffectRecolor(effect, hue, saturation) : effect;
|
|
@@ -505,7 +533,7 @@ function particleEffectResolve(nameOrEffect, options)
|
|
|
505
533
|
* @param {Object} [options] - What to change for this play, each left out when not wanted:
|
|
506
534
|
* scale grows the whole effect, the built-ins fit a one unit object at 1;
|
|
507
535
|
* hue turns its colors around the color wheel, 1 is all the way;
|
|
508
|
-
* saturation multiplies its saturation, 0 is
|
|
536
|
+
* saturation multiplies its saturation, 0 is gray;
|
|
509
537
|
* angle is its direction, 0 is up, the effect's own angle when not given;
|
|
510
538
|
* tileInfo, a TileInfo or a TextureInfo, is the game's own art to draw with in place of the effect's shape,
|
|
511
539
|
* tinted by its colors, a whole texture drawn as one tile;
|
|
@@ -514,6 +542,7 @@ function particleEffectResolve(nameOrEffect, options)
|
|
|
514
542
|
* @memberof ParticleEffects */
|
|
515
543
|
function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
516
544
|
{
|
|
545
|
+
options ||= {}; // null is no options too
|
|
517
546
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
518
547
|
if (!effect) return;
|
|
519
548
|
const emitter = new ParticleEmitter(pos.copy());
|
|
@@ -528,8 +557,9 @@ function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
|
528
557
|
}
|
|
529
558
|
|
|
530
559
|
/** An effect with a 2D emitter's settings, to save, build again, or build in 3D with particleEffect3D; its tile is
|
|
531
|
-
* left out, since a hand made emitter's tile is its own texture and not one an effect can name, and so
|
|
532
|
-
* scale, which an effect does not keep: pass it again with options.scale
|
|
560
|
+
* left out, since a hand made emitter's tile is its own texture and not one an effect can name, and so are its
|
|
561
|
+
* scale, which an effect does not keep: pass it again with options.scale, and its renderOrder, which an effect takes
|
|
562
|
+
* from additive; a finished emitter's emit time is the time it emitted for
|
|
533
563
|
* @param {ParticleEmitter} emitter
|
|
534
564
|
* @param {string} [name]
|
|
535
565
|
* @return {Object}
|
|
@@ -540,6 +570,8 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
540
570
|
for (const setting of particleEffectSettings)
|
|
541
571
|
if (!particleEffectIndirect.includes(setting.name) && emitter[setting.name] !== undefined)
|
|
542
572
|
settings[setting.name] = emitter[setting.name]; // a Color is taken as its channels by the sanitizer
|
|
573
|
+
if (emitter.emitTime < 0) // destroyed, its last particles going: as long as it emitted, not 0, for ever
|
|
574
|
+
settings.emitTime = emitter.emitElapsed;
|
|
543
575
|
settings.emitRect = !emitter.emitCircle;
|
|
544
576
|
settings.emitSize = emitter.emitSize.x;
|
|
545
577
|
settings.emitHeight = emitter.emitSize.y;
|
|
@@ -630,6 +662,7 @@ function particleEffectSet3D(emitter, effect)
|
|
|
630
662
|
* @memberof ParticleEffects */
|
|
631
663
|
function particleEffect3D(nameOrEffect, pos3D=vec3(), options={})
|
|
632
664
|
{
|
|
665
|
+
options ||= {}; // null is no options too
|
|
633
666
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
634
667
|
if (!effect) return;
|
|
635
668
|
const e = new ParticleEmitter3D(pos3D.copy());
|
package/plugins/pathFinder.js
CHANGED
|
@@ -26,7 +26,8 @@ const PATHFINDER_TILE_VEC = vec2(1);
|
|
|
26
26
|
* @memberof PathFinding */
|
|
27
27
|
class PathFinderNode
|
|
28
28
|
{
|
|
29
|
-
/**
|
|
29
|
+
/** Make the node of a grid cell, as PathFinder does for each
|
|
30
|
+
* @param {number} x - Tile x
|
|
30
31
|
* @param {number} y - Tile y */
|
|
31
32
|
constructor(x, y)
|
|
32
33
|
{
|
|
@@ -63,10 +64,11 @@ class PathFinderNode
|
|
|
63
64
|
this.isOpen = this.isClosed = false;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
/** Reset per-search state and walkability (called by buildNodeData)
|
|
67
|
+
/** Reset per-search state and walkability (called by buildNodeData) */
|
|
67
68
|
reset() { this.walkable = false; this.cost = 0; this.resetSearch(); }
|
|
68
69
|
|
|
69
|
-
/** True if walkable and not blocked by cost
|
|
70
|
+
/** True if walkable and not blocked by cost
|
|
71
|
+
* @return {boolean} */
|
|
70
72
|
isClear()
|
|
71
73
|
{
|
|
72
74
|
return this.walkable && this.cost === 0;
|
|
@@ -75,7 +77,7 @@ class PathFinderNode
|
|
|
75
77
|
|
|
76
78
|
///////////////////////////////////////////////////////////////////////////////
|
|
77
79
|
|
|
78
|
-
/** Grid pathfinder using A* with two optional smoothing passes
|
|
80
|
+
/** Grid pathfinder using A* with two optional smoothing passes
|
|
79
81
|
* @memberof PathFinding
|
|
80
82
|
* @example
|
|
81
83
|
* // Tile-layer driven (most common):
|
|
@@ -88,7 +90,8 @@ class PathFinderNode
|
|
|
88
90
|
*/
|
|
89
91
|
class PathFinder
|
|
90
92
|
{
|
|
91
|
-
/**
|
|
93
|
+
/** Make a path finder over a tile layer or a grid
|
|
94
|
+
* @param {TileCollisionLayer|Vector2} source - Either a TileCollisionLayer
|
|
92
95
|
* (size and walkability auto-derived) or a Vector2 grid size (user
|
|
93
96
|
* overrides isWalkable). */
|
|
94
97
|
constructor(source)
|
|
@@ -157,7 +160,7 @@ class PathFinder
|
|
|
157
160
|
* the instance or via a subclass.
|
|
158
161
|
* @param {number} x - Tile x
|
|
159
162
|
* @param {number} y - Tile y
|
|
160
|
-
* @
|
|
163
|
+
* @return {boolean} */
|
|
161
164
|
isWalkable(x, y)
|
|
162
165
|
{
|
|
163
166
|
if (!this.tileLayer) return true;
|
|
@@ -168,25 +171,25 @@ class PathFinder
|
|
|
168
171
|
* Override to add cost-weighted terrain (mud, swamp, etc).
|
|
169
172
|
* @param {number} x - Tile x
|
|
170
173
|
* @param {number} y - Tile y
|
|
171
|
-
* @
|
|
174
|
+
* @return {number} */
|
|
172
175
|
getCost(x, y)
|
|
173
176
|
{
|
|
174
177
|
return 0;
|
|
175
178
|
}
|
|
176
179
|
|
|
177
|
-
/** Get the node at tile coords, or null if out of bounds
|
|
180
|
+
/** Get the node at tile coords, or null if out of bounds
|
|
178
181
|
* @param {number} x
|
|
179
182
|
* @param {number} y
|
|
180
|
-
* @
|
|
183
|
+
* @return {PathFinderNode|null} */
|
|
181
184
|
getNode(x, y)
|
|
182
185
|
{
|
|
183
186
|
if (x < 0 || y < 0 || x >= this.size.x || y >= this.size.y) return null;
|
|
184
187
|
return this.nodes[x + y * this.size.x];
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
/** Convert a world-space position to integer tile coords (no clamping)
|
|
190
|
+
/** Convert a world-space position to integer tile coords (no clamping)
|
|
188
191
|
* @param {Vector2} worldPos
|
|
189
|
-
* @
|
|
192
|
+
* @return {Vector2} */
|
|
190
193
|
worldToTile(worldPos)
|
|
191
194
|
{
|
|
192
195
|
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
@@ -194,10 +197,10 @@ class PathFinder
|
|
|
194
197
|
return vec2(floor(worldPos.x - ox), floor(worldPos.y - oy));
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
/** Convert integer tile coords to the world-space center of that tile
|
|
200
|
+
/** Convert integer tile coords to the world-space center of that tile
|
|
198
201
|
* @param {number} x
|
|
199
202
|
* @param {number} y
|
|
200
|
-
* @
|
|
203
|
+
* @return {Vector2} */
|
|
201
204
|
tileToWorld(x, y)
|
|
202
205
|
{
|
|
203
206
|
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
@@ -241,7 +244,7 @@ class PathFinder
|
|
|
241
244
|
* reached; false on disconnected goal or maxLoop exhaustion, which sets searchGaveUp.
|
|
242
245
|
* @param {PathFinderNode} startNode
|
|
243
246
|
* @param {PathFinderNode} endNode
|
|
244
|
-
* @
|
|
247
|
+
* @return {boolean}
|
|
245
248
|
* @private */
|
|
246
249
|
aStarSearch(startNode, endNode)
|
|
247
250
|
{
|
|
@@ -385,9 +388,9 @@ class PathFinder
|
|
|
385
388
|
* unchanged walkability, pass `rebuild=false` and call `buildNodeData()`
|
|
386
389
|
* once externally to avoid redundant work.
|
|
387
390
|
* @param {Vector2} worldPos
|
|
388
|
-
* @param {number} [searchRange
|
|
389
|
-
* @param {boolean} [rebuild
|
|
390
|
-
* @
|
|
391
|
+
* @param {number} [searchRange] - Max box-radius in tiles
|
|
392
|
+
* @param {boolean} [rebuild] - Whether to call buildNodeData first
|
|
393
|
+
* @return {PathFinderNode|null} */
|
|
391
394
|
getNearestClearNode(worldPos, searchRange = 10, rebuild = true)
|
|
392
395
|
{
|
|
393
396
|
ASSERT(isVector2(worldPos), 'worldPos must be a Vector2');
|
|
@@ -550,20 +553,36 @@ class PathFinder
|
|
|
550
553
|
{
|
|
551
554
|
if (path.length <= 2) return;
|
|
552
555
|
|
|
553
|
-
// Greedy: from each kept node, jump to the furthest
|
|
554
|
-
// line to it,
|
|
556
|
+
// Greedy: from each kept node, jump to the furthest corner with a clear
|
|
557
|
+
// line to it, then on along the straight run past it as far as one is
|
|
558
|
+
// clear, or else the next node. Every segment is one isLineClear
|
|
555
559
|
// accepted or one the path already had, so none can cross a wall.
|
|
556
|
-
|
|
560
|
+
// Corners first, so a winding path costs a check per corner and not
|
|
561
|
+
// per cell of it.
|
|
562
|
+
const original = path.slice(), last = original.length - 1;
|
|
563
|
+
const corners = [];
|
|
564
|
+
for (let i = 1; i < last; ++i)
|
|
565
|
+
{
|
|
566
|
+
const a = original[i - 1].pos, b = original[i].pos, c = original[i + 1].pos;
|
|
567
|
+
if ((b.x - a.x) * (c.y - a.y) !== (b.y - a.y) * (c.x - a.x))
|
|
568
|
+
corners.push(i);
|
|
569
|
+
}
|
|
570
|
+
corners.push(last);
|
|
557
571
|
path.length = 0;
|
|
558
572
|
path.push(original[0]);
|
|
559
|
-
for (let k = 0; k <
|
|
573
|
+
for (let k = 0; k < last;)
|
|
560
574
|
{
|
|
561
|
-
let j =
|
|
575
|
+
let j = k + 1;
|
|
562
576
|
if (original[k].isClear()) // isLineClear needs both ends clear
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
577
|
+
{
|
|
578
|
+
const clear = (i)=> original[i].isClear() && this.isLineClear(original[k].pos, original[i].pos);
|
|
579
|
+
let c = corners.length - 1, next = last + 1; // next is the corner past the one found
|
|
580
|
+
while (c >= 0 && corners[c] > k + 1 && !clear(corners[c]))
|
|
581
|
+
next = corners[c--];
|
|
582
|
+
j = c >= 0 && corners[c] > k + 1 ? corners[c] : k + 1;
|
|
583
|
+
for (let i = next - 1; i > j; --i)
|
|
584
|
+
if (clear(i)) { j = i; break; }
|
|
585
|
+
}
|
|
567
586
|
path.push(original[j]);
|
|
568
587
|
k = j;
|
|
569
588
|
}
|
|
@@ -578,20 +597,25 @@ class PathFinder
|
|
|
578
597
|
* @private */
|
|
579
598
|
dropCollinearNodes(path)
|
|
580
599
|
{
|
|
581
|
-
|
|
600
|
+
// one pass, a node kept only where the way turns from the last one kept
|
|
601
|
+
if (path.length < 3) return;
|
|
602
|
+
let kept = 1;
|
|
603
|
+
for (let i = 1; i < path.length - 1; ++i)
|
|
582
604
|
{
|
|
583
|
-
const a = path[
|
|
584
|
-
if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y)
|
|
605
|
+
const a = path[kept - 1], b = path[i], c = path[i + 1];
|
|
606
|
+
if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) !==
|
|
585
607
|
(b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
|
|
586
|
-
path
|
|
608
|
+
path[kept++] = b;
|
|
587
609
|
}
|
|
610
|
+
path[kept++] = path[path.length - 1];
|
|
611
|
+
path.length = kept;
|
|
588
612
|
}
|
|
589
613
|
|
|
590
614
|
/** Lookup helper: true when the node at tile coords (x, y) is in-bounds
|
|
591
615
|
* and clear (walkable, zero-cost). Used by isLineClear's hot path.
|
|
592
616
|
* @param {number} x
|
|
593
617
|
* @param {number} y
|
|
594
|
-
* @
|
|
618
|
+
* @return {boolean}
|
|
595
619
|
* @private */
|
|
596
620
|
isNodeClear(x, y)
|
|
597
621
|
{
|
|
@@ -608,7 +632,7 @@ class PathFinder
|
|
|
608
632
|
* CheckLine() in pathFinding.cpp.
|
|
609
633
|
* @param {Vector2} startPos - Tile coords
|
|
610
634
|
* @param {Vector2} endPos - Tile coords
|
|
611
|
-
* @
|
|
635
|
+
* @return {boolean}
|
|
612
636
|
* @private */
|
|
613
637
|
isLineClear(startPos, endPos)
|
|
614
638
|
{
|
|
@@ -731,7 +755,7 @@ class PathFinder
|
|
|
731
755
|
* @param {Vector2} startPos - World-space start
|
|
732
756
|
* @param {Vector2} endPos - World-space end
|
|
733
757
|
* @param {boolean} [rebuild] - Whether to call buildNodeData first
|
|
734
|
-
* @
|
|
758
|
+
* @return {Vector2[]} */
|
|
735
759
|
findPath(startPos, endPos, rebuild = true)
|
|
736
760
|
{
|
|
737
761
|
ASSERT(isVector2(startPos) && isVector2(endPos), 'findPath needs Vector2 endpoints');
|
|
@@ -798,13 +822,11 @@ function pathFinderNearestNode(finder, worldPos, searchRange, test)
|
|
|
798
822
|
const bound = max(0, offset - .5);
|
|
799
823
|
if (nearest && bound * bound >= nearestDistSq) break;
|
|
800
824
|
|
|
825
|
+
// only the ring itself, the inside was searched already: its top and bottom rows whole, its two ends between,
|
|
826
|
+
// in the same row by row order a full walk of the square visits them in, so ties pick the same node
|
|
801
827
|
for (let dy = -offset; dy <= offset; ++dy)
|
|
802
|
-
for (let dx = -offset; dx <= offset;
|
|
828
|
+
for (let dx = -offset, step = abs(dy) === offset ? 1 : 2*offset; dx <= offset; dx += step)
|
|
803
829
|
{
|
|
804
|
-
// only the ring itself, the inside was searched already
|
|
805
|
-
if (offset > 0 && abs(dx) !== offset && abs(dy) !== offset)
|
|
806
|
-
continue;
|
|
807
|
-
|
|
808
830
|
const node = finder.getNode(centerX + dx, centerY + dy);
|
|
809
831
|
if (!node || !test(node)) continue;
|
|
810
832
|
|
package/plugins/pluginExport.js
CHANGED
|
@@ -24,6 +24,11 @@ export
|
|
|
24
24
|
NewgroundsPlugin,
|
|
25
25
|
NewgroundsMedal,
|
|
26
26
|
|
|
27
|
+
// Wavedash
|
|
28
|
+
wavedash,
|
|
29
|
+
WavedashPlugin,
|
|
30
|
+
WavedashMedal,
|
|
31
|
+
|
|
27
32
|
// Post Process
|
|
28
33
|
postProcess,
|
|
29
34
|
PostProcessPlugin,
|
|
@@ -37,12 +42,15 @@ export
|
|
|
37
42
|
postProcessCurve,
|
|
38
43
|
postProcessChromatic,
|
|
39
44
|
postProcessOutline,
|
|
45
|
+
postProcessTiltShift,
|
|
46
|
+
postProcessDepthOfField,
|
|
40
47
|
postProcessTV,
|
|
41
48
|
|
|
42
49
|
// Light System
|
|
43
50
|
lightSystem,
|
|
44
51
|
LightSystemPlugin,
|
|
45
52
|
Light,
|
|
53
|
+
DirectionalLight,
|
|
46
54
|
|
|
47
55
|
// Audio Effects
|
|
48
56
|
AudioEffect,
|
|
@@ -149,6 +157,9 @@ export
|
|
|
149
157
|
// Render3D
|
|
150
158
|
render3D,
|
|
151
159
|
Render3DPlugin,
|
|
160
|
+
CubeMap,
|
|
161
|
+
makeCubeMap,
|
|
162
|
+
loadCubeMap,
|
|
152
163
|
Camera3D,
|
|
153
164
|
EngineObject3D,
|
|
154
165
|
Mesh,
|