littlejsengine 1.18.12 → 1.18.15

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.
@@ -74,14 +74,29 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
74
74
  * @memberof TileLayers */
75
75
  function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
76
76
  {
77
+ // check every layer and keep the closest hit so a far hit in an
78
+ // earlier-registered layer doesn't shadow a closer hit in a later one
79
+ let closestHit, closestDistSq, closestNormal;
80
+ const scratchNormal = normal && vec2();
77
81
  for (const layer of tileCollisionLayers)
78
82
  {
79
83
  if (!solidOnly || layer.isSolid)
80
84
  {
81
- const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, normal)
82
- if (hitPos) return hitPos;
85
+ const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, scratchNormal);
86
+ if (hitPos)
87
+ {
88
+ const d = posStart.distanceSquared(hitPos);
89
+ if (closestHit === undefined || d < closestDistSq)
90
+ {
91
+ closestHit = hitPos;
92
+ closestDistSq = d;
93
+ if (normal) closestNormal = scratchNormal.copy();
94
+ }
95
+ }
83
96
  }
84
97
  }
98
+ if (closestHit && normal) normal.setFrom(closestNormal);
99
+ return closestHit;
85
100
  }
86
101
 
87
102
  ///////////////////////////////////////////////////////////////////////////////
@@ -255,38 +270,6 @@ class CanvasLayer extends EngineObject
255
270
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
256
271
  }
257
272
 
258
- /** Draw a tile onto the layer canvas in world space
259
- * @param {Vector2} pos
260
- * @param {Vector2} [size=vec2(1)]
261
- * @param {TileInfo} [tileInfo]
262
- * @param {Color} [color=WHITE]
263
- * @param {number} [angle]
264
- * @param {boolean} [mirror] */
265
- drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
266
- {
267
- pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
268
- size = size.multiply(this.tileInfo.size);
269
- pos.y = this.canvas.height - pos.y;
270
-
271
- // draw the tile onto the layer canvas
272
- const oldMainCanvasSize = mainCanvasSize;
273
- mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
274
- const useWebGL = this.hasWebGL();
275
- useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
276
- const drawContext = useWebGL ? undefined : this.context;
277
- drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
278
- useWebGL && glSetRenderTarget();
279
- mainCanvasSize = oldMainCanvasSize;
280
- }
281
-
282
- /** Draw a rectangle onto the layer canvas in world space
283
- * @param {Vector2} pos
284
- * @param {Vector2} [size=vec2(1)]
285
- * @param {Color} [color=WHITE]
286
- * @param {number} [angle] */
287
- drawRect(pos, size, color, angle)
288
- { this.drawTile(pos, size, undefined, color, angle); }
289
-
290
273
  /** Create WebGL texture if necessary and copy layer canvas to it */
291
274
  updateWebGL()
292
275
  { this.textureInfo.createWebGLTexture(); }
@@ -342,6 +325,8 @@ class TileLayer extends CanvasLayer
342
325
  this.redrawTileData = ()=> {};
343
326
  this.drawLayerTile = ()=> {};
344
327
  this.drawLayerRect = ()=> {};
328
+ this.drawTile = ()=> {};
329
+ this.drawRect = ()=> {};
345
330
  this.clearLayerRect = ()=> {};
346
331
  return;
347
332
  }
@@ -369,7 +354,7 @@ class TileLayer extends CanvasLayer
369
354
  ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
370
355
 
371
356
  if (!layerPos.arrayCheck(this.size)) return;
372
- this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
357
+ this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] = data;
373
358
 
374
359
  if (!redraw) return;
375
360
  const isRedraw = drawContext === this.context;
@@ -384,11 +369,11 @@ class TileLayer extends CanvasLayer
384
369
 
385
370
  /** Get data at a given position in the array
386
371
  * @param {Vector2} layerPos - Local position in array
387
- * @return {TileLayerData} */
372
+ * @return {TileLayerData|undefined} */
388
373
  getData(layerPos)
389
- {
374
+ {
390
375
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
391
- return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
376
+ return layerPos.arrayCheck(this.size) ? this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] : undefined;
392
377
  }
393
378
 
