littlejsengine 1.24.3 → 1.25.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.
@@ -14,7 +14,7 @@
14
14
 
15
15
  /**
16
16
  * @callback EditorPlayFromCallback - Puts the player at a world position, for the level editor's Play from mouse
17
- * @param {Vector2} pos - Where to start playing
17
+ * @param {any} pos - Where to start playing, a Vector2 from the 2D editor and a Vector3 from the 3D one
18
18
  * @memberof Editor
19
19
  */
20
20
 
@@ -32,25 +32,29 @@
32
32
  */
33
33
 
34
34
  /**
35
- * @typedef {Object} EditorToolAt - What the level editor gives the callbacks of a tool of the game's own
36
- * @property {any} pos - Where the mouse is in the level: a Vector2 in the 2D editor; in the 3D one a Vector3 on
35
+ * @template [P=Vector2]
36
+ * @typedef {Object} EditorToolAt - What the level editor gives the callbacks of a tool of the game's own; P is
37
+ * the type of pos, a Vector2 for a 2D tool, the default, and Vector3|undefined for a 3D one
38
+ * @property {P} pos - Where the mouse is in the level: a Vector2 in the 2D editor; in the 3D one a Vector3 on
37
39
  * the level or the ground, undefined when the mouse is over the panel
38
40
  * @property {Vector2|undefined} cell - 2D: the tile cell under the mouse on the selected tile layer
39
- * @property {any} ray - 3D: the mouse's Ray3D
41
+ * @property {Ray3D|undefined} ray - 3D: the mouse's Ray3D
40
42
  * @property {boolean} shift - Is Shift held
41
43
  * @property {boolean} ctrl - Is Ctrl held
42
44
  * @memberof Editor
43
45
  */
44
46
 
45
47
  /**
46
- * @typedef {Object} EditorTool - A tool of the game's own for the level editor, see LevelEditor.addTool
48
+ * @template [P=Vector2]
49
+ * @typedef {Object} EditorTool - A tool of the game's own for the level editor, see LevelEditor.addTool; P is the
50
+ * type of its pos, as EditorToolAt has it
47
51
  * @property {string} [key] - The key that picks it, as addKey spells one
48
52
  * @property {string} [hint] - The hint line while it is on
49
- * @property {function(EditorToolAt): any} [onPress] - The left button went down in the level; returning false
53
+ * @property {function(EditorToolAt<P>): any} [onPress] - The left button went down in the level; returning false
50
54
  * says the press was not the tool's
51
- * @property {function(EditorToolAt): any} [onDrag] - Each frame it is held
52
- * @property {function(EditorToolAt): any} [onRelease] - It was let go
53
- * @property {function(EditorToolAt): any} [onDraw] - Each frame the tool is on, to draw its cursor or preview
55
+ * @property {function(EditorToolAt<P>): any} [onDrag] - Each frame it is held
56
+ * @property {function(EditorToolAt<P>): any} [onRelease] - It was let go
57
+ * @property {function(EditorToolAt<P>): any} [onDraw] - Each frame the tool is on, to draw its cursor or preview
54
58
  * @memberof Editor
55
59
  */
56
60
 
@@ -143,7 +147,7 @@ class LevelEditor
143
147
  this.keys = {};
144
148
  /** @type {Array<{label: string, onClick: Function, title: string}>} */
145
149
  this.buttons = [];
146
- /** @type {Object<string, EditorTool>} */
150
+ /** @type {Object<string, EditorTool<any>>} */
147
151
  this.tools = {};
148
152
  }
149
153
 
@@ -161,7 +165,7 @@ class LevelEditor
161
165
 
162
166
  /** Put the player at a position, a Vector2 in the 2D editor and a Vector3 in the 3D one; set it or override it
163
167
  * and the editor has Play from mouse, which starts play there
164
- * @param {any} pos */
168
+ * @param {any} pos - A Vector2 from the 2D editor, a Vector3 from the 3D one */
165
169
  onPlayFrom(pos) {}
166
170
 
167
171
  /** Called when the editor opens, to set or override */
