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
 
@@ -1068,7 +1069,8 @@ function debugRender()
1068
1069
  debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
1069
1070
  x, y += h);
1070
1071
  debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
1071
- debugContext.fillText('Draw Count: ' + drawCount, x, y += h);
1072
+ debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
1073
+ debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
1072
1074
  debugContext.fillText('---------', x, y += h);
1073
1075
  debugContext.fillStyle = '#f00';
1074
1076
  debugContext.fillText('ESC: Debug Overlay', x, y += h);
@@ -1139,7 +1141,8 @@ function debugRenderPost()
1139
1141
  mainContext.font = '1em monospace';
1140
1142
  mainContext.fillStyle = '#000';
1141
1143
  const text = engineName + ' v' + engineVersion + ' / '
1142
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
1144
+ + drawCount + ' / ' + primitiveCount + ' / '
1145
+ + engineObjects.length + ' / ' + averageFPS.toFixed(1)
1143
1146
  + (glEnable ? ' GL' : ' 2D') ;
1144
1147
  mainContext.fillText(text, mainCanvas.width-3, 3);
1145
1148
  mainContext.fillStyle = '#fff';
@@ -2449,6 +2452,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2449
2452
  * - File saving (text, canvas, data URLs)
2450
2453
  * - Native share dialog support
2451
2454
  * - Local storage save data management
2455
+ * - Gradient noise (1D and 2D)
2452
2456
  * @namespace Utilities
2453
2457
  */
2454
2458
 
@@ -2650,6 +2654,48 @@ function writeSaveData(saveName, saveData)
2650
2654
  {
2651
2655
  ASSERT(isStringLike(saveName), 'saveData requires saveName string');
2652
2656
  localStorage[saveName] = JSON.stringify(saveData);
2657
+ }
2658
+
2659
+ ///////////////////////////////////////////////////////////////////////////////
2660
+
2661
+ // Deterministic well-distributed hash of an integer lattice index to [0, 1).
2662
+ // Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
2663
+ function noiseHash(i)
2664
+ {
2665
+ let h = (i | 0) ^ 0x9e3779b9;
2666
+ h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
2667
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
2668
+ h ^= h >>> 16;
2669
+ return (h >>> 0) / 2**32;
2670
+ }
2671
+
2672
+ /** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
2673
+ * Integer inputs land on deterministic lattice values; non-integer inputs
2674
+ * are interpolated with smoothStep for C1 continuity.
2675
+ * @param {number} x
2676
+ * @return {number}
2677
+ * @memberof Utilities */
2678
+ function noise1D(x)
2679
+ {
2680
+ const i = floor(x);
2681
+ return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
2682
+ }
2683
+
2684
+ /** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
2685
+ * @param {number} x
2686
+ * @param {number} y
2687
+ * @return {number}
2688
+ * @memberof Utilities */
2689
+ function noise2D(x, y)
2690
+ {
2691
+ const ix = floor(x), iy = floor(y);
2692
+ const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
2693
+ // large prime decorrelates neighboring rows
2694
+ const h = (a, b) => noiseHash(a + b * 374761393);
2695
+ return lerp(
2696
+ lerp(h(ix, iy ), h(ix + 1, iy ), fx),
2697
+ lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
2698
+ fy);
2653
2699
  }
2654
2700
  /**
2655
2701
  * LittleJS Engine Settings
@@ -3856,6 +3902,12 @@ let textureInfos = [];
3856
3902
  * @memberof Draw */
3857
3903
  let drawCount;
3858
3904
 
3905
+ /** Keeps track of how many primitives were drawn each frame for debugging
3906
+ * A single draw call can render many primitives (e.g. a WebGL sprite batch).
3907
+ * @type {number}
3908
+ * @memberof Draw */
3909
+ let primitiveCount;
3910
+
3859
3911
  // internal predicates for tint short-circuiting in canvas2D draw paths