394
379
  // Update the tile layer, refresh texture if needed
@@ -496,7 +481,7 @@ class TileLayer extends CanvasLayer
496
481
 
497
482
  // draw the tile if it has layer data
498
483
  const d = this.getData(layerPos);
499
- if (!d.tile) return;
484
+ if (!d || !d.tile) return;
500
485
 
501
486
  const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
502
487
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
@@ -542,6 +527,38 @@ class TileLayer extends CanvasLayer
542
527
  drawLayerRect(pos, size, color, angle=0)
543
528
  { this.drawLayerTile(pos, size, undefined, color, angle); }
544
529
 
530
+ /** Draw a tile onto the layer canvas in world space
531
+ * @param {Vector2} pos
532
+ * @param {Vector2} [size=vec2(1)]
533
+ * @param {TileInfo} [tileInfo]
534
+ * @param {Color} [color=WHITE]
535
+ * @param {number} [angle]
536
+ * @param {boolean} [mirror] */
537
+ drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
538
+ {
539
+ pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
540
+ size = size.multiply(this.tileInfo.size);
541
+ pos.y = this.canvas.height - pos.y;
542
+
543
+ // draw the tile onto the layer canvas
544
+ const oldMainCanvasSize = mainCanvasSize;
545
+ mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
546
+ const useWebGL = this.hasWebGL();
547
+ useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
548
+ const drawContext = useWebGL ? undefined : this.context;
549
+ drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
550
+ useWebGL && glSetRenderTarget();
551
+ mainCanvasSize = oldMainCanvasSize;
552
+ }
553
+
554
+ /** Draw a rectangle onto the layer canvas in world space
555
+ * @param {Vector2} pos
556
+ * @param {Vector2} [size=vec2(1)]
557
+ * @param {Color} [color=WHITE]
558
+ * @param {number} [angle] */
559
+ drawRect(pos, size, color, angle)
560
+ { this.drawTile(pos, size, undefined, color, angle); }
561
+
545
562
  /** Clear a rectangle in layer space
546
563
  * @param {Vector2} pos - position in pixel coordinates
547
564
  * @param {Vector2} size
@@ -620,7 +637,7 @@ class TileCollisionLayer extends TileLayer
620
637
  setCollisionData(layerPos, data=1)
621
638
  {
622
639
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
623
- const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
640
+ const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
624
641
  layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
625
642
  }
626
643
 
@@ -635,7 +652,7 @@ class TileCollisionLayer extends TileLayer
635
652
  getCollisionData(layerPos)
636
653
  {
637
654
  ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
638
- const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
655
+ const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
639
656
  return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
640
657
  }
641
658
 
@@ -660,8 +677,10 @@ class TileCollisionLayer extends TileLayer
660
677
  const posY = pos.y - this.pos.y;
661
678
  const minX = max(posX - size.x/2|0, 0);
662
679
  const minY = max(posY - size.y/2|0, 0);
663
- const maxX = min(posX + size.x/2, this.size.x);
664
- const maxY = min(posY + size.y/2, this.size.y);
680
+ // ensure at least one cell is visited even when size is 0 and pos
681
+ // lands exactly on an integer boundary (documented point-test mode)
682
+ const maxX = min(max(posX + size.x/2, minX + 1), this.size.x);
683
+ const maxY = min(max(posY + size.y/2, minY + 1), this.size.y);
665
684
  const hitPos = new Vector2;
666
685
  for (let y = minY; y < maxY; ++y)
667
686
  for (let x = minX; x < maxX; ++x)
@@ -73,9 +73,15 @@ class Timer
73
73
  * @return {number} */
74
74
  get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
75
75
 
76
- /** Get percentage elapsed based on time it was set to, returns 0 if not set
76
+ /** Get percentage elapsed based on time it was set to, returns 0 if not set.
77
+ * Zero-duration timers report 1 (already elapsed).
77
78
  * @return {number} */
78
- getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
79
+ getPercent()
80
+ {
81
+ if (!this.isSet()) return 0;
82
+ if (!this.setTime) return 1;
83
+ return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
84
+ }
79
85
 