@@ -244,8 +248,9 @@ class LevelEditor
244
248
  * in 3D
245
249
  * - What the callbacks change through edit2D or edit3D is one undo: the editor ends the stroke at the release,
246
250
  * and takes it back when the right button or Escape ends the press
251
+ * @template [P=Vector2]
247
252
  * @param {string} name
248
- * @param {EditorTool} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
253
+ * @param {EditorTool<P>} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
249
254
  * false says the press was not the tool's */
250
255
  addTool(name, tool)
251
256
  {
@@ -260,7 +265,10 @@ class LevelEditor
260
265
  open()
261
266
  {
262
267
  if (editorOther?.wanted())
263
- return editorOther.open();
268
+ {
269
+ editorOther.open();
270
+ return;
271
+ }
264
272
  if (!editorSession)
265
273
  editorCameraScale = cameraScale; // a new session starts at the game's zoom, a return keeps the editor's
266
274
  editorSession = true;
@@ -272,7 +280,10 @@ class LevelEditor
272
280
  close()
273
281
  {
274
282
  if (editorOther?.active())
275
- return editorOther.close();
283
+ {
284
+ editorOther.close();
285
+ return;
286
+ }
276
287
  editorSession = false;
277
288
  editorSetOpen(false);
278
289
  }
@@ -1084,6 +1095,8 @@ let editorStroke;
1084
1095
 
1085
1096
  // during a big edit, the layers whose cell by cell redraws are held off, with the redraw each had of its own
1086
1097
  let editorHeldRedraws;
1098
+ // the live layers a change can only show by drawing them again whole, drawn when it is done
1099
+ const editorRedrawWhole = new Set;
1087
1100
 
1088
1101
  // make a big edit, a fill, clear, undo, apply or revert, with each layer's cell by cell redraws held off, a game's
1089
1102
  // tile callback's included; the stroke's end draws each layer it touched again whole
@@ -1110,6 +1123,11 @@ function editorSetCell(layer, pos, gid)
1110
1123
  source.data[index] = gid;
1111
1124
  if (live.destroyed) return before; // a layer the game let go of, the map still has the change
1112
1125
 
1126
+ // a plain cell draws itself here; held draws, a game's onTile, which may change the cells around, and its own
1127
+ // drawing over the layer show only when the whole layer draws again, once the change is done
1128
+ if (editorHeldRedraws || editorHas('onTile') || live.onRedraw !== TileLayer.prototype.onRedraw)
1129
+ editorRedrawWhole.add(live);
1130
+
1113
1131
  if (editorHeldRedraws && !editorHeldRedraws.has(live))
1114
1132
  {
1115
1133
  editorHeldRedraws.set(live, live.hasOwnProperty('redrawTileData') ? live.redrawTileData : undefined);
@@ -1187,8 +1205,7 @@ function editorUndo(redo=false)
1187
1205
  editorChanged(stroke);
1188
1206
  }
1189
1207
 
1190
- // after a change the tile layers it touched draw again whole, so a game's onRedraw decoration sees the new tiles,
1191
- // and the maps it touched are autosaved; a resized map is made again by the game, with the editor staying open
1208
+ // after a change the tile layers it touched that need it draw again whole, and the maps it touched are autosaved; a resized map is made again by the game, with the editor staying open
1192
1209
  function editorChanged(stroke)
1193
1210
  {
1194
1211
  editorRedraw(stroke);
@@ -1197,11 +1214,11 @@ function editorChanged(stroke)
1197
1214
  stroke.some((entry)=> entry.resize) && editorHas('onRestart') && levelEditor.onRestart();
1198
1215
  }
1199
1216
 
1200
- // the tile layers a stroke touched draw again whole
1217
+ // the tile layers a stroke touched that need it draw again whole
1201
1218
  function editorRedraw(stroke)
1202
1219
  {
1203
1220
  for (const layer of new Set(stroke.filter((entry)=> entry.layer).map((entry)=> entry.layer)))
1204
- layer.live.destroyed || layer.live.redraw();
1221
+ editorRedrawWhole.delete(layer.live) && !layer.live.destroyed && layer.live.redraw();
1205
1222
  }
1206
1223
 
1207
1224
  ///////////////////////////////////////////////////////////////////////////////
@@ -1708,6 +1725,40 @@ function editorGidMirror(gid)
1708
1725
  return t ? editorTileToGid(t.tile, (4 - t.direction) % 4, !t.mirror) : gid;
1709
1726
  }
1710
1727
 
1728
+ // a stamp's object turned a quarter clockwise with it: its place by the stamp, its body by its own rotation, which
1729
+ // Tiled turns clockwise about the object's place, so the place needs no other move
1730
+ function editorStampObjectTurn(entry, width)
1731
+ {
1732
+ const offset = vec2(entry.offset.y, width - entry.offset.x), object = entry.object;
1733
+ return {...entry, offset, ...(object && {object: {...object, rotation: ((object.rotation || 0) + 90) % 360}})};
1734
+ }
1735
+
1736
+ // a stamp's object mirrored left to right with it: its rotation the other way, a polygon's points mirrored about its
1737
+ // place, a tile object's image flipped, and a box, ellipse, text or tile, placed by a corner, placed by the corner
1738
+ // that becomes it, its width along its turn from the mirrored place; mirrored twice it is as it was
1739
+ function editorStampObjectMirror(entry, width)
1740
+ {
1741
+ const offset = vec2(width - entry.offset.x, entry.offset.y), object = entry.object;
1742
+ if (!object) return {...entry, offset};
1743
+ const mirrored = {...object, rotation: object.rotation ? -object.rotation : 0};
1744
+ const points = (list)=> list.map((p)=> ({...p, x: -p.x}));
1745
+ if (object.polygon || object.polyline)
1746
+ {
1747
+ object.polygon && (mirrored.polygon = points(object.polygon));
1748
+ object.polyline && (mirrored.polyline = points(object.polyline));
1749
+ }
1750
+ else
1751
+ {
1752
+ // Tiled's pixels run down, the stamp's cells up
1753
+ const angle = (object.rotation || 0) * PI / 180, w = object.width || 0, {x: tw, y: th} = entry.tileSize ?? vec2(1);
1754
+ offset.x -= w * cos(angle) / tw;
1755
+ offset.y -= w * sin(angle) / th;
1756
+ }
1757
+ if (object.gid)
1758
+ mirrored.gid = (object.gid ^ 0x80000000) >>> 0; // Tiled's flip across
1759
+ return {...entry, offset, object: mirrored};
1760
+ }
1761
+
1711
1762
  // a stamp turned a quarter turn clockwise on screen, or back, its cells, their tiles and its objects together; cell
1712
1763
  // (x, y) goes to (y, width - 1 - x), so the bottom row becomes the left column, its left end at the top
1713
1764
  function editorStampTurn(stamp, back=false)
@@ -1715,7 +1766,7 @@ function editorStampTurn(stamp, back=false)
1715
1766
  for (let turns = back ? 3 : 1; turns--;)
1716
1767
  {
1717
1768
  const {width, height} = stamp;
1718
- const objects = stamp.objects?.map((object)=> ({...object, offset: vec2(object.offset.y, width - object.offset.x)}));
1769
+ const objects = stamp.objects?.map((entry)=> editorStampObjectTurn(entry, width));
1719
1770
  stamp = {width: height, height: width, grids: stamp.grids.map((grid)=>
1720
1771
  {
1721
1772
  const turned = [];
@@ -1732,7 +1783,7 @@ function editorStampTurn(stamp, back=false)
1732
1783
  function editorStampMirror(stamp)
1733
1784
  {
1734
1785
  const {width, height} = stamp;
1735
- const objects = stamp.objects?.map((object)=> ({...object, offset: vec2(width - object.offset.x, object.offset.y)}));
1786
+ const objects = stamp.objects?.map((entry)=> editorStampObjectMirror(entry, width));
1736
1787
  return {width, height, grids: stamp.grids.map((grid)=>
1737
1788
  {
1738
1789
  const mirrored = [];
@@ -1864,8 +1915,9 @@ function editorClear()
1864
1915
  editorStrokeEnd();
1865
1916
  }
1866
1917
 
1867
- // the objects of a map's object layers inside a tile area, each {group, type, offset, object}: the index of its
1868
- // object layer, its offset from the area's bottom left corner, and a copy of it, as a stamp keeps them
1918
+ // the objects of a map's object layers inside a tile area, each {group, type, offset, object, tileSize}: the index of
1919
+ // its object layer, its offset from the area's bottom left corner, a copy of it, and the map's tile size in pixels,
1920
+ // which a mirror reads its width in cells by, as a stamp keeps them
1869
1921
  function editorAreaObjects(layer, area)
1870
1922
  {
1871
1923
  const corner = layer.live.pos.add(area.min), far = layer.live.pos.add(area.max).add(vec2(1));
@@ -1874,7 +1926,7 @@ function editorAreaObjects(layer, area)
1874
1926
  const ids = new Set(editorObjectsIn(objectLayer, corner, far));
1875
1927
  return (objectLayer.group?.objects ?? []).filter((object)=> ids.has(object.id)).map((object)=>
1876
1928
  ({group, type: object.type || object.class, offset: editorObjectPos(layer.record, object).subtract(corner),
1877
- object: editorObjectsCopy(object)}));
1929
+ object: editorObjectsCopy(object), tileSize: vec2(layer.record.map.tilewidth || 1, layer.record.map.tileheight || 1)}));
1878
1930
  });
1879
1931
  }
1880
1932
 
@@ -248,7 +248,6 @@ function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='A
248
248
  }
249
249
 
250
250
  /** Returns true if mouse button is down
251
- * @function
252
251
  * @param {number} button
253
252
  * @return {boolean}
254
253
  * @memberof Input */
@@ -259,7 +258,6 @@ function mouseIsDown(button)
259
258
  }
260
259
 
261
260
  /** Returns true if mouse button was pressed
262
- * @function
263
261
  * @param {number} button
264
262
  * @return {boolean}
265
263
  * @memberof Input */
@@ -270,7 +268,6 @@ function mouseWasPressed(button)
270
268
  }
