littlejsengine 1.18.8 → 1.18.12

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.
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.8';
38
+ const engineVersion = '1.18.12';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -207,7 +207,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
207
207
  const combinedScale = timeScale * debugScale;
208
208
  frameTimeDeltaMS *= combinedScale;
209
209
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
210
- if (debugScale <= 1)
210
+ if (combinedScale <= 1)
211
211
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
212
212
 
213
213
  let wasUpdated = false;
@@ -294,6 +294,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
294
294
  glFlush();
295
295
  debugRenderPost();
296
296
  drawCount = 0;
297
+ primitiveCount = 0;
297
298
  }
298
299
  }
299
300
 
@@ -1769,6 +1770,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1769
1770
  * - File saving (text, canvas, data URLs)
1770
1771
  * - Native share dialog support
1771
1772
  * - Local storage save data management
1773
+ * - Gradient noise (1D and 2D)
1772
1774
  * @namespace Utilities
1773
1775
  */
1774
1776
 
@@ -1970,6 +1972,48 @@ function writeSaveData(saveName, saveData)
1970
1972
  {
1971
1973
  ASSERT(isStringLike(saveName), 'saveData requires saveName string');
1972
1974
  localStorage[saveName] = JSON.stringify(saveData);
1975
+ }
1976
+
1977
+ ///////////////////////////////////////////////////////////////////////////////
1978
+
1979
+ // Deterministic well-distributed hash of an integer lattice index to [0, 1).
1980
+ // Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
1981
+ function noiseHash(i)
1982
+ {
1983
+ let h = (i | 0) ^ 0x9e3779b9;
1984
+ h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
1985
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
1986
+ h ^= h >>> 16;
1987
+ return (h >>> 0) / 2**32;
1988
+ }
1989
+
1990
+ /** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
1991
+ * Integer inputs land on deterministic lattice values; non-integer inputs
1992
+ * are interpolated with smoothStep for C1 continuity.
1993
+ * @param {number} x
1994
+ * @return {number}
1995
+ * @memberof Utilities */
1996
+ function noise1D(x)
1997
+ {
1998
+ const i = floor(x);
1999
+ return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
2000
+ }
2001
+
2002
+ /** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
2003
+ * @param {number} x
2004
+ * @param {number} y
2005
+ * @return {number}
2006
+ * @memberof Utilities */
2007
+ function noise2D(x, y)
2008
+ {
2009
+ const ix = floor(x), iy = floor(y);
2010
+ const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
2011
+ // large prime decorrelates neighboring rows
2012
+ const h = (a, b) => noiseHash(a + b * 374761393);
2013
+ return lerp(
2014
+ lerp(h(ix, iy ), h(ix + 1, iy ), fx),
2015
+ lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
2016
+ fy);
1973
2017
  }
1974
2018
  /**
1975
2019
  * LittleJS Engine Settings
@@ -3176,6 +3220,12 @@ let textureInfos = [];
3176
3220
  * @memberof Draw */
3177
3221
  let drawCount;
3178
3222
 
3223
+ /** Keeps track of how many primitives were drawn each frame for debugging
3224
+ * A single draw call can render many primitives (e.g. a WebGL sprite batch).
3225
+ * @type {number}
3226
+ * @memberof Draw */
3227
+ let primitiveCount;
3228
+
3179
3229
  // internal predicates for tint short-circuiting in canvas2D draw paths
3180
3230
  // isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
3181
3231
  // isBlack includes alpha so additive colors that only contribute alpha are not skipped
@@ -3416,19 +3466,19 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
3416
3466
  }
3417
3467
  else
3418
3468
  {
3419
- // if no tile info, force untextured by zeroing rgba (so whatever
3420
- // texture is bound doesn't leak in) and folding color+additive
3421
- // into the additive slot matches the Canvas2D path's
3422
- // color.add(additiveColor) on line ~337.
3469
+ // untextured: glDrawUntextured picks the optimal path (poly
3470
+ // tristrip if already in poly mode, otherwise instanced with
3471
+ // uvs/rgba zeroed). Color+additive are folded together to match
3472
+ // the Canvas2D path's color.add(additiveColor) on line ~337.
3423
3473
  const combined = additiveColor ? color.add(additiveColor) : color;
3424
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
3425
- 0, combined.rgbaInt());
3474
+ glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
3426
3475
  }