80
86
  /** Get the time this timer was set to, returns 0 if not set
81
87
  * @return {number} */
@@ -102,9 +108,9 @@ class Timer
102
108
  * @memberof Utilities */
103
109
  function formatTime(t)
104
110
  {
105
- const sign = t < 0 ? '-' : '';
111
+ const signStr = t < 0 ? '-' : '';
106
112
  t = abs(t)|0;
107
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
113
+ return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
108
114
  }
109
115
 
110
116
  /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
@@ -190,15 +196,20 @@ function shareURL(title, url, callback)
190
196
  function readSaveData(saveName, defaultSaveData)
191
197
  {
192
198
  ASSERT(isStringLike(saveName), 'loadData requires saveName string');
193
-
194
- // replace undefined values with defaults; tolerate corrupt JSON
195
- const data = localStorage[saveName];
199
+
200
+ // tolerate localStorage being unavailable (iOS private mode, sandboxed
201
+ // iframes) and corrupt JSON in stored data
196
202
  let loadedData = {};
197
- if (data)
203
+ try
198
204
  {
199
- try { loadedData = JSON.parse(data); }
200
- catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
205
+ const data = localStorage[saveName];
206
+ if (data)
207
+ {
208
+ try { loadedData = JSON.parse(data); }
209
+ catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
210
+ }
201
211
  }
212
+ catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
202
213
  return { ...defaultSaveData, ...loadedData };
203
214
  }
204
215
 
@@ -209,7 +220,9 @@ function readSaveData(saveName, defaultSaveData)
209
220
  function writeSaveData(saveName, saveData)
210
221
  {
211
222
  ASSERT(isStringLike(saveName), 'saveData requires saveName string');
212
- localStorage[saveName] = JSON.stringify(saveData);
223
+ // tolerate localStorage being unavailable or quota exceeded
224
+ try { localStorage[saveName] = JSON.stringify(saveData); }
225
+ catch { LOG('writeSaveData: failed to write', saveName); }
213
226
  }
214
227
 
215
228
  ///////////////////////////////////////////////////////////////////////////////
@@ -88,6 +88,10 @@ function glInit(rootElement)
88
88
  for (const info of glTextureInfos)
89
89
  info.glTexture = undefined;
90
90
  glActiveTexture = undefined;
91
+ // drop any partially-filled batch so the next glFlush doesn't
92
+ // upload stale glBatchCount against fresh empty buffers on restore
93
+ glBatchCount = 0;
94
+ glPolyMode = false;
91
95
  pluginList.forEach(plugin=>plugin.glContextLost?.());
92
96
  });
93
97
  glCanvas.addEventListener('webglcontextrestored', ()=>
@@ -446,6 +450,10 @@ function glSetTextureData(texture, image)
446
450
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
447
451
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
448
452
 
453
+ // keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
454
+ if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
455
+ glContext.generateMipmap(glContext.TEXTURE_2D);
456
+
449
457
  // rebind active texture
450
458
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
451
459
  }