271
269
 
272
270
  /** Returns true if mouse button was released
273
- * @function
274
271
  * @param {number} button
275
272
  * @return {boolean}
276
273
  * @memberof Input */
@@ -498,7 +495,7 @@ function inputInit()
498
495
  {
499
496
  // fix stalled audio requiring user interaction, a keyboard only game has no other gesture
500
497
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
501
- audioContext.resume();
498
+ audioResume();
502
499
 
503
500
  // keys typed into an html text field are the player's typing, not game input;
504
501
  // a key already down still releases on keyup, which only lets go of keys that are down
@@ -584,7 +581,7 @@ function inputInit()
584
581
 
585
582
  // fix stalled audio requiring user interaction
586
583
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
587
- audioContext.resume();
584
+ audioResume();
588
585
 
589
586
  // a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves; under
590
587
  // pointer lock the mouse stays where the lock began, which may be in a bar, and every click is the game's
@@ -684,7 +681,7 @@ function inputInit()
684
681
 
685
682
  // fix stalled audio requiring user interaction
686
683
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
687
- audioContext.resume();
684
+ audioResume();
688
685
 
689
686
  // when the touch gamepad is enabled it owns touch input: suppress the
690
687
  // touch->mouse passthrough entirely unless touchGamepadPassthrough is set
@@ -1449,7 +1446,7 @@ function touchGamepadPointerDown(e, zone)
1449
1446
 
1450
1447
  // resume audio on first interaction
1451
1448
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
1452
- audioContext.resume();
1449
+ audioResume();
1453
1450
 
1454
1451
  // while paused, any touch is the start button; a control belongs to the first finger on it until that
1455
1452
  // finger lifts, so a second finger landing on the same one neither takes it over nor lets it go
package/src/engineMath.js CHANGED
@@ -413,8 +413,10 @@ function lineTest(posStart, posEnd, testFunction, normal)
413
413
  const totalLength = (dx*dx + dy*dy)**.5;
414
414
  if (!totalLength) return;
415
415
 
416
- // current integer cell we are in
416
+ // current integer cell we are in, the one below a whole start heading down the grid, which it goes into
417
417
  const pos = posStart.floor();
418
+ if (dx < 0 && pos.x === posStart.x) --pos.x;
419
+ if (dy < 0 && pos.y === posStart.y) --pos.y;
418
420
 
419
421
  // normalize ray direction
420
422
  const dirX = dx / totalLength;
@@ -687,11 +689,6 @@ function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
687
689
  // vector2 asserts
688
690
  function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
689
691
  function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
690
- function ASSERT_VECTOR2_NORMAL(v)
691
- {
692
- ASSERT_VECTOR2_VALID(v);
693
- ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
694
- }
695
692
 
696
693
  /**
697
694
  * 2D Vector object with vector math library
@@ -483,19 +483,23 @@ class EngineObject
483
483
  }
484
484
 
485
485
  /** Convert from local space to world space
486
- * @param {Vector2} pos - local space point */
486
+ * @param {Vector2} pos - local space point
487
+ * @return {Vector2} */
487
488
  localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
488
489
 
489
490
  /** Convert from world space to local space
490
- * @param {Vector2} pos - world space point */
491
+ * @param {Vector2} pos - world space point
492
+ * @return {Vector2} */
491
493
  worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
492
494
 
493
495
  /** Convert from local space to world space for a vector (rotation only)
494
- * @param {Vector2} vec - local space vector */
496
+ * @param {Vector2} vec - local space vector
497
+ * @return {Vector2} */
495
498
  localToWorldVector(vec) { return vec.rotate(this.angle); }
496
499
 
497
500
  /** Convert from world space to local space for a vector (rotation only)
498
- * @param {Vector2} vec - world space vector */
501
+ * @param {Vector2} vec - world space vector
502
+ * @return {Vector2} */
499
503
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
500
504
 
501
505
  /** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
@@ -634,11 +638,6 @@ class EngineObject
634
638
  this.collideRaycast = collideRaycast;
635
639
  }
636
640
 
637
- /** @deprecated since 1.20, use collideLevel
638
- * @type {boolean} */
639
- get collideTiles() { return this.collideLevel; }
640
- set collideTiles(collide) { this.collideLevel = collide; }
641
-
642
641
  /** Returns string containing info about this object for debugging
643
642
  * @return {string} */
644
643
  toString()
@@ -37,13 +37,13 @@
37
37
  * let pos = vec2(2,3);
38
38
  * let particleEmitter = new ParticleEmitter
39
39
  * (
40
- * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitCone
40
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitConeAngle
41
41
  * tile(0, 16), // tileInfo
42
42
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
43
43
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
44
- * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
45
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
46
- * .5, true // randomness, collide
44
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, speed, angleSpeed
45
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleConeAngle, fadeRate
46
+ * .5, true // randomness, collideLevel
47
47
  * );
48
48
  */
49
49
  class ParticleEmitter extends EngineObject
@@ -336,7 +336,8 @@ class ParticleEmitter extends EngineObject
336
336
  this.additive && setAdditiveBlendMode(false);
337
337
  }
