littlejsengine 1.18.23 → 1.18.25

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.23';
38
+ const engineVersion = '1.18.25';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -92,6 +92,7 @@ function setPaused(isPaused=true) { paused = isPaused; }
92
92
 
93
93
  // Engine internal variables
94
94
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
95
+ let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
95
96
  let showEngineVersion = true;
96
97
 
97
98
  ///////////////////////////////////////////////////////////////////////////////
@@ -280,7 +281,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
280
281
 
281
282
  if (!debugVideoCaptureIsActive())
282
283
  renderFrame();
283
- requestAnimationFrame(engineUpdate);
284
+ if (!engineManualStep)
285
+ requestAnimationFrame(engineUpdate);
284
286
 
285
287
  function renderFrame()
286
288
  {
@@ -308,6 +310,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
308
310
  primitiveCount = 0;
309
311
  }
310
312
  }
313
+ engineUpdateInternal = engineUpdate;
311
314
 
312
315
  function updateCanvas()
313
316
  {
@@ -462,10 +465,39 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
462
465
  {
463
466
  // wait for gameInit to load
464
467
  await gameInit();
465
- engineUpdate();
468
+ engineManualStep || engineUpdate();
466
469
  }
467
470
  }
468
471
 
472
+ // max frames engineStep can advance in one call, 10 minutes at 60fps
473
+ // large counts block until they finish, so this catches runaway values
474
+ const engineStepMaxFrames = 36000;
475
+
476
+ /** Advance the engine by a number of frames
477
+ * Requires setEngineManualStep(true) before engineInit
478
+ * Respects paused exactly as the normal update loop does
479
+ * @param {number} [frames] - number of engine update ticks, max 36000, each running one fixed update at timeScale 1
480
+ * @example
481
+ * setHeadlessMode(true);
482
+ * setEngineManualStep(true);
483
+ * await engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost);
484
+ * engineStep(600); // advance 10 seconds of game time
485
+ * @memberof Engine */
486
+ function engineStep(frames=1)
487
+ {
488
+ ASSERT(engineManualStep,
489
+ 'engineStep requires setEngineManualStep(true) before engineInit');
490
+ ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
491
+ // runtime guard so release builds (where the asserts are stripped) can't
492
+ // start a second requestAnimationFrame chain or call an undefined update
493
+ if (!engineManualStep || !engineUpdateInternal) return;
494
+ ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
495
+ 'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
496
+ frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
497
+ for (let i = frames; i > 0; --i)
498
+ engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
499
+ }
500
+
469
501
  /** Update each engine object, remove destroyed objects, and update time
470
502
  * can be called manually if objects need to be updated outside of main loop
471
503
  * @memberof Engine */
@@ -1997,12 +2029,17 @@ function shareURL(title, url, callback)
1997
2029
 
1998
2030
  /** Read save data from local storage
1999
2031
  * @param {string} saveName - unique name for the game/save
2000
- * @param {Object} [defaultSaveData] - default values for save
2032
+ * @param {Object} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
2001
2033
  * @return {Object}
2002
2034
  * @memberof Utilities */
2003
2035
  function readSaveData(saveName, defaultSaveData)