@@ -491,7 +499,7 @@ function glFlush()
491
499
  {
492
500
  if (glEnable && glContext && glBatchCount)
493
501
  {
494
- // set bend mode
502
+ // set blend mode
495
503
  const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
496
504
  glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
497
505
  glContext.enable(glContext.BLEND);
@@ -568,9 +576,9 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
568
576
  }
569
577
 
570
578
  /** Add an untextured rect to the gl draw list
571
- * Picks the optimal path: if already in poly mode, emits a tristrip rect
572
- * so it batches with surrounding polys; otherwise uses the instanced path
573
- * with uvs and rgba zeroed so the color falls through the additive slot.
579
+ * Zeroes the uvs and rgba so the texture contribution multiplies to 0,
580
+ * then carries the real color in the additive slot. Works regardless of
581
+ * which texture is currently bound.
574
582
  * @param {number} x
575
583
  * @param {number} y
576
584
  * @param {number} sizeX
@@ -580,52 +588,7 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
580
588
  * @memberof WebGL */
581
589
  function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
582
590
  {
583
- if (glPolyMode)
584
- {
585
- // batch with surrounding polys as a 4-vertex tristrip rect
586
- const vertCount = 6; // 4 corners + 2 degenerate verts
587
- if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
588
- glFlush();
589
-
590
- // compute rotated corners in world space (matches glDrawPointsTransform rotation)
591
- const hx = sizeX*.5, hy = sizeY*.5;
592
- const c = cos(angle), s = sin(angle);
593
- const chx = c*hx, shx = s*hx, chy = c*hy, shy = s*hy;
594
- const x0 = x - chx - shy, y0 = y + shx - chy; // (-hx,-hy)
595
- const x1 = x + chx - shy, y1 = y - shx - chy; // ( hx,-hy)
596
- const x2 = x - chx + shy, y2 = y + shx + chy; // (-hx, hy)
597
- const x3 = x + chx + shy, y3 = y - shx + chy; // ( hx, hy)
598
-
599
- // write tristrip with leading/trailing degenerate verts
600
- let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
601
- glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
602
- glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
603
- glPositionData[offset++] = x1; glPositionData[offset++] = y1; glColorData[offset++] = rgba;
604
- glPositionData[offset++] = x2; glPositionData[offset++] = y2; glColorData[offset++] = rgba;
605
- glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
606
- glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
607
- glBatchCount += vertCount;
608
- return;
609
- }
610
-
611
- // instanced path: zero uvs and rgba so the texture contribution is killed,
612
- // then carry the real color in the additive slot
613
- if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
614
- glFlush();
615
- glSetInstancedMode();
616
-
617
- let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
618
- glPositionData[offset++] = x;
619
- glPositionData[offset++] = y;
620
- glPositionData[offset++] = sizeX;
621
- glPositionData[offset++] = sizeY;
622
- glPositionData[offset++] = 0;
623
- glPositionData[offset++] = 0;
624
- glPositionData[offset++] = 0;
625
- glPositionData[offset++] = 0;
626
- glColorData[offset++] = 0;
627
- glColorData[offset++] = rgba;
628
- glPositionData[offset++] = angle;
591
+ glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
629
592
  }
630
593
 
631
594
  /** Transform and add a polygon to the gl draw list
@@ -641,13 +604,13 @@ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
641
604
  function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
642
605
  {
643
606
  const pointsOut = [];
607
+ const sa = sin(-angle);
608
+ const ca = cos(-angle);
644
609
  for (const p of points)
645
610
  {
646
611
  // transform the point
647
612
  const px = p.x*sx;
648
613
  const py = p.y*sy;
649
- const sa = sin(-angle);
650
- const ca = cos(-angle);
651
614
  pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
652
615
  }
653
616
  const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
@@ -679,11 +642,13 @@ function glDrawPoints(points, rgba)
679
642
  {
680
643
  if (!glEnable || points.length < 3)
681
644
  return; // needs at least 3 points to have area
682
-
645
+
683
646
  // flush if there is not enough room or if different blend mode
684
647
  const vertCount = points.length + 2;
685
648
  if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
686
649
  glFlush();
650
+ ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
651
+ if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
687
652
  glSetPolyMode();
688
653
 
689
654
  // setup triangle strip with degenerate verts at start and end
@@ -707,11 +672,13 @@ function glDrawColoredPoints(points, pointColors)
707
672
  {
708
673
  if (!glEnable || points.length < 3)
709
674
  return; // needs at least 3 points to have area
710
-
675
+
711
676
  // flush if there is not enough room or if different blend mode
712
677
  const vertCount = points.length + 2;
713
678
  if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
714
679
  glFlush();
680
+ ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
681
+ if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
715
682
  glSetPolyMode();
716
683
 
717
684
  // setup triangle strip with degenerate verts at start and end
@@ -781,7 +748,8 @@ function glMakeOutline(points, width, wrap=true)
781
748
  const strip = [];
782
749
  const n = points.length;
783
750
  const e = 1e-6;
784
- const miterLimit = width*100;
751
+ // miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
752
+ const miterLimit = 10;
785
753
  for (let i = 0; i < n; i++)
786
754
  {
787
755
  // for each vertex, calculate normal based on adjacent edges