338
338
 
339
- /** is emitter actively spawning */
339
+ /** is emitter actively spawning
340
+ * @return {boolean} */
340
341
  isActive() { return !this.emitTime || this.getAliveTime() < this.emitTime; }
341
342
 
342
343
  /** Destroy the particle emitter
@@ -548,8 +549,8 @@ class Particle
548
549
  }
549
550
  if (isBlockedY || !isBlockedX)
550
551
  {
551
- // down is the world's gravity, or the emitter's own fall in a world with none
552
- const down = gravity.y || emitter.gravity || 0;
552
+ // down is the way it falls, the world's gravity by its gravityScale and the emitter's own
553
+ const down = gravityY;
553
554
  const wasFalling = this.velocity.y < 0 && down < 0 || this.velocity.y > 0 && down > 0;
554
555
  if (wasFalling)
555
556
  this.groundObject = hitLayer;
@@ -10,18 +10,17 @@
10
10
 
11
11
  'use strict';
12
12
 
13
- let debugWatermark = 0;
13
+ let debugWatermark = false;
14
14
  let debugKey = '';
15
15
  let debugKeysAlways = false;
16
16
  let debugTweakables = false;
17
- let debugClearCount = 0;
18
- const debug = 0;
19
- const debugOverlay = 0;
20
- const debugPhysics = 0;
21
- const debugParticles = 0;
22
- const debugRaycast = 0;
23
- const debugGamepads = 0;
24
- const debugSound = 0;
17
+ const debug = false;
18
+ const debugOverlay = false;
19
+ const debugPhysics = false;
20
+ const debugParticles = false;
21
+ const debugRaycast = false;
22
+ const debugGamepads = false;
23
+ const debugSound = false;
25
24
  const debugPointSize = .5;
26
25
 
27
26
  // debug commands are automatically removed from the final build
@@ -52,6 +51,14 @@ function tweak(){}
52
51
  function tweakButton(){}
53
52
  function tweakDivider(){}
54
53
  function tweakEngineDefaults(){}
54
+ // the 2D editor's edit functions in a release build, which has no editor: there is nothing to edit and nothing
55
+ // changes, and a game that calls them needs no guard; edit3D stays undefined, as it is with no 3D plugins
56
+ const editorEdit2DRelease =
57
+ {
58
+ map: undefined, layer: undefined, hover: undefined, get objects() { return []; }, get selection() { return new Set; },
59
+ paint(){ return false; }, changeObjects(){ return false; }, strokeEnd(){}, strokeCancel(){}, bulk(){},
60
+ undo(){ return false; }, toJSON(){ return ''; },
61
+ };
55
62
  class LevelEditor
56
63
  {
57
64
  constructor()
@@ -61,7 +68,7 @@ class LevelEditor
61
68
  }
62
69
  get isOpen() { return false; }
63
70
  get is3D() { return false; }
64
- get edit2D() { return undefined; }
71
+ get edit2D() { return editorEdit2DRelease; }
65
72
  get edit3D() { return undefined; }
66
73
  open(){} close(){} addKey(){} addButton(){} addTool(){}
67
74
  onTile(){} onRestart(){} onPlayFrom(){} onOpen(){} onClose(){} onUpdate(){} onDraw(){} onPanel(){}
@@ -7,7 +7,7 @@
7
7
  * - TileLayer for rendering, TileCollisionLayer for physics
8
8
  * - Collision callbacks for tile interactions with objects
9
9
  * - Optimized raycast support for tile-based physics
10
- * - Integration with Box2D physics via Box2DTileLayer plugin
10
+ * - Integration with Box2D physics via the Box2dTileLayer plugin
11
11
  * @namespace TileLayers
12
12
  */