2004
2036
  {
2005
- ASSERT(isStringLike(saveName), 'loadData requires saveName string');
2037
+ ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
2038
+ ASSERT(defaultSaveData === undefined ||
2039
+ (typeof defaultSaveData === 'object' && defaultSaveData !== null),
2040
+ 'readSaveData: default must be an object - the result is ' +
2041
+ '{...default, ...loaded}, so a scalar default yields {}. ' +
2042
+ 'Use readSaveData(key, {best:0}).best');
2006
2043
 
2007
2044
  // tolerate localStorage being unavailable (iOS private mode, sandboxed
2008
2045
  // iframes) and corrupt JSON in stored data
@@ -2026,7 +2063,7 @@ function readSaveData(saveName, defaultSaveData)
2026
2063
  * @memberof Utilities */
2027
2064
  function writeSaveData(saveName, saveData)
2028
2065
  {
2029
- ASSERT(isStringLike(saveName), 'saveData requires saveName string');
2066
+ ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
2030
2067
  // tolerate localStorage being unavailable or quota exceeded
2031
2068
  try { localStorage[saveName] = JSON.stringify(saveData); }
2032
2069
  catch { LOG('writeSaveData: failed to write', saveName); }
@@ -2194,6 +2231,13 @@ let showSplashScreen = false;
2194
2231
  * @memberof Settings */
2195
2232
  let headlessMode = false;
2196
2233
 
2234
+ /** Disables the automatic requestAnimationFrame loop so the engine only
2235
+ * advances when engineStep is called, for tests and frame-stepping tools
2236
+ * @type {boolean}
2237
+ * @default
2238
+ * @memberof Settings */
2239
+ let engineManualStep = false;
2240
+
2197
2241
  ///////////////////////////////////////////////////////////////////////////////
2198
2242
  // WebGL settings
2199
2243
 
@@ -2544,6 +2588,12 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
2544
2588
  * @memberof Settings */
2545
2589
  function setHeadlessMode(headless) { headlessMode = headless; }
2546
2590
 
2591
+ /** Set if the engine only advances when engineStep is called
2592
+ * Must be set before engineInit
2593
+ * @param {boolean} [enable]
2594
+ * @memberof Settings */
2595
+ function setEngineManualStep(enable=true) { engineManualStep = enable; }
2596
+
2547
2597
  /** Set if WebGL rendering is enabled
2548
2598
  * @param {boolean} enable
2549
2599
  * @memberof Settings */
@@ -14307,17 +14357,21 @@ class TextureSheet
14307
14357
  * @param {Vector2} imageSize - Size of the source image in pixels
14308
14358
  * @param {Vector2} [frameSize] - Size of each frame, or the whole image if not passed
14309
14359
  * @param {number} [padding] - How many pixels padding around each frame
14310
- * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
14360
+ * @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image
14311
14361
  * @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
14312
14362
  tryAdd(imageSize, frameSize=imageSize, padding=textureSheetPadding, sourcePadding=0)
14313
14363
  {
14314
14364
  ASSERT(isVector2(imageSize) && isVector2(frameSize), 'sizes must be vec2');
14315
14365
  ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
14316
- ASSERT(isNumber(sourcePadding) && sourcePadding >= 0, 'sourcePadding must be a number >= 0');
14366
+
14367
+ if (isNumber(sourcePadding))
14368
+ sourcePadding = vec2(sourcePadding);
14369
+ ASSERT(isVector2(sourcePadding) && sourcePadding.x >= 0 && sourcePadding.y >= 0,
14370
+ 'sourcePadding must be a number or vec2 >= 0');
14317
14371
 
14318
14372
  // the source may have its own padding baked in around each frame
14319
- const sourceCellWidth = frameSize.x + sourcePadding*2;
14320
- const sourceCellHeight = frameSize.y + sourcePadding*2;
14373
+ const sourceCellWidth = frameSize.x + sourcePadding.x*2;
14374
+ const sourceCellHeight = frameSize.y + sourcePadding.y*2;
14321
14375
  ASSERT(imageSize.x % sourceCellWidth === 0 && imageSize.y % sourceCellHeight === 0,
14322
14376
  'image size must be a multiple of the padded frame size');
14323
14377
 
@@ -14359,16 +14413,19 @@ class TextureSheet
14359
14413
  * @param {HTMLImageElement} image - Source image to copy from
14360
14414
  * @param {TileInfo} tileInfo - Where to put it, from tryAdd
14361
14415
  * @param {boolean} [update] - Upload to webgl now, pass false when batching
14362
- * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image */
14416
+ * @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image */
14363
14417
  drawImage(image, tileInfo, update=true, sourcePadding=0)
14364
14418
  {
14365
14419
  ASSERT(!!this.context, 'texture sheet has no canvas');
14366
14420
 
14421
+ if (isNumber(sourcePadding))
14422
+ sourcePadding = vec2(sourcePadding);
14423
+
14367
14424
  // copy frames in order, reading the source left to right, top to bottom
14368
14425
  // the destination wraps at tileInfo.columns which may be narrower than the source
14369
14426
  const frameSize = tileInfo.size;
14370
- const sourceCellWidth = frameSize.x + sourcePadding*2;
14371
- const sourceCellHeight = frameSize.y + sourcePadding*2;
14427
+ const sourceCellWidth = frameSize.x + sourcePadding.x*2;
14428
+ const sourceCellHeight = frameSize.y + sourcePadding.y*2;
14372
14429
  const sourceColumns = image.width / sourceCellWidth;
14373
14430
  const frameCount = sourceColumns * (image.height / sourceCellHeight);
14374
14431
  const columns = tileInfo.columns || frameCount;
@@ -14376,8 +14433,8 @@ class TextureSheet
14376
14433
  const cellHeight = frameSize.y + tileInfo.padding*2;
14377
14434
  for (let i = frameCount; i--;)
14378
14435
  {
14379
- const sourceX = (i % sourceColumns) * sourceCellWidth + sourcePadding;
14380
- const sourceY = (i / sourceColumns | 0) * sourceCellHeight + sourcePadding;
14436
+ const sourceX = (i % sourceColumns) * sourceCellWidth + sourcePadding.x;
14437
+ const sourceY = (i / sourceColumns | 0) * sourceCellHeight + sourcePadding.y;
14381
14438
  this.context.drawImage(image,
14382
14439
  sourceX, sourceY, frameSize.x, frameSize.y,
14383
14440
  tileInfo.pos.x + (i % columns) * cellWidth,
@@ -14411,7 +14468,7 @@ class TextureSheet
14411
14468
  * @param {string} src - Image source path
14412
14469
  * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
14413
14470
  * @param {number} [padding] - How many pixels padding around each frame
14414
- * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
14471
+ * @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image
14415
14472
  * @return {TileInfo}
14416
14473
  * @example
14417
14474
  * const playerTile = loadSprite('player.png'); // a single sprite
@@ -14422,6 +14479,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
14422
14479
  ASSERT(isStringLike(src), 'image src must be a string');
14423
14480
  ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
14424
14481
  ASSERT(isNumber(padding), 'padding must be a number');
14482
+ ASSERT(isNumber(sourcePadding) || isVector2(sourcePadding), 'sourcePadding must be a number or vec2');
14425
14483
 
14426
14484
  if (isNumber(frameSize))
14427
14485
  frameSize = vec2(frameSize);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.23",
3
+ "version": "1.18.25",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
@@ -38,6 +38,7 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "node src/engineBuild.mjs",
41
+ "build-docs": "node tools/buildDocs.mjs",
41
42
  "test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\""
42
43
  },
43
44
  "engines": {
@@ -45,9 +46,11 @@
45
46
  },
46
47
  "devDependencies": {
47
48
  "bestzip": "^2.2.1",
49
+ "clean-jsdoc-theme": "~4.2.9",
48
50
  "electron": "^38.2.0",
49
51
  "electron-packager": "^17.1.2",
50
52
  "google-closure-compiler": "~20230502.0.0",
53
+ "jsdoc": "~4.0.2",
51
54
  "roadroller": "~2.1.0",
52
55
  "typescript": "~5.1.6",
53
56
  "uglify-js": "~3.17.4"
@@ -73,17 +73,21 @@ class TextureSheet
73
73
  * @param {Vector2} imageSize - Size of the source image in pixels
74
74
  * @param {Vector2} [frameSize] - Size of each frame, or the whole image if not passed
75
75
  * @param {number} [padding] - How many pixels padding around each frame
76
- * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
76
+ * @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image
77
77
  * @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
78
78
  tryAdd(imageSize, frameSize=imageSize, padding=textureSheetPadding, sourcePadding=0)
79
79
  {
80
80
  ASSERT(isVector2(imageSize) && isVector2(frameSize), 'sizes must be vec2');
81
81
  ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
82
- ASSERT(isNumber(sourcePadding) && sourcePadding >= 0, 'sourcePadding must be a number >= 0');
82
+
83
+ if (isNumber(sourcePadding))
84
+ sourcePadding = vec2(sourcePadding);
85
+ ASSERT(isVector2(sourcePadding) && sourcePadding.x >= 0 && sourcePadding.y >= 0,
86
+ 'sourcePadding must be a number or vec2 >= 0');
83
87
 
84
88
  // the source may have its own padding baked in around each frame
85
- const sourceCellWidth = frameSize.x + sourcePadding*2;
86
- const sourceCellHeight = frameSize.y + sourcePadding*2;
89
+ const sourceCellWidth = frameSize.x + sourcePadding.x*2;
90
+ const sourceCellHeight = frameSize.y + sourcePadding.y*2;
87
91
  ASSERT(imageSize.x % sourceCellWidth === 0 && imageSize.y % sourceCellHeight === 0,
88
92
  'image size must be a multiple of the padded frame size');
89
93
 
@@ -125,16 +129,19 @@ class TextureSheet
125
129
  * @param {HTMLImageElement} image - Source image to copy from
126
130
  * @param {TileInfo} tileInfo - Where to put it, from tryAdd
127
131
  * @param {boolean} [update] - Upload to webgl now, pass false when batching
128
- * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image */
132
+ * @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image */
129
133
  drawImage(image, tileInfo, update=true, sourcePadding=0)
130
134
  {
131
135
  ASSERT(!!this.context, 'texture sheet has no canvas');
132
136
 
137
+ if (isNumber(sourcePadding))
138
+ sourcePadding = vec2(sourcePadding);
139
+
133
140
  // copy frames in order, reading the source left to right, top to bottom
134
141
  // the destination wraps at tileInfo.columns which may be narrower than the source
135
142
  const frameSize = tileInfo.size;
136
- const sourceCellWidth = frameSize.x + sourcePadding*2;
137
- const sourceCellHeight = frameSize.y + sourcePadding*2;
143
+ const sourceCellWidth = frameSize.x + sourcePadding.x*2;
144
+ const sourceCellHeight = frameSize.y + sourcePadding.y*2;
138
145
  const sourceColumns = image.width / sourceCellWidth;
139
146
  const frameCount = sourceColumns * (image.height / sourceCellHeight);
140
147
  const columns = tileInfo.columns || frameCount;
@@ -142,8 +149,8 @@ class TextureSheet
142
149
  const cellHeight = frameSize.y + tileInfo.padding*2;
143
150
  for (let i = frameCount; i--;)
144
151
  {
145
- const sourceX = (i % sourceColumns) * sourceCellWidth + sourcePadding;
146
- const sourceY = (i / sourceColumns | 0) * sourceCellHeight + sourcePadding;
152
+ const sourceX = (i % sourceColumns) * sourceCellWidth + sourcePadding.x;
153
+ const sourceY = (i / sourceColumns | 0) * sourceCellHeight + sourcePadding.y;
147
154
  this.context.drawImage(image,
148
155
  sourceX, sourceY, frameSize.x, frameSize.y,
149
156
  tileInfo.pos.x + (i % columns) * cellWidth,
@@ -177,7 +184,7 @@ class TextureSheet
177
184
  * @param {string} src - Image source path
178
185
  * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
179
186
  * @param {number} [padding] - How many pixels padding around each frame
180
- * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
187
+ * @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image
181
188
  * @return {TileInfo}
182
189
  * @example
183
190
  * const playerTile = loadSprite('player.png'); // a single sprite
@@ -188,6 +195,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
188
195
  ASSERT(isStringLike(src), 'image src must be a string');
189
196
  ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
190
197
  ASSERT(isNumber(padding), 'padding must be a number');
198
+ ASSERT(isNumber(sourcePadding) || isVector2(sourcePadding), 'sourcePadding must be a number or vec2');
191
199
 
192
200
  if (isNumber(frameSize))
193
201
  frameSize = vec2(frameSize);
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.23';
35
+ const engineVersion = '1.18.25';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -89,6 +89,7 @@ function setPaused(isPaused=true) { paused = isPaused; }
89
89
 
90
90
  // Engine internal variables
91
91
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
92
+ let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
92
93
  let showEngineVersion = true;
93
94
 
94
95
  ///////////////////////////////////////////////////////////////////////////////
@@ -277,7 +278,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
277
278
 
278
279
  if (!debugVideoCaptureIsActive())
279
280
  renderFrame();
280
- requestAnimationFrame(engineUpdate);
281
+ if (!engineManualStep)
282
+ requestAnimationFrame(engineUpdate);
281
283
 
282
284
  function renderFrame()
283
285
  {
@@ -305,6 +307,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
305
307
  primitiveCount = 0;
306
308
  }
307
309
  }
310
+ engineUpdateInternal = engineUpdate;
308
311
 
309
312
  function updateCanvas()
310
313
  {
@@ -459,10 +462,39 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
459
462
  {
460
463
  // wait for gameInit to load
461
464
  await gameInit();
462
- engineUpdate();
465
+ engineManualStep || engineUpdate();
463
466
  }
464
467
  }
465
468
 
469
+ // max frames engineStep can advance in one call, 10 minutes at 60fps
470
+ // large counts block until they finish, so this catches runaway values
471
+ const engineStepMaxFrames = 36000;
472
+
473
+ /** Advance the engine by a number of frames
474
+ * Requires setEngineManualStep(true) before engineInit
475
+ * Respects paused exactly as the normal update loop does
476
+ * @param {number} [frames] - number of engine update ticks, max 36000, each running one fixed update at timeScale 1
477
+ * @example
478
+ * setHeadlessMode(true);
479
+ * setEngineManualStep(true);
480
+ * await engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost);
481
+ * engineStep(600); // advance 10 seconds of game time
482
+ * @memberof Engine */
483
+ function engineStep(frames=1)
484
+ {
485
+ ASSERT(engineManualStep,
486
+ 'engineStep requires setEngineManualStep(true) before engineInit');
487
+ ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
488
+ // runtime guard so release builds (where the asserts are stripped) can't
489
+ // start a second requestAnimationFrame chain or call an undefined update
490
+ if (!engineManualStep || !engineUpdateInternal) return;
491
+ ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
492
+ 'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
493
+ frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
494
+ for (let i = frames; i > 0; --i)
495
+ engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
496
+ }
497
+
466
498
  /** Update each engine object, remove destroyed objects, and update time
467
499
  * can be called manually if objects need to be updated outside of main loop
468
500
  * @memberof Engine */
@@ -19,6 +19,7 @@ export
19
19
  getPaused,
20
20
  setPaused,
21
21
  engineInit,
22
+ engineStep,
22
23
  engineObjectsUpdate,
23
24
  engineObjectsDestroy,
24
25
  engineObjectsCollect,
@@ -66,6 +67,7 @@ export
66
67
  fontDefault,
67
68
  showSplashScreen,
68
69
  headlessMode,
70
+ engineManualStep,
69
71
  tileDefaultSize,
70
72
  tileDefaultPadding,
71
73
  tileDefaultBleed,
@@ -120,6 +122,7 @@ export
120
122
  setFontDefault,
121
123
  setShowSplashScreen,
122
124
  setHeadlessMode,
125
+ setEngineManualStep,
123
126
  setGLEnable,
124
127
  setTileDefaultSize,
125
128
  setTileDefaultPadding,
@@ -121,6 +121,13 @@ let showSplashScreen = false;
121
121
  * @memberof Settings */
122
122
  let headlessMode = false;
123
123
 
124
+ /** Disables the automatic requestAnimationFrame loop so the engine only
125
+ * advances when engineStep is called, for tests and frame-stepping tools
126
+ * @type {boolean}
127
+ * @default
128
+ * @memberof Settings */
129
+ let engineManualStep = false;
130
+
124
131
  ///////////////////////////////////////////////////////////////////////////////
125
132
  // WebGL settings
126
133
 
@@ -471,6 +478,12 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
471
478
  * @memberof Settings */
472
479
  function setHeadlessMode(headless) { headlessMode = headless; }
473
480
 
481
+ /** Set if the engine only advances when engineStep is called
482
+ * Must be set before engineInit
483
+ * @param {boolean} [enable]
484
+ * @memberof Settings */
485
+ function setEngineManualStep(enable=true) { engineManualStep = enable; }
486
+
474
487
  /** Set if WebGL rendering is enabled
475
488
  * @param {boolean} enable
476
489
  * @memberof Settings */
@@ -190,12 +190,17 @@ function shareURL(title, url, callback)
190
190
 
191
191
  /** Read save data from local storage
192
192
  * @param {string} saveName - unique name for the game/save
193
- * @param {Object} [defaultSaveData] - default values for save
193
+ * @param {Object} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
194
194
  * @return {Object}
195
195
  * @memberof Utilities */
196
196
  function readSaveData(saveName, defaultSaveData)
197
197
  {
198
- ASSERT(isStringLike(saveName), 'loadData requires saveName string');
198
+ ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
199
+ ASSERT(defaultSaveData === undefined ||
200
+ (typeof defaultSaveData === 'object' && defaultSaveData !== null),
201
+ 'readSaveData: default must be an object - the result is ' +
202
+ '{...default, ...loaded}, so a scalar default yields {}. ' +
203
+ 'Use readSaveData(key, {best:0}).best');
199
204
 
200
205
  // tolerate localStorage being unavailable (iOS private mode, sandboxed
201
206
  // iframes) and corrupt JSON in stored data
@@ -219,7 +224,7 @@ function readSaveData(saveName, defaultSaveData)
219
224
  * @memberof Utilities */
220
225
  function writeSaveData(saveName, saveData)
221
226
  {
222
- ASSERT(isStringLike(saveName), 'saveData requires saveName string');
227
+ ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
223
228
  // tolerate localStorage being unavailable or quota exceeded
224
229
  try { localStorage[saveName] = JSON.stringify(saveData); }
225
230
  catch { LOG('writeSaveData: failed to write', saveName); }