3860
3912
  // isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
3861
3913
  // isBlack includes alpha so additive colors that only contribute alpha are not skipped
@@ -4096,19 +4148,19 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
4096
4148
  }
4097
4149
  else
4098
4150
  {
4099
- // if no tile info, force untextured by zeroing rgba (so whatever
4100
- // texture is bound doesn't leak in) and folding color+additive
4101
- // into the additive slot matches the Canvas2D path's
4102
- // color.add(additiveColor) on line ~337.
4151
+ // untextured: glDrawUntextured picks the optimal path (poly
4152
+ // tristrip if already in poly mode, otherwise instanced with
4153
+ // uvs/rgba zeroed). Color+additive are folded together to match
4154
+ // the Canvas2D path's color.add(additiveColor) on line ~337.
4103
4155
  const combined = additiveColor ? color.add(additiveColor) : color;
4104
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
4105
- 0, combined.rgbaInt());
4156
+ glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
4106
4157
  }
4107
4158
  }
4108
4159
  else
4109
4160
  {
4110
4161
  // normal canvas 2D rendering method (slower)
4111
4162
  ++drawCount;
4163
+ ++primitiveCount;
4112
4164
  size = new Vector2(size.x, -size.y); // flip upside down sprites
4113
4165
  drawCanvas2D(pos, size, angle, mirror, (context)=>
4114
4166
  {
@@ -4148,13 +4200,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
4148
4200
  * @param {Vector2} pos
4149
4201
  * @param {Vector2} [size=vec2(1)]
4150
4202
  * @param {Color} [colorTop=WHITE]
4151
- * @param {Color} [colorBottom=BLACK]
4203
+ * @param {Color} [colorBottom=CLEAR_WHITE]
4152
4204
  * @param {number} [angle]
4153
4205
  * @param {boolean} [useWebGL=glEnable]
4154
4206
  * @param {boolean} [screenSpace]
4155
4207
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4156
4208
  * @memberof Draw */
4157
- function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
4209
+ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
4158
4210
  {
4159
4211
  ASSERT(isVector2(pos), 'pos must be a vec2');
4160
4212
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -4194,6 +4246,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4194
4246
  {
4195
4247
  // normal canvas 2D rendering method (slower)
4196
4248
  ++drawCount;
4249
+ ++primitiveCount;
4197
4250
  size = new Vector2(size.x, -size.y); // fix upside down sprites
4198
4251
  drawCanvas2D(pos, size, angle, false, (context)=>
4199
4252
  {
@@ -4256,8 +4309,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
4256
4309
  return;
4257
4310
  }
4258
4311
 
4259
- // Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
4312
+ // Canvas2D path — increment counts here (WebGL counts via glFlush)
4260
4313
  ++drawCount;
4314
+ ++primitiveCount;
4261
4315
 
4262
4316
  if (!screenSpace)
4263
4317
  {
@@ -4331,6 +4385,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
4331
4385
  {
4332
4386
  // normal canvas 2D rendering method (slower)
4333
4387
  ++drawCount;
4388
+ ++primitiveCount;
4334
4389
  drawCanvas2D(pos, vec2(1), angle, false, (context)=>
4335
4390
  {
4336
4391
  context.strokeStyle = color.toString();
@@ -4511,6 +4566,77 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
4511
4566
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
4512
4567
  }
4513
4568
 
4569
+ /** Draw a circle filled with a radial gradient from the center to the rim
4570
+ * - Best when batched with other untextured polys
4571
+ * - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
4572
+ * - Stacking gradients at the exact same position may show a faint vertical artifact
4573
+ * @param {Vector2} pos
4574
+ * @param {number} [size=1] - Diameter
4575
+ * @param {Color} [colorInner=WHITE]
4576
+ * @param {Color} [colorOuter=CLEAR_WHITE]
4577
+ * @param {boolean} [useWebGL=glEnable]
4578
+ * @param {boolean} [screenSpace]
4579
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4580
+ * @memberof Draw */
4581
+ let drawCircleGradientOffset = 0;
4582
+ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
4583
+ {
4584
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4585
+ ASSERT(isNumber(size), 'size must be a number');
4586
+ ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
4587
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4588
+
4589
+ if (headlessMode) return;
4590
+
4591
+ if (useWebGL && glEnable)
4592
+ {
4593
+ ASSERT(!!glContext, 'WebGL is not enabled!');
4594
+ if (screenSpace)
4595
+ {
4596
+ // convert to world space
4597
+ pos = screenToWorld(pos);
4598
+ size /= cameraScale;
4599
+ }
4600
+ // fan as tristrip; rotate the boundary vertex by one slice per call
4601
+ // so back-to-back gradients at the same position have their hole
4602
+ // (from gpu edge-rule on the boundary line-degen) at different rim
4603
+ // verts and don't visibly stack
4604
+ const sides = glCircleSides;
4605
+ const radius = size/2;
4606
+ const innerInt = colorInner.rgbaInt();
4607
+ const outerInt = colorOuter.rgbaInt();
4608
+ const offset = drawCircleGradientOffset++;
4609
+ const startA = (offset%sides)/sides*PI*2;
4610
+ const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
4611
+ const colors = [outerInt];
4612
+ for (let i=sides; i--;)
4613
+ {
4614
+ const a = ((i+offset)%sides)/sides*PI*2;
4615
+ points.push(pos);
4616
+ colors.push(innerInt);
4617
+ points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));
4618
+ colors.push(outerInt);
4619
+ }
4620
+ glDrawColoredPoints(points, colors);
4621
+ }
4622
+ else
4623
+ {
4624
+ // normal canvas 2D rendering method (slower)
4625
+ ++drawCount;
4626
+ ++primitiveCount;
4627
+ drawCanvas2D(pos, vec2(size), 0, false, (context)=>
4628
+ {
4629
+ const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
4630
+ gradient.addColorStop(0, colorInner.toString());
4631
+ gradient.addColorStop(1, colorOuter.toString());
4632
+ context.fillStyle = gradient;
4633
+ context.beginPath();
4634
+ context.ellipse(0, 0, .5, .5, 0, 0, 9);
4635
+ context.fill();
4636
+ }, screenSpace, context);
4637
+ }
4638
+ }
4639
+
4514
4640
  /**
4515
4641
  * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
4516
4642
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
@@ -6356,14 +6482,15 @@ class SoundInstance
6356
6482
 
6357
6483
  /** Speak text with passed in settings
6358
6484
  * @param {string} text - The text to speak
6359
- * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
6360
6485
  * @param {number} [volume] - How much to scale volume by
6361
6486
  * @param {number} [rate] - How quickly to speak
6362
6487
  * @param {number} [pitch] - How much to change the pitch by
6488
+ * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
6363
6489
  * @return {SpeechSynthesisUtterance} - The utterance that was spoken
6364
6490
  * @memberof Audio */
6365
- function speak(text, language='', volume=1, rate=1, pitch=1)
6491
+ function speak(text, volume=1, rate=1, pitch=1, language='')
6366
6492
  {
6493
+ ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
6367
6494
  if (!soundEnable || headlessMode) return;
6368
6495
  if (!speechSynthesis) return;
6369
6496
 
@@ -8356,7 +8483,8 @@ function glFlush()
8356
8483
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
8357
8484
  else
8358
8485
  glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
8359
- drawCount += glBatchCount;
8486
+ ++drawCount;
8487
+ primitiveCount += glBatchCount;
8360
8488
  glBatchCount = 0;
8361
8489
  }
8362
8490
  glBatchAdditive = glAdditive;
@@ -8417,6 +8545,67 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
8417
8545
  glPositionData[offset++] = angle;
8418
8546
  }
8419
8547
 
8548
+ /** Add an untextured rect to the gl draw list
8549
+ * Picks the optimal path: if already in poly mode, emits a tristrip rect
8550
+ * so it batches with surrounding polys; otherwise uses the instanced path
8551
+ * with uvs and rgba zeroed so the color falls through the additive slot.
8552
+ * @param {number} x
8553
+ * @param {number} y
8554
+ * @param {number} sizeX
8555
+ * @param {number} sizeY
8556
+ * @param {number} angle
8557
+ * @param {number} rgba - color as 32-bit integer
8558
+ * @memberof WebGL */
8559
+ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
8560
+ {
8561
+ if (glPolyMode)
8562
+ {
8563
+ // batch with surrounding polys as a 4-vertex tristrip rect
8564
+ const vertCount = 6; // 4 corners + 2 degenerate verts
8565
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
8566
+ glFlush();
8567
+
8568
+ // compute rotated corners in world space (matches glDrawPointsTransform rotation)
8569
+ const hx = sizeX*.5, hy = sizeY*.5;
8570
+ const c = cos(angle), s = sin(angle);
8571
+ const chx = c*hx, shx = s*hx, chy = c*hy, shy = s*hy;
8572
+ const x0 = x - chx - shy, y0 = y + shx - chy; // (-hx,-hy)
8573
+ const x1 = x + chx - shy, y1 = y - shx - chy; // ( hx,-hy)
8574
+ const x2 = x - chx + shy, y2 = y + shx + chy; // (-hx, hy)
8575
+ const x3 = x + chx + shy, y3 = y - shx + chy; // ( hx, hy)
8576
+
8577
+ // write tristrip with leading/trailing degenerate verts
8578
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
8579
+ glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
8580
+ glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
8581
+ glPositionData[offset++] = x1; glPositionData[offset++] = y1; glColorData[offset++] = rgba;
8582
+ glPositionData[offset++] = x2; glPositionData[offset++] = y2; glColorData[offset++] = rgba;
8583
+ glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
8584
+ glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
8585
+ glBatchCount += vertCount;
8586
+ return;
8587
+ }
8588
+
8589
+ // instanced path: zero uvs and rgba so the texture contribution is killed,
8590
+ // then carry the real color in the additive slot
8591
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
8592
+ glFlush();
8593
+ glSetInstancedMode();
8594
+
8595
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
8596
+ glPositionData[offset++] = x;
8597
+ glPositionData[offset++] = y;
8598
+ glPositionData[offset++] = sizeX;
8599
+ glPositionData[offset++] = sizeY;
8600
+ glPositionData[offset++] = 0;
8601
+ glPositionData[offset++] = 0;
8602
+ glPositionData[offset++] = 0;
8603
+ glPositionData[offset++] = 0;
8604
+ glColorData[offset++] = 0;
8605
+ glColorData[offset++] = rgba;
8606
+ glPositionData[offset++] = angle;
8607
+ }
8608
+
8420
8609
  /** Transform and add a polygon to the gl draw list
8421
8610
  * @param {Array<Vector2>} points - Array of Vector2 points
8422
8611
  * @param {number} rgba - Color of the polygon as a 32-bit integer
@@ -9053,7 +9242,8 @@ class Medal
9053
9242
  /** @property {boolean} - Is the medal unlocked? */
9054
9243
  this.unlocked = false;
9055
9244
 
9056
- // load the source image if provided
9245
+ /** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
9246
+ this.image = undefined;
9057
9247
  if (src)
9058
9248
  (this.image = new Image).src = src;
9059
9249
 
@@ -9216,13 +9406,18 @@ class NewgroundsPlugin
9216
9406
  ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
9217
9407
 
9218
9408
  newgrounds = this; // set global newgrounds object
9409
+ /** @property {string} - The newgrounds App ID */
9219
9410
  this.app_id = app_id;
9411
+ /** @property {string|undefined} - AES-128/Base64 encryption key, if any */
9220
9412
  this.cipher = cipher;
9413
+ /** @property {Object|undefined} - CryptoJS instance used when cipher is set */
9221
9414
  this.cryptoJS = cryptoJS;
9415
+ /** @property {string} - Hostname used when logging views */
9222
9416
  this.host = location ? location.hostname : '';
9223
9417
 
9224
9418
  // get session id from url search params
9225
9419
  const url = new URL(location.href);
9420
+ /** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
9226
9421
  this.session_id = url.searchParams.get('ngio_session_id');
9227
9422
 
9228
9423
  if (!this.session_id)
@@ -9230,6 +9425,7 @@ class NewgroundsPlugin
9230
9425
 
9231
9426
  // get medals
9232
9427
  const medalsResult = this.call('Medal.getList');
9428
+ /** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
9233
9429
  this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
9234
9430
  debugMedals && LOG(this.medals);
9235
9431
  for (const newgroundsMedal of this.medals)
@@ -9253,6 +9449,7 @@ class NewgroundsPlugin
9253
9449
 
9254
9450
  // get scoreboards
9255
9451
  const scoreboardResult = this.call('ScoreBoard.getBoards');
9452
+ /** @property {Array} - Scoreboards fetched from Newgrounds */
9256
9453
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
9257
9454
  debugMedals && LOG(this.scoreboards);
9258
9455
 
@@ -13565,13 +13762,21 @@ class Tween
13565
13762
  }
13566
13763
  ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
13567
13764
 
13765
+ /** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
13568
13766
  this.callback = callback;
13767
+ /** @property {number|Vector2|Color} - Starting value */
13569
13768
  this.start = start;
13769
+ /** @property {number|Vector2|Color} - Ending value */
13570
13770
  this.end = end;
13771
+ /** @property {number} - Total duration in seconds */
13571
13772
  this.duration = duration;
13773
+ /** @property {number} - Remaining time in seconds (counts down from duration to 0) */
13572
13774
  this.life = duration;
13775
+ /** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
13573
13776
  this.ease = options.ease || Ease.LINEAR;
13777
+ /** @property {boolean} - If true, advance even when the game is paused */
13574
13778
  this.useRealTime = !!options.useRealTime;
13779
+ /** @property {boolean} - If true, stop advancing until cleared */
13575
13780
  this.paused = !!options.paused;
13576
13781
 
13577
13782
  /** @private completion callback set by then(), loop(), pingPong(). */
@@ -14104,7 +14309,9 @@ class PathFinder
14104
14309
  // .size + .getCollisionData.
14105
14310
  if (isVector2(source))
14106
14311
  {
14312
+ /** @property {Vector2} - Grid dimensions in tiles */
14107
14313
  this.size = source.floor();
14314
+ /** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
14108
14315
  this.tileLayer = undefined;
14109
14316
  }
14110
14317
  else
@@ -14116,13 +14323,18 @@ class PathFinder
14116
14323
  }
14117
14324
 
14118
14325
  // Tunables (public, freely re-assignable).
14326
+ /** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
14119
14327
  this.heuristicWeight = 1;
14120
- this.maxLoop = 500;
14328
+ /** @property {number} - Maximum A* expansions before giving up */
14329
+ this.maxLoop = 1e3;
14330
+ /** @property {boolean} - If true, post-process paths with two-pass smoothing */
14121
14331
  this.smoothPath = true;
14332
+ /** @property {boolean} - If true, draw debug visualization during findPath */
14122
14333
  this.debug = false;
14123
- this.debugTime = 2;
14334
+ /** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
14335
+ this.debugTime = 1;
14124
14336
 
14125
- // Pre-allocate the node array one node per tile, reused across calls.
14337
+ /** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
14126
14338
  this.nodes = new Array(this.size.x * this.size.y);
14127
14339
  for (let y = 0; y < this.size.y; ++y)
14128
14340
  for (let x = 0; x < this.size.x; ++x)
@@ -14296,9 +14508,12 @@ class PathFinder
14296
14508
  // Best path so far through neighbor — record it.
14297
14509
  neighbor.parent = current;
14298
14510
  neighbor.g = tentativeG;
14299
- const gdx = endNode.pos.x - neighbor.pos.x;
14300
- const gdy = endNode.pos.y - neighbor.pos.y;
14301
- neighbor.f = neighbor.g + (gdx * gdx + gdy * gdy) * this.heuristicWeight;
14511
+ // Octile heuristic tightest admissible distance for an
14512
+ // 8-connected grid with cardinal cost 1 and diagonal cost √2.
14513
+ const adx = abs(endNode.pos.x - neighbor.pos.x);
14514
+ const ady = abs(endNode.pos.y - neighbor.pos.y);
14515
+ const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
14516
+ neighbor.f = neighbor.g + h * this.heuristicWeight;
14302
14517
  }
14303
14518
  }
14304
14519
 
@@ -14580,6 +14795,24 @@ class PathFinder
14580
14795
  path.push(original[original.length - 1]);
14581
14796
  }
14582
14797
 
14798
+ /** Drop any middle node that lies exactly on the line through its two
14799
+ * neighbors. Backstop for the smoothing passes — the corners pass
14800
+ * intentionally keeps truly-straight runs, and the string-pulling pass
14801
+ * checks collinearity against the original path, not the in-progress
14802
+ * result, so it can leave 3+ collinear nodes in some edge cases.
14803
+ * @param {PathFinderNode[]} path
14804
+ * @private */
14805
+ dropCollinearNodes(path)
14806
+ {
14807
+ for (let i = path.length - 2; i >= 1; --i)
14808
+ {
14809
+ const a = path[i - 1], b = path[i], c = path[i + 1];
14810
+ if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
14811
+ (b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
14812
+ path.splice(i, 1);
14813
+ }
14814
+ }
14815
+
14583
14816
  /** Lookup helper: true when the node at tile coords (x, y) is in-bounds
14584
14817
  * and clear (walkable, zero-cost). Used by isLineClear's hot path.
14585
14818
  * @param {number} x
@@ -14747,6 +14980,7 @@ class PathFinder
14747
14980
  {
14748
14981
  this.smoothPathCorners(nodePath);
14749
14982
  this.smoothPathStringPull(nodePath);
14983
+ this.dropCollinearNodes(nodePath);
14750
14984
  }
14751
14985
 
14752
14986
  // Convert to world-space Vector2 path. Return copies, not live node
@@ -14957,6 +15191,8 @@ export
14957
15191
  shareURL,
14958
15192
  readSaveData,
14959
15193
  writeSaveData,
15194
+ noise1D,
15195
+ noise2D,
14960
15196
 
14961
15197
  // Random
14962
15198
  rand,
@@ -15010,6 +15246,7 @@ export
15010
15246
  mainCanvasSize,
15011
15247
  textureInfos,
15012
15248
  drawCount,
15249
+ primitiveCount,
15013
15250
  screenToWorld,
15014
15251
  worldToScreen,
15015
15252
  screenToWorldDelta,
@@ -15025,6 +15262,7 @@ export
15025
15262
  drawRegularPoly,
15026
15263
  drawEllipse,
15027
15264
  drawCircle,
15265
+ drawCircleGradient,
15028
15266
  drawCanvas2D,
15029
15267
  drawText,
15030
15268
  drawTextScreen,
@@ -15054,6 +15292,7 @@ export
15054
15292
  glCopyToContext,
15055
15293
  glSetAntialias,
15056
15294
  glDraw,
15295
+ glDrawUntextured,
15057
15296
  glDrawPointsTransform,
15058
15297
  glDrawOutlineTransform,
15059
15298
  glDrawPoints,