13
13
 
@@ -16,6 +16,28 @@
16
16
  ///////////////////////////////////////////////////////////////////////////////
17
17
  // Tile Layer System
18
18
 
19
+ /** A Tiled map as Tiled saves it as JSON, what tileLayersLoad and objectLayersLoad take and tileLayersFromLDtk
20
+ * makes; the fields read are listed, and the rest of the file is kept as it is
21
+ * @typedef {Object} TiledMap
22
+ * @property {number} width - Cells across
23
+ * @property {number} height - Cells up
24
+ * @property {number} [tilewidth] - A tile's width in pixels
25
+ * @property {number} [tileheight] - A tile's height in pixels
26
+ * @property {Array<Object>} layers - Tile layers, object layers and groups of them, bottom first
27
+ * @property {Array<Object>} [tilesets] - The first one's margin, spacing and columns are read
28
+ * @property {number} [nextlayerid]
29
+ * @property {number} [nextobjectid]
30
+ * @property {string} [orientation]
31
+ * @property {string} [renderorder]
32
+ * @property {boolean} [infinite]
33
+ * @property {string} [type]
34
+ * @property {string|number} [version] - A number in files from Tiled before 1.6
35
+ * @property {string} [tiledversion]
36
+ * @property {number} [compressionlevel]
37
+ * @property {string} [backgroundcolor]
38
+ * @property {Array<Object>} [properties]
39
+ * @memberof TileLayers */
40
+
19
41
  /** Keep track of all tile layers with collision
20
42
  * @type {Array<TileCollisionLayer>}
21
43
  * @memberof TileLayers */
