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.
- package/README.md +3 -0
- package/dist/littlejs.d.ts +29 -7
- package/dist/littlejs.esm.js +77 -16
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +74 -16
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +74 -16
- package/package.json +4 -1
- package/plugins/textureSheet.js +18 -10
- package/src/engine.js +35 -3
- package/src/engineExport.js +3 -0
- package/src/engineSettings.js +13 -0
- package/src/engineUtilities.js +8 -3
package/dist/littlejs.release.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
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
|
-
|
|
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
|
|
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), '
|
|
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), '
|
|
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
|
-
|
|
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.
|
|
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"
|
package/plugins/textureSheet.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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 */
|
package/src/engineExport.js
CHANGED
|
@@ -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,
|
package/src/engineSettings.js
CHANGED
|
@@ -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 */
|
package/src/engineUtilities.js
CHANGED
|
@@ -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
|
|
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), '
|
|
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), '
|
|
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); }
|