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.
Files changed (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -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, they are read when those medals are made
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
- ASSERT(medalsSaveName, 'save name must be set');
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
  }
@@ -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 cancelled
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()
@@ -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', 0, 1e9);
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', 0, 1e9);
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
- const name = text ? text.slice(0, 60).trim() : 'Effect'; // no space left where it was cut
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 grey and white have no hue to turn
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 grey, clamped to 1
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
- // any setting in the options replaces the effect's own for this play, emitTime for a burst or a loop
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
- if (options[setting.name] !== undefined)
492
- settings[setting.name] = options[setting.name];
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 grey;
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 is its
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());
@@ -26,7 +26,8 @@ const PATHFINDER_TILE_VEC = vec2(1);
26
26
  * @memberof PathFinding */
27
27
  class PathFinderNode
28
28
  {
29
- /** @param {number} x - Tile x
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
- /** @param {TileCollisionLayer|Vector2} source - Either a TileCollisionLayer
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
- * @returns {boolean} */
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
- * @returns {number} */
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
- * @returns {PathFinderNode|null} */
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
- * @returns {Vector2} */
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
- * @returns {Vector2} */
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
- * @returns {boolean}
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=10] - Max box-radius in tiles
389
- * @param {boolean} [rebuild=true] - Whether to call buildNodeData first
390
- * @returns {PathFinderNode|null} */
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 node with a clear
554
- // line to it, or else the next node. Every segment is one isLineClear
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
- const original = path.slice();
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 < original.length - 1;)
573
+ for (let k = 0; k < last;)
560
574
  {
561
- let j = original.length - 1;
575
+ let j = k + 1;
562
576
  if (original[k].isClear()) // isLineClear needs both ends clear
563
- while (j > k + 1 && !(original[j].isClear() && this.isLineClear(original[k].pos, original[j].pos)))
564
- --j;
565
- else
566
- j = k + 1;
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
- for (let i = path.length - 2; i >= 1; --i)
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[i - 1], b = path[i], c = path[i + 1];
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.splice(i, 1);
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
- * @returns {boolean}
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
- * @returns {boolean}
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
- * @returns {Vector2[]} */
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; ++dx)
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
 
@@ -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,