@@ -90,7 +112,7 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
90
112
  * @param {Vector2} posEnd
91
113
  * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
92
114
  * @param {Vector2} [normal] - Optional normal of the surface hit
93
- * @param {boolean} [solidOnly=true] - Only check solid layers?
115
+ * @param {boolean} [solidOnly] - Only check solid layers?
94
116
  * @return {Vector2|undefined} - where the ray meets the first tile hit, nudged just inside it, or undefined if no hit
95
117
  * @memberof TileLayers */
96
118
  function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
@@ -151,7 +173,7 @@ function tileLayerTileInfo(t, tile)
151
173
  * - An object or image layer keeps its index, with its slot in the returned array left empty
152
174
  * - A hidden layer (visible false) is loaded, its collision included, but not drawn; its render
153
175
  * is a no-op, delete that and call redraw() to show it
154
- * @param {Object} tileMapData - Level data from exported data
176
+ * @param {TiledMap} [tileMapData] - Level data from exported data, a 50 by 50 empty level when left out
155
177
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
156
178
  * @param {number} [renderOrder] - Render order of the top layer
157
179
  * @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
@@ -165,10 +187,7 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
165
187
  {
166
188
  // default level data if loading failed
167
189
  const s = 50;
168
- tileMapData = {};
169
- tileMapData.height = tileMapData.width = s;
170
- tileMapData.layers = [{}];
171
- tileMapData.layers[0].data = new Array(s*s).fill(0);
190
+ tileMapData = {width: s, height: s, layers: [{data: new Array(s*s).fill(0)}]};
172
191
  }