3427
3476
  }
3428
3477
  else
3429
3478
  {
3430
3479
  // normal canvas 2D rendering method (slower)
3431
3480
  ++drawCount;
3481
+ ++primitiveCount;
3432
3482
  size = new Vector2(size.x, -size.y); // flip upside down sprites
3433
3483
  drawCanvas2D(pos, size, angle, mirror, (context)=>
3434
3484
  {
@@ -3468,13 +3518,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3468
3518
  * @param {Vector2} pos
3469
3519
  * @param {Vector2} [size=vec2(1)]
3470
3520
  * @param {Color} [colorTop=WHITE]
3471
- * @param {Color} [colorBottom=BLACK]
3521
+ * @param {Color} [colorBottom=CLEAR_WHITE]
3472
3522
  * @param {number} [angle]
3473
3523
  * @param {boolean} [useWebGL=glEnable]
3474
3524
  * @param {boolean} [screenSpace]
3475
3525
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3476
3526
  * @memberof Draw */
3477
- function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3527
+ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
3478
3528
  {
3479
3529
  ASSERT(isVector2(pos), 'pos must be a vec2');
3480
3530
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -3514,6 +3564,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3514
3564
  {
3515
3565
  // normal canvas 2D rendering method (slower)
3516
3566
  ++drawCount;
3567
+ ++primitiveCount;
3517
3568
  size = new Vector2(size.x, -size.y); // fix upside down sprites
3518
3569
  drawCanvas2D(pos, size, angle, false, (context)=>
3519
3570
  {
@@ -3576,8 +3627,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
3576
3627
  return;
3577
3628
  }
3578
3629
 
3579
- // Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
3630
+ // Canvas2D path — increment counts here (WebGL counts via glFlush)
3580
3631
  ++drawCount;
3632
+ ++primitiveCount;
3581
3633
 
3582
3634
  if (!screenSpace)
3583
3635
  {
@@ -3651,6 +3703,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3651
3703
  {
3652
3704
  // normal canvas 2D rendering method (slower)
3653
3705
  ++drawCount;
3706
+ ++primitiveCount;
3654
3707
  drawCanvas2D(pos, vec2(1), angle, false, (context)=>
3655
3708
  {
3656
3709
  context.strokeStyle = color.toString();
@@ -3831,6 +3884,77 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
3831
3884
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3832
3885
  }
3833
3886
 
3887
+ /** Draw a circle filled with a radial gradient from the center to the rim
3888
+ * - Best when batched with other untextured polys
3889
+ * - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
3890
+ * - Stacking gradients at the exact same position may show a faint vertical artifact
3891
+ * @param {Vector2} pos
3892
+ * @param {number} [size=1] - Diameter
3893
+ * @param {Color} [colorInner=WHITE]
3894
+ * @param {Color} [colorOuter=CLEAR_WHITE]
3895
+ * @param {boolean} [useWebGL=glEnable]
3896
+ * @param {boolean} [screenSpace]
3897
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3898
+ * @memberof Draw */
3899
+ let drawCircleGradientOffset = 0;
3900
+ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
3901
+ {
3902
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3903
+ ASSERT(isNumber(size), 'size must be a number');
3904
+ ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
3905
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3906
+
3907
+ if (headlessMode) return;
3908
+
3909
+ if (useWebGL && glEnable)
3910
+ {
3911
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3912
+ if (screenSpace)
3913
+ {
3914
+ // convert to world space
3915
+ pos = screenToWorld(pos);
3916
+ size /= cameraScale;
3917
+ }
3918
+ // fan as tristrip; rotate the boundary vertex by one slice per call
3919
+ // so back-to-back gradients at the same position have their hole
3920
+ // (from gpu edge-rule on the boundary line-degen) at different rim
3921
+ // verts and don't visibly stack
3922
+ const sides = glCircleSides;
3923
+ const radius = size/2;
3924
+ const innerInt = colorInner.rgbaInt();
3925
+ const outerInt = colorOuter.rgbaInt();
3926
+ const offset = drawCircleGradientOffset++;
3927
+ const startA = (offset%sides)/sides*PI*2;
3928
+ const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
3929
+ const colors = [outerInt];
3930
+ for (let i=sides; i--;)
3931
+ {
3932
+ const a = ((i+offset)%sides)/sides*PI*2;
3933
+ points.push(pos);
3934
+ colors.push(innerInt);
3935
+ points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));
3936
+ colors.push(outerInt);
3937
+ }
3938
+ glDrawColoredPoints(points, colors);
3939
+ }
3940
+ else
3941
+ {
3942
+ // normal canvas 2D rendering method (slower)
3943
+ ++drawCount;
3944
+ ++primitiveCount;
3945
+ drawCanvas2D(pos, vec2(size), 0, false, (context)=>
3946
+ {
3947
+ const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
3948
+ gradient.addColorStop(0, colorInner.toString());
3949
+ gradient.addColorStop(1, colorOuter.toString());
3950
+ context.fillStyle = gradient;
3951
+ context.beginPath();
3952
+ context.ellipse(0, 0, .5, .5, 0, 0, 9);
3953
+ context.fill();
3954
+ }, screenSpace, context);
3955
+ }
3956
+ }
3957
+
3834
3958
  /**
3835
3959
  * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
3836
3960
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
@@ -5676,14 +5800,15 @@ class SoundInstance
5676
5800
 
5677
5801
  /** Speak text with passed in settings
5678
5802
  * @param {string} text - The text to speak
5679
- * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
5680
5803
  * @param {number} [volume] - How much to scale volume by
5681
5804
  * @param {number} [rate] - How quickly to speak
5682
5805
  * @param {number} [pitch] - How much to change the pitch by
5806
+ * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
5683
5807
  * @return {SpeechSynthesisUtterance} - The utterance that was spoken
5684
5808
  * @memberof Audio */
5685
- function speak(text, language='', volume=1, rate=1, pitch=1)
5809
+ function speak(text, volume=1, rate=1, pitch=1, language='')
5686
5810
  {
5811
+ ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
5687
5812
  if (!soundEnable || headlessMode) return;
5688
5813
  if (!speechSynthesis) return;
5689
5814
 
@@ -7676,7 +7801,8 @@ function glFlush()
7676
7801
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
7677
7802
  else
7678
7803
  glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
7679
- drawCount += glBatchCount;
7804
+ ++drawCount;
7805
+ primitiveCount += glBatchCount;
7680
7806
  glBatchCount = 0;
7681
7807
  }
7682
7808
  glBatchAdditive = glAdditive;
@@ -7737,6 +7863,67 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
7737
7863
  glPositionData[offset++] = angle;
7738
7864
  }
7739
7865
 
7866
+ /** Add an untextured rect to the gl draw list
7867
+ * Picks the optimal path: if already in poly mode, emits a tristrip rect
7868
+ * so it batches with surrounding polys; otherwise uses the instanced path
7869
+ * with uvs and rgba zeroed so the color falls through the additive slot.
7870
+ * @param {number} x
7871
+ * @param {number} y
7872
+ * @param {number} sizeX
7873
+ * @param {number} sizeY
7874
+ * @param {number} angle
7875
+ * @param {number} rgba - color as 32-bit integer
7876
+ * @memberof WebGL */
7877
+ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
7878
+ {
7879
+ if (glPolyMode)
7880
+ {
7881
+ // batch with surrounding polys as a 4-vertex tristrip rect
7882
+ const vertCount = 6; // 4 corners + 2 degenerate verts
7883
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
7884
+ glFlush();
7885
+
7886
+ // compute rotated corners in world space (matches glDrawPointsTransform rotation)
7887
+ const hx = sizeX*.5, hy = sizeY*.5;
7888
+ const c = cos(angle), s = sin(angle);
7889
+ const chx = c*hx, shx = s*hx, chy = c*hy, shy = s*hy;
7890
+ const x0 = x - chx - shy, y0 = y + shx - chy; // (-hx,-hy)
7891
+ const x1 = x + chx - shy, y1 = y - shx - chy; // ( hx,-hy)
7892
+ const x2 = x - chx + shy, y2 = y + shx + chy; // (-hx, hy)
7893
+ const x3 = x + chx + shy, y3 = y - shx + chy; // ( hx, hy)
7894
+
7895
+ // write tristrip with leading/trailing degenerate verts
7896
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
7897
+ glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
7898
+ glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
7899
+ glPositionData[offset++] = x1; glPositionData[offset++] = y1; glColorData[offset++] = rgba;
7900
+ glPositionData[offset++] = x2; glPositionData[offset++] = y2; glColorData[offset++] = rgba;
7901
+ glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
7902
+ glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
7903
+ glBatchCount += vertCount;
7904
+ return;
7905
+ }
7906
+
7907
+ // instanced path: zero uvs and rgba so the texture contribution is killed,
7908
+ // then carry the real color in the additive slot
7909
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
7910
+ glFlush();
7911
+ glSetInstancedMode();
7912
+
7913
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
7914
+ glPositionData[offset++] = x;
7915
+ glPositionData[offset++] = y;
7916
+ glPositionData[offset++] = sizeX;
7917
+ glPositionData[offset++] = sizeY;
7918
+ glPositionData[offset++] = 0;
7919
+ glPositionData[offset++] = 0;
7920
+ glPositionData[offset++] = 0;
7921
+ glPositionData[offset++] = 0;
7922
+ glColorData[offset++] = 0;
7923
+ glColorData[offset++] = rgba;
7924
+ glPositionData[offset++] = angle;
7925
+ }
7926
+
7740
7927
  /** Transform and add a polygon to the gl draw list
7741
7928
  * @param {Array<Vector2>} points - Array of Vector2 points
7742
7929
  * @param {number} rgba - Color of the polygon as a 32-bit integer
@@ -8373,7 +8560,8 @@ class Medal
8373
8560
  /** @property {boolean} - Is the medal unlocked? */
8374
8561
  this.unlocked = false;
8375
8562
 
8376
- // load the source image if provided
8563
+ /** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
8564
+ this.image = undefined;
8377
8565
  if (src)
8378
8566
  (this.image = new Image).src = src;
8379
8567
 
@@ -8536,13 +8724,18 @@ class NewgroundsPlugin
8536
8724
  ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
8537
8725
 
8538
8726
  newgrounds = this; // set global newgrounds object
8727
+ /** @property {string} - The newgrounds App ID */
8539
8728
  this.app_id = app_id;
8729
+ /** @property {string|undefined} - AES-128/Base64 encryption key, if any */
8540
8730
  this.cipher = cipher;
8731
+ /** @property {Object|undefined} - CryptoJS instance used when cipher is set */
8541
8732
  this.cryptoJS = cryptoJS;
8733
+ /** @property {string} - Hostname used when logging views */
8542
8734
  this.host = location ? location.hostname : '';
8543
8735
 
8544
8736
  // get session id from url search params
8545
8737
  const url = new URL(location.href);
8738
+ /** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
8546
8739
  this.session_id = url.searchParams.get('ngio_session_id');
8547
8740
 
8548
8741
  if (!this.session_id)
@@ -8550,6 +8743,7 @@ class NewgroundsPlugin
8550
8743
 
8551
8744
  // get medals
8552
8745
  const medalsResult = this.call('Medal.getList');
8746
+ /** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
8553
8747
  this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
8554
8748
  debugMedals && LOG(this.medals);
8555
8749
  for (const newgroundsMedal of this.medals)
@@ -8573,6 +8767,7 @@ class NewgroundsPlugin
8573
8767
 
8574
8768
  // get scoreboards
8575
8769
  const scoreboardResult = this.call('ScoreBoard.getBoards');
8770
+ /** @property {Array} - Scoreboards fetched from Newgrounds */
8576
8771
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
8577
8772
  debugMedals && LOG(this.scoreboards);
8578
8773
 
@@ -12885,13 +13080,21 @@ class Tween
12885
13080
  }
12886
13081
  ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
12887
13082
 
13083
+ /** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
12888
13084
  this.callback = callback;
13085
+ /** @property {number|Vector2|Color} - Starting value */
12889
13086
  this.start = start;
13087
+ /** @property {number|Vector2|Color} - Ending value */
12890
13088
  this.end = end;
13089
+ /** @property {number} - Total duration in seconds */
12891
13090
  this.duration = duration;
13091
+ /** @property {number} - Remaining time in seconds (counts down from duration to 0) */
12892
13092
  this.life = duration;
13093
+ /** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
12893
13094
  this.ease = options.ease || Ease.LINEAR;
13095
+ /** @property {boolean} - If true, advance even when the game is paused */
12894
13096
  this.useRealTime = !!options.useRealTime;
13097
+ /** @property {boolean} - If true, stop advancing until cleared */
12895
13098
  this.paused = !!options.paused;
12896
13099
 
12897
13100
  /** @private completion callback set by then(), loop(), pingPong(). */
@@ -13424,7 +13627,9 @@ class PathFinder
13424
13627
  // .size + .getCollisionData.
13425
13628
  if (isVector2(source))
13426
13629
  {
13630
+ /** @property {Vector2} - Grid dimensions in tiles */
13427
13631
  this.size = source.floor();
13632
+ /** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
13428
13633
  this.tileLayer = undefined;
13429
13634
  }
13430
13635
  else
@@ -13436,13 +13641,18 @@ class PathFinder
13436
13641
  }
13437
13642
 
13438
13643
  // Tunables (public, freely re-assignable).
13644
+ /** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
13439
13645
  this.heuristicWeight = 1;
13440
- this.maxLoop = 500;
13646
+ /** @property {number} - Maximum A* expansions before giving up */
13647
+ this.maxLoop = 1e3;
13648
+ /** @property {boolean} - If true, post-process paths with two-pass smoothing */
13441
13649
  this.smoothPath = true;
13650
+ /** @property {boolean} - If true, draw debug visualization during findPath */
13442
13651
  this.debug = false;
13443
- this.debugTime = 2;
13652
+ /** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
13653
+ this.debugTime = 1;
13444
13654
 
13445
- // Pre-allocate the node array one node per tile, reused across calls.
13655
+ /** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
13446
13656
  this.nodes = new Array(this.size.x * this.size.y);
13447
13657
  for (let y = 0; y < this.size.y; ++y)
13448
13658
  for (let x = 0; x < this.size.x; ++x)
@@ -13616,9 +13826,12 @@ class PathFinder
13616
13826
  // Best path so far through neighbor — record it.
13617
13827
  neighbor.parent = current;
13618
13828
  neighbor.g = tentativeG;
13619
- const gdx = endNode.pos.x - neighbor.pos.x;
13620
- const gdy = endNode.pos.y - neighbor.pos.y;
13621
- neighbor.f = neighbor.g + (gdx * gdx + gdy * gdy) * this.heuristicWeight;
13829
+ // Octile heuristic tightest admissible distance for an
13830
+ // 8-connected grid with cardinal cost 1 and diagonal cost √2.
13831
+ const adx = abs(endNode.pos.x - neighbor.pos.x);
13832
+ const ady = abs(endNode.pos.y - neighbor.pos.y);
13833
+ const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
13834
+ neighbor.f = neighbor.g + h * this.heuristicWeight;
13622
13835
  }
13623
13836
  }
13624
13837
 
@@ -13900,6 +14113,24 @@ class PathFinder
13900
14113
  path.push(original[original.length - 1]);
13901
14114
  }
13902
14115
 
14116
+ /** Drop any middle node that lies exactly on the line through its two
14117
+ * neighbors. Backstop for the smoothing passes — the corners pass
14118
+ * intentionally keeps truly-straight runs, and the string-pulling pass
14119
+ * checks collinearity against the original path, not the in-progress
14120
+ * result, so it can leave 3+ collinear nodes in some edge cases.
14121
+ * @param {PathFinderNode[]} path
14122
+ * @private */
14123
+ dropCollinearNodes(path)
14124
+ {
14125
+ for (let i = path.length - 2; i >= 1; --i)
14126
+ {
14127
+ const a = path[i - 1], b = path[i], c = path[i + 1];
14128
+ if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
14129
+ (b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
14130
+ path.splice(i, 1);
14131
+ }
14132
+ }
14133
+
13903
14134
  /** Lookup helper: true when the node at tile coords (x, y) is in-bounds
13904
14135
  * and clear (walkable, zero-cost). Used by isLineClear's hot path.
13905
14136
  * @param {number} x
@@ -14067,6 +14298,7 @@ class PathFinder
14067
14298
  {
14068
14299
  this.smoothPathCorners(nodePath);
14069
14300
  this.smoothPathStringPull(nodePath);
14301
+ this.dropCollinearNodes(nodePath);
14070
14302
  }
14071
14303
 
14072
14304
  // Convert to world-space Vector2 path. Return copies, not live node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.8",
3
+ "version": "1.18.12",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
@@ -147,7 +147,8 @@ class Medal
147
147
  /** @property {boolean} - Is the medal unlocked? */
148
148
  this.unlocked = false;
149
149
 
150
- // load the source image if provided
150
+ /** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
151
+ this.image = undefined;
151
152
  if (src)
152
153
  (this.image = new Image).src = src;
153
154
 
@@ -63,13 +63,18 @@ class NewgroundsPlugin
63
63
  ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
64
64
 
65
65
  newgrounds = this; // set global newgrounds object
66
+ /** @property {string} - The newgrounds App ID */
66
67
  this.app_id = app_id;
68
+ /** @property {string|undefined} - AES-128/Base64 encryption key, if any */
67
69
  this.cipher = cipher;
70
+ /** @property {Object|undefined} - CryptoJS instance used when cipher is set */
68
71
  this.cryptoJS = cryptoJS;
72
+ /** @property {string} - Hostname used when logging views */
69
73
  this.host = location ? location.hostname : '';
70
74
 
71
75
  // get session id from url search params
72
76
  const url = new URL(location.href);
77
+ /** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
73
78
  this.session_id = url.searchParams.get('ngio_session_id');
74
79
 
75
80
  if (!this.session_id)
@@ -77,6 +82,7 @@ class NewgroundsPlugin
77
82
 
78
83
  // get medals
79
84
  const medalsResult = this.call('Medal.getList');
85
+ /** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
80
86
  this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
81
87
  debugMedals && LOG(this.medals);
82
88
  for (const newgroundsMedal of this.medals)
@@ -100,6 +106,7 @@ class NewgroundsPlugin
100
106
 
101
107
  // get scoreboards
102
108
  const scoreboardResult = this.call('ScoreBoard.getBoards');
109
+ /** @property {Array} - Scoreboards fetched from Newgrounds */
103
110
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
104
111
  debugMedals && LOG(this.scoreboards);
105
112
 
@@ -94,7 +94,9 @@ class PathFinder
94
94
  // .size + .getCollisionData.
95
95
  if (isVector2(source))
96
96
  {
97
+ /** @property {Vector2} - Grid dimensions in tiles */
97
98
  this.size = source.floor();
99
+ /** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
98
100
  this.tileLayer = undefined;
99
101
  }
100
102
  else
@@ -106,13 +108,18 @@ class PathFinder
106
108
  }
107
109
 
108
110
  // Tunables (public, freely re-assignable).
111
+ /** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
109
112
  this.heuristicWeight = 1;
110
- this.maxLoop = 500;
113
+ /** @property {number} - Maximum A* expansions before giving up */
114
+ this.maxLoop = 1e3;
115
+ /** @property {boolean} - If true, post-process paths with two-pass smoothing */
111
116
  this.smoothPath = true;
117
+ /** @property {boolean} - If true, draw debug visualization during findPath */
112
118
  this.debug = false;
113
- this.debugTime = 2;
119
+ /** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
120
+ this.debugTime = 1;
114
121
 
115
- // Pre-allocate the node array one node per tile, reused across calls.
122
+ /** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
116
123
  this.nodes = new Array(this.size.x * this.size.y);
117
124
  for (let y = 0; y < this.size.y; ++y)
118
125
  for (let x = 0; x < this.size.x; ++x)
@@ -286,9 +293,12 @@ class PathFinder
286
293
  // Best path so far through neighbor — record it.
287
294
  neighbor.parent = current;
288
295
  neighbor.g = tentativeG;
289
- const gdx = endNode.pos.x - neighbor.pos.x;
290
- const gdy = endNode.pos.y - neighbor.pos.y;
291
- neighbor.f = neighbor.g + (gdx * gdx + gdy * gdy) * this.heuristicWeight;
296
+ // Octile heuristic tightest admissible distance for an
297
+ // 8-connected grid with cardinal cost 1 and diagonal cost √2.
298
+ const adx = abs(endNode.pos.x - neighbor.pos.x);
299
+ const ady = abs(endNode.pos.y - neighbor.pos.y);
300
+ const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
301
+ neighbor.f = neighbor.g + h * this.heuristicWeight;
292
302
  }
293
303
  }
294
304
 
@@ -570,6 +580,24 @@ class PathFinder
570
580
  path.push(original[original.length - 1]);
571
581
  }
572
582
 
583
+ /** Drop any middle node that lies exactly on the line through its two
584
+ * neighbors. Backstop for the smoothing passes — the corners pass
585
+ * intentionally keeps truly-straight runs, and the string-pulling pass
586
+ * checks collinearity against the original path, not the in-progress
587
+ * result, so it can leave 3+ collinear nodes in some edge cases.
588
+ * @param {PathFinderNode[]} path
589
+ * @private */
590
+ dropCollinearNodes(path)
591
+ {
592
+ for (let i = path.length - 2; i >= 1; --i)
593
+ {
594
+ const a = path[i - 1], b = path[i], c = path[i + 1];
595
+ if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
596
+ (b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
597
+ path.splice(i, 1);
598
+ }
599
+ }
600
+
573
601
  /** Lookup helper: true when the node at tile coords (x, y) is in-bounds
574
602
  * and clear (walkable, zero-cost). Used by isLineClear's hot path.
575
603
  * @param {number} x
@@ -737,6 +765,7 @@ class PathFinder
737
765
  {
738
766
  this.smoothPathCorners(nodePath);
739
767
  this.smoothPathStringPull(nodePath);
768
+ this.dropCollinearNodes(nodePath);
740
769
  }
741
770
 
742
771
  // Convert to world-space Vector2 path. Return copies, not live node
@@ -63,13 +63,21 @@ class Tween
63
63
  }
64
64
  ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
65
65
 
66
+ /** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
66
67
  this.callback = callback;
68
+ /** @property {number|Vector2|Color} - Starting value */
67
69
  this.start = start;
70
+ /** @property {number|Vector2|Color} - Ending value */
68
71
  this.end = end;
72
+ /** @property {number} - Total duration in seconds */
69
73
  this.duration = duration;
74
+ /** @property {number} - Remaining time in seconds (counts down from duration to 0) */
70
75
  this.life = duration;
76
+ /** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
71
77
  this.ease = options.ease || Ease.LINEAR;
78
+ /** @property {boolean} - If true, advance even when the game is paused */
72
79
  this.useRealTime = !!options.useRealTime;
80
+ /** @property {boolean} - If true, stop advancing until cleared */
73
81
  this.paused = !!options.paused;
74
82
 
75
83
  /** @private completion callback set by then(), loop(), pingPong(). */
package/src/engine.js CHANGED
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
32
32
  * @type {string}
33
33
  * @default
34
34
  * @memberof Engine */
35
- const engineVersion = '1.18.8';
35
+ const engineVersion = '1.18.12';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -204,7 +204,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
204
204
  const combinedScale = timeScale * debugScale;
205
205
  frameTimeDeltaMS *= combinedScale;
206
206
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
207
- if (debugScale <= 1)
207
+ if (combinedScale <= 1)
208
208
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
209
209
 
210
210
  let wasUpdated = false;
@@ -291,6 +291,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
291
291
  glFlush();
292
292
  debugRenderPost();
293
293
  drawCount = 0;
294
+ primitiveCount = 0;
294
295
  }
295
296
  }
296
297