173
192
 
174
193
  // the editor, in debug builds, keeps the map as the source of its edits and brings back autosaved ones
@@ -310,7 +329,7 @@ const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: '
310
329
  * - The level editor edits the map this returns, and saves it as a Tiled map
311
330
  * @param {Object} ldtk - The LDtk project, its JSON
312
331
  * @param {number|string} [level] - Which level, by its index or its identifier
313
- * @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
332
+ * @return {TiledMap} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
314
333
  * @example
315
334
  * const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
316
335
  * const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
@@ -441,7 +460,7 @@ const objectLayersTypes = new Map;
441
460
  * - Adding a name again replaces it
442
461
  * @param {string} name - The type the objects have in Tiled
443
462
  * @param {Function} make - A class made at each object's position, or a function called with it
444
- * @param {Object} [defaults] - Properties set on each one made, the level editor shows inputs for them
463
+ * @param {Object<string, any>} [defaults] - Properties set on each one made, the level editor shows inputs for them
445
464
  * @param {TileInfo} [tileInfo] - An icon for the level editor
446
465
  * @memberof TileLayers
447
466
  * @example
@@ -462,7 +481,7 @@ function objectLayersAddType(name, make, defaults={}, tileInfo)
462
481
  * - The object's properties in Tiled are set over the type's defaults: numbers, booleans, strings, and colors,
463
482
  * and for a Vector2 default the string x,y
464
483
  * - An object whose type was not added is skipped, with a warning in debug builds
465
- * @param {Object} tileMapData - The same Tiled map given to tileLayersLoad
484
+ * @param {TiledMap} tileMapData - The same Tiled map given to tileLayersLoad
466
485
  * @return {Array<any>} - What each object's type made, a function that made nothing is left out
467
486
  * @memberof TileLayers */
468
487
  function objectLayersLoad(tileMapData)
@@ -556,7 +575,7 @@ class TileLayerData
556
575
  /** Create a tile layer data object, one for each tile in a TileLayer
557
576
  * @param {number} [tile] - The tile to use, from 0 like tile(); undefined is an empty cell that draws nothing
558
577
  * @param {number} [direction] - Integer direction of tile, in 90 degree increments
559
- * @param {boolean} [mirror] - If the tile should be mirrored along the x axis
578
+ * @param {boolean} [mirror] - If the tile is flipped left to right
560
579
  * @param {Color} [color] - Color of the tile */
561
580
  constructor(tile, direction=0, mirror=false, color=new Color)
562
581
  {
@@ -565,7 +584,7 @@ class TileLayerData
565
584
  this.tile = tile;
566
585
  /** @property {number} - Integer direction of tile, in 90 degree increments */
567
586
  this.direction = direction;
568
- /** @property {boolean} - If the tile should be mirrored along the x axis */
587
+ /** @property {boolean} - If the tile is flipped left to right */
569
588
  this.mirror = mirror;
570
589
  /** @property {Color} - Color of the tile */
571
590
  this.color = color.copy();
@@ -641,7 +660,7 @@ class CanvasLayer extends EngineObject
641
660
  * @param {Vector2} [size] - Size in world space
642
661
  * @param {Color} [color] - Color to modulate with
643
662
  * @param {number} [angle] - Angle to rotate by
644
- * @param {boolean} [mirror] - If true image is flipped along the Y axis
663
+ * @param {boolean} [mirror] - If true the image is flipped left to right
645
664
  * @param {Color} [additiveColor] - Additive color to be applied if any
646
665
  * @param {boolean} [screenSpace=drawScreenSpace] - If true the pos and size are in screen space
647
666
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to */
@@ -931,7 +950,7 @@ class TileLayer extends CanvasLayer
931
950
  * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
932
951
  * @param {Color} [color=WHITE] - Color to modulate with
933
952
  * @param {number} [angle] - Angle to rotate by
934
- * @param {boolean} [mirror] - Is image flipped along the Y axis?
953
+ * @param {boolean} [mirror] - Is the image flipped left to right?
935
954
  * @param {Color} [additiveColor] - Additive color to be applied if any */
936
955
  drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
937
956
  angle=0, mirror, additiveColor)
@@ -117,18 +117,29 @@ function formatTime(t)
117
117
 
118
118
  /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
119
119
  * @param {string} url - URL of JSON file
120
- * @return {Promise<object>}
120
+ * @return {Promise<any>} - The parsed JSON, any shape, as response.json() gives it
121
121
  * @memberof Utilities */
122
122
  async function fetchJSON(url)
123
123
  {
124
124
  const response = await fetch(url);
125
125
  if (!response.ok)
126
126
  throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
127
- const json = await response.json();
127
+ const text = await response.text();
128
+ let json;
129
+ try { json = JSON.parse(text); }
130
+ catch (e)
131
+ {
132
+ // a dev server answers a mistyped path with its own page, which is no JSON, so the error says which file
133
+ throw new Error(`fetchJSON: ${url} is not JSON` + (loadIsWebPage(text) ?
134
+ ', it is a web page, so the path may be wrong' : ': ' + e.message));
135
+ }
128
136
  editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
129
137
  return json;
130
138
  }
131
139
 
140
+ // is a file's text a web page, as a dev server sends for a path it does not have
141
+ const loadIsWebPage = (text)=> /^\s*</.test(text);
142
+
132
143
  ///////////////////////////////////////////////////////////////////////////////
133
144
 
134
145
  /** Save a text file to disk
@@ -239,6 +250,8 @@ function readSaveData(saveName, defaultSaveData)
239
250
  {
240
251
  try { loadedData = JSON.parse(data); }
241
252
  catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
253
+ if (!loadedData || typeof loadedData !== 'object' || isArray(loadedData))
254
+ loadedData = {}; // only an object of saved values, never a string spread into its letters
242
255
  }
243
256
  }
244
257
  catch { LOG('readSaveData: localStorage unavailable — using defaults'); }