littlejsengine 1.18.25 → 1.18.26

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.
@@ -3,616 +3,616 @@
3
3
 
4
4
  'use strict';
5
5
 
6
- /**
7
- * LittleJS - The Tiny Fast JavaScript Game Engine
8
- * MIT License - Copyright 2021 Frank Force
9
- *
10
- * Engine Features
11
- * - Object oriented system with EngineObject base class
12
- * - Automatic object lifecycle (update, physics, collision, rendering)
13
- * - Engine helper classes: Vector2, Color, Timer, RandomGenerator
14
- * - Hybrid rendering with WebGL batching and Canvas2D fallback
15
- * - Audio system with wave, mp3, or ZzFX sound effects
16
- * - Input system with keyboard, mouse, gamepad, and touch support
17
- * - Tile layer rendering and collision detection
18
- * - Particle effect system with emitters
19
- * - Medal/achievement system with local storage
20
- * - Comprehensive debug tools and visualizations
21
- * - Fixed 60 FPS timestep with configurable time scale
22
- * - Raycast and spatial query utilities
23
- * - Plugin system for extending engine functionality
24
- * - Start with engineInit() and provide your game callbacks
25
- * @namespace Engine
26
- */
27
-
28
- /** Name of engine
29
- * @type {string}
30
- * @default
31
- * @memberof Engine */
32
- const engineName = 'LittleJS';
33
-
34
- /** Version of engine
35
- * @type {string}
36
- * @default
37
- * @memberof Engine */
38
- const engineVersion = '1.18.25';
39
-
40
- /** Frames per second to update
41
- * @type {number}
42
- * @default
43
- * @memberof Engine */
44
- const frameRate = 60;
45
-
46
- /** How many seconds each frame lasts, engine uses a fixed time step
47
- * @type {number}
48
- * @default 1/60
49
- * @memberof Engine */
50
- const timeDelta = 1/frameRate;
51
-
52
- /** Array containing all engine objects
53
- * @type {Array<EngineObject>}
54
- * @memberof Engine */
55
- let engineObjects = [];
56
-
57
- /** Array with only objects set to collide with other objects this frame (for optimization)
58
- * @type {Array<EngineObject>}
59
- * @memberof Engine */
60
- let engineObjectsCollide = [];
61
-
62
- /** Current update frame, used to calculate time
63
- * @type {number}
64
- * @memberof Engine */
65
- let frame = 0;
66
-
67
- /** Current engine time since start in seconds
68
- * @type {number}
69
- * @memberof Engine */
70
- let time = 0;
71
-
72
- /** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
73
- * @type {number}
74
- * @memberof Engine */
75
- let timeReal = 0;
76
-
77
- /** Is the game paused? Causes time and objects to not be updated
78
- * @type {boolean}
79
- * @default false
80
- * @memberof Engine */
81
- let paused = false;
82
-
83
- /** Get if game is paused
84
- * @return {boolean}
85
- * @memberof Engine */
86
- function getPaused() { return paused; }
87
-
88
- /** Set if game is paused
89
- * @param {boolean} [isPaused]
90
- * @memberof Engine */
91
- function setPaused(isPaused=true) { paused = isPaused; }
92
-
93
- // Engine internal variables
94
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
95
- let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
96
- let showEngineVersion = true;
97
-
98
- ///////////////////////////////////////////////////////////////////////////////
99
- // plugin hooks
100
-
101
- const pluginList = [];
102
- class EnginePlugin
103
- {
104
- constructor(update, render, glContextLost, glContextRestored)
105
- {
106
- this.update = update;
107
- this.render = render;
108
- this.glContextLost = glContextLost;
109
- this.glContextRestored = glContextRestored;
110
- }
111
- }
112
-
113
- /**
114
- * @callback PluginCallback - Update or render function for a plugin
115
- * @memberof Engine
116
- */
117
-
118
- /** Add a new update function for a plugin
119
- * @param {PluginCallback} [update]
120
- * @param {PluginCallback} [render]
121
- * @param {PluginCallback} [glContextLost]
122
- * @param {PluginCallback} [glContextRestored]
123
- * @memberof Engine */
124
- function engineAddPlugin(update, render, glContextLost, glContextRestored)
125
- {
126
- // make sure plugin functions are unique
127
- ASSERT(!pluginList.find(p=>
128
- p.update === update && p.render === render &&
129
- p.glContextLost === glContextLost &&
130
- p.glContextRestored === glContextRestored));
131
-
132
- const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
133
- pluginList.push(plugin);
134
- }
135
-
136
- ///////////////////////////////////////////////////////////////////////////////
137
- // Main Engine Functions
138
-
139
- /**
140
- * @callback GameInitCallback - Called after the engine starts, can be async
141
- * @return {void|Promise<void>}
142
- * @memberof Engine
143
- */
144
- /**
145
- * @callback GameCallback - Update or render function for the game
146
- * @memberof Engine
147
- */
148
-
149
- /** Startup LittleJS engine with your callback functions
150
- * @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
151
- * @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
152
- * @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
153
- * @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
154
- * @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
155
- * @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
156
- * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
157
- * @example
158
- * // Basic engine startup
159
- * engineInit(
160
- * ()=> { LOG('Game initialized!'); }, // gameInit
161
- * ()=> { updateGameLogic(); }, // gameUpdate
162
- * ()=> { updateUI(); }, // gameUpdatePost
163
- * ()=> { drawBackground(); }, // gameRender
164
- * ()=> { drawHUD(); }, // gameRenderPost
165
- * ['tiles.png', 'tilesLevel.png'] // images to load
166
- * );
167
- * @memberof Engine */
168
- async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
169
- {
170
- showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
171
- ASSERT(!mainContext, 'engine already initialized');
172
- // runtime guard so release builds (where the assert is stripped) don't
173
- // double-register listeners / double-add canvases on a second call
174
- if (mainContext) return;
175
- ASSERT(isArray(imageSources), 'pass in images as array');
176
-
177
- // ensure body exists for minimal HTML where the script runs before <body> is parsed
178
- if (!document.body)
179
- document.documentElement.appendChild(document.createElement('body'));
180
- rootElement ||= document.body;
181
-
182
- // allow passing in empty functions
183
- gameInit ||= ()=>{};
184
- gameUpdate ||= ()=>{};
185
- gameUpdatePost ||= ()=>{};
186
- gameRender ||= ()=>{};
187
- gameRenderPost ||= ()=>{};
188
-
189
- // Called automatically by engine to setup render system
190
- function enginePreRender()
191
- {
192
- // save canvas size
193
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
194
-
195
- // disable smoothing for pixel art
196
- mainContext.imageSmoothingEnabled = !tilesPixelated;
197
-
198
- // setup gl rendering if enabled
199
- glPreRender();
200
- }
201
-
202
- // internal update loop for engine
203
- function engineUpdate(frameTimeMS=0)
204
- {
205
- // update time keeping
206
- let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
207
- // skip delta on the very first frame so timeReal doesn't jump
208
- // by ~page-load-time when RAF starts handing real timestamps
209
- if (!frameTimeLastMS) frameTimeDeltaMS = 0;
210
- frameTimeLastMS = frameTimeMS;
211
- if (debug || debugWatermark)
212
- averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
213
- const debugSpeedUp = debug && keyIsDown('Equal'); // +
214
- const debugSpeedDown = debug && keyIsDown('Minus'); // -
215
- const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
216
-
217
- // apply time deltas
218
- timeReal += frameTimeDeltaMS * debugScale / 1e3;
219
- const combinedScale = timeScale * debugScale;
220
- frameTimeDeltaMS *= combinedScale;
221
- frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
222
- if (combinedScale <= 1)
223
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
224
-
225
- let wasUpdated = false;
226
- if (paused)
227
- {
228
- // update everything except the game and objects
229
- wasUpdated = true;
230
- updateCanvas();
231
- inputUpdate();
232
- pluginList.forEach(plugin=>plugin.update?.());
233
-
234
- // update object transforms even when paused
235
- for (const o of engineObjects)
236
- o.parent || o.updateTransforms();
237
-
238
- // do post update
239
- debugUpdate();
240
- gameUpdatePost();
241
- inputUpdatePost();
242
- if (debugVideoCaptureIsActive())
243
- renderFrame();
244
- }
245
- else
246
- {
247
- // apply time delta smoothing, improves smoothness of framerate in some browsers
248
- let deltaSmooth = 0;
249
- if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
250
- {
251
- // force at least one update each frame since it is waiting for refresh
252
- deltaSmooth = frameTimeBufferMS;
253
- frameTimeBufferMS = 0;
254
- }
255
-
256
- // update multiple frames if necessary in case of slow framerate
257
- for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
258
- {
259
- // increment frame and update time
260
- time = frame++ / frameRate;
261
-
262
- // update game and objects
263
- wasUpdated = true;
264
- updateCanvas();
265
- inputUpdate();
266
- gameUpdate();
267
- pluginList.forEach(plugin=>plugin.update?.());
268
- engineObjectsUpdate();
269
-
270
- // do post update
271
- debugUpdate();
272
- gameUpdatePost();
273
- inputUpdatePost();
274
- if (debugVideoCaptureIsActive())
275
- renderFrame();
276
- }
277
-
278
- // add the time smoothing back in
279
- frameTimeBufferMS += deltaSmooth;
280
- }
281
-
282
- if (!debugVideoCaptureIsActive())
283
- renderFrame();
284
- if (!engineManualStep)
285
- requestAnimationFrame(engineUpdate);
286
-
287
- function renderFrame()
288
- {
289
- if (headlessMode) return;
290
-
291
- // canvas must be updated before rendering
292
- if (!wasUpdated)
293
- updateCanvas();
294
-
295
- // render the game and objects
296
- enginePreRender();
297
- gameRender();
298
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
299
- for (const o of engineObjects)
300
- o.destroyed || o.render();
301
-
302
- // post rendering
303
- gameRenderPost();
304
- pluginList.forEach(plugin=>plugin.render?.());
305
- inputRender();
306
- debugRender();
307
- glFlush();
308
- debugRenderPost();
309
- drawCount = 0;
310
- primitiveCount = 0;
311
- }
312
- }
313
- engineUpdateInternal = engineUpdate;
314
-
315
- function updateCanvas()
316
- {
317
- if (headlessMode) return;
318
-
319
- if (canvasFixedSize.x)
320
- {
321
- // set canvas fixed size
322
- mainCanvasSize = canvasFixedSize.copy();
323
-
324
- // fit to window using css width and height
325
- const innerAspect = innerWidth / innerHeight;
326
- const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
327
- const w = innerAspect < fixedAspect ? '100%' : '';
328
- const h = innerAspect < fixedAspect ? '' : '100%';
329
- mainCanvas.style.width = w;
330
- mainCanvas.style.height = h;
331
- if (glCanvas)
332
- {
333
- glCanvas.style.width = w;
334
- glCanvas.style.height = h;
335
- }
336
- }
337
- else
338
- {
339
- // apply device pixel ratio for crisp rendering
340
- const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
341
- const viewWidth = innerWidth * dpr | 0;
342
- const viewHeight = innerHeight * dpr | 0;
343
-
344
- // get main canvas size based on window size
345
- mainCanvasSize.x = min(viewWidth, canvasMaxSize.x);
346
- mainCanvasSize.y = min(viewHeight, canvasMaxSize.y);
347
-
348
- // responsive aspect ratio with native resolution
349
- const innerAspect = viewWidth / viewHeight;
350
- ASSERT(canvasMinAspect <= canvasMaxAspect);
351
- if (canvasMaxAspect && innerAspect > canvasMaxAspect)
352
- {
353
- // full height
354
- const w = mainCanvasSize.y * canvasMaxAspect | 0;
355
- mainCanvasSize.x = min(w, canvasMaxSize.x);
356
- }
357
- else if (innerAspect < canvasMinAspect)
358
- {
359
- // full width
360
- const h = mainCanvasSize.x / canvasMinAspect | 0;
361
- mainCanvasSize.y = min(h, canvasMaxSize.y);
362
- }
363
-
364
- // set CSS display size so backing store renders at viewport size
365
- const cssW = (mainCanvasSize.x / dpr | 0) + 'px';
366
- const cssH = (mainCanvasSize.y / dpr | 0) + 'px';
367
- mainCanvas.style.width = cssW;
368
- mainCanvas.style.height = cssH;
369
- if (glCanvas)
370
- {
371
- glCanvas.style.width = cssW;
372
- glCanvas.style.height = cssH;
373
- }
374
- }
375
-
376
- // clear main canvas and set size
377
- mainCanvas.width = mainCanvasSize.x;
378
- mainCanvas.height = mainCanvasSize.y;
379
-
380
- // apply the clear color to main canvas
381
- if (canvasClearColor.a > 0 && !glEnable)
382
- {
383
- mainContext.fillStyle = canvasClearColor.toString();
384
- mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
385
- mainContext.fillStyle = BLACK.toString();
386
- }
387
-
388
- // set default line join and cap
389
- mainContext.lineJoin = 'round';
390
- mainContext.lineCap = 'round';
391
- }
392
-
393
- // skip setup if headless
394
- if (headlessMode) return startEngine();
395
-
396
- // setup webgl
397
- glInit(rootElement);
398
-
399
- // setup html
400
- const styleRoot =
401
- 'margin:0;' + // fill the window
402
- 'overflow:hidden;' + // no scroll bars
403
- 'background:#000;' + // set background color
404
- 'user-select:none;' + // prevent hold to select
405
- '-webkit-user-select:none;' + // compatibility for ios
406
- 'touch-action:none;' + // prevent mobile pinch to resize
407
- '-webkit-touch-callout:none'; // compatibility for ios
408
- rootElement.style.cssText = styleRoot;
409
- mainCanvas = rootElement.appendChild(document.createElement('canvas'));
410
- drawContext = mainContext = mainCanvas.getContext('2d');
411
-
412
- // init stuff and start engine
413
- inputInit();
414
- audioInit();
415
- debugInit();
416
-
417
- // setup canvases
418
- // transform way is still more reliable than flexbox or grid
419
- const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
420
- 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
421
- mainCanvas.style.cssText = styleCanvas;
422
- if (glCanvas)
423
- glCanvas.style.cssText = styleCanvas;
424
- setCanvasPixelated(canvasPixelated);
425
- updateCanvas();
426
- glPreRender();
427
-
428
- // create offscreen canvases for image processing
429
- workCanvas = new OffscreenCanvas(64, 64);
430
- workContext = workCanvas.getContext('2d');
431
- workReadCanvas = new OffscreenCanvas(64, 64);
432
- workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
433
-
434
- // create promises for loading images
435
- const promises = imageSources.map((src, i)=> loadTexture(i, src));
436
-
437
- // no images to load
438
- if (!imageSources.length)
439
- promises.push(loadTexture(0));
440
-
441
- // load engine font image
442
- promises.push(imageFontInit());
443
-
444
- if (showSplashScreen)
445
- {
446
- // draw splash screen
447
- promises.push(new Promise(resolve =>
448
- {
449
- let t = 0;
450
- updateSplash();
451
- function updateSplash()
452
- {
453
- inputClear();
454
- drawEngineLogo(t+=.01);
455
- t>1 ? resolve() : setTimeout(updateSplash, 16);
456
- }
457
- }));
458
- }
459
-
460
- // wait for all the promises to finish
461
- await Promise.all(promises);
462
- return startEngine();
463
-
464
- async function startEngine()
465
- {
466
- // wait for gameInit to load
467
- await gameInit();
468
- engineManualStep || engineUpdate();
469
- }
470
- }
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
-
501
- /** Update each engine object, remove destroyed objects, and update time
502
- * can be called manually if objects need to be updated outside of main loop
503
- * @memberof Engine */
504
- function engineObjectsUpdate()
505
- {
506
- // get list of solid objects for physics optimization
507
- engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
508
-
509
- // update physics before object update
510
- for (const o of engineObjects)
511
- if (!o.parent && !o.destroyed)
512
- o.updatePhysics();
513
-
514
- // recursive object update
515
- function updateChildObject(o)
516
- {
517
- if (o.destroyed) return;
518
-
519
- o.update();
520
- for (const child of o.children)
521
- updateChildObject(child);
522
- }
523
- for (const o of engineObjects)
524
- {
525
- if (o.parent || o.destroyed) continue;
526
-
527
- // update top level objects
528
- o.update();
529
- for (const child of o.children)
530
- updateChildObject(child);
531
- o.updateTransforms();
532
- }
533
-
534
- // remove destroyed objects
535
- engineObjects = engineObjects.filter(o=>!o.destroyed);
536
- }
537
-
538
- /** Destroy and remove all objects
539
- * - This can be used to clear out all objects when restarting a level
540
- * - Objects can override their destroy function to do cleanup or stick around
541
- * @param {boolean} [immediate] - should attached effects be allowed to die off?
542
- * @memberof Engine */
543
- function engineObjectsDestroy(immediate=true)
544
- {
545
- for (const o of engineObjects)
546
- o.parent || o.destroy(immediate);
547
- engineObjects = engineObjects.filter(o=>!o.destroyed);
548
- }
549
-
550
- /** Collects all object within a given area
551
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
552
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
553
- * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
554
- * @return {Array<EngineObject>} - List of collected objects
555
- * @memberof Engine */
556
- function engineObjectsCollect(pos, size, objects=engineObjects)
557
- {
558
- const collectedObjects = [];
559
- if (!pos)
560
- {
561
- // all objects
562
- for (const o of objects)
563
- collectedObjects.push(o);
564
- }
565
- else if (size instanceof Vector2)
566
- {
567
- // bounding box test
568
- for (const o of objects)
569
- o.isOverlapping(pos, size) && collectedObjects.push(o);
570
- }
571
- else
572
- {
573
- // circle test
574
- const sizeSquared = size*size;
575
- for (const o of objects)
576
- pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
577
- }
578
- return collectedObjects;
579
- }
580
-
581
- /**
582
- * @callback ObjectCallbackFunction - Function that processes an object
583
- * @param {EngineObject} object
584
- * @memberof Engine
585
- */
586
-
587
- /** Triggers a callback for each object within a given area
588
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
589
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
590
- * @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
591
- * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
592
- * @memberof Engine */
593
- function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
594
- { engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
595
-
596
- /** Return a list of objects intersecting a ray
597
- * @param {Vector2} start
598
- * @param {Vector2} end
599
- * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
600
- * @return {Array<EngineObject>} - List of objects hit
601
- * @memberof Engine */
602
- function engineObjectsRaycast(start, end, objects=engineObjects)
603
- {
604
- const hitObjects = [];
605
- for (const o of objects)
606
- {
607
- if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
608
- {
609
- debugRaycast && debugRect(o.pos, o.size, '#f00');
610
- hitObjects.push(o);
611
- }
612
- }
613
-
614
- debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
615
- return hitObjects;
6
+ /**
7
+ * LittleJS - The Tiny Fast JavaScript Game Engine
8
+ * MIT License - Copyright 2021 Frank Force
9
+ *
10
+ * Engine Features
11
+ * - Object oriented system with EngineObject base class
12
+ * - Automatic object lifecycle (update, physics, collision, rendering)
13
+ * - Engine helper classes: Vector2, Color, Timer, RandomGenerator
14
+ * - Hybrid rendering with WebGL batching and Canvas2D fallback
15
+ * - Audio system with wave, mp3, or ZzFX sound effects
16
+ * - Input system with keyboard, mouse, gamepad, and touch support
17
+ * - Tile layer rendering and collision detection
18
+ * - Particle effect system with emitters
19
+ * - Medal/achievement system with local storage
20
+ * - Comprehensive debug tools and visualizations
21
+ * - Fixed 60 FPS timestep with configurable time scale
22
+ * - Raycast and spatial query utilities
23
+ * - Plugin system for extending engine functionality
24
+ * - Start with engineInit() and provide your game callbacks
25
+ * @namespace Engine
26
+ */
27
+
28
+ /** Name of engine
29
+ * @type {string}
30
+ * @default
31
+ * @memberof Engine */
32
+ const engineName = 'LittleJS';
33
+
34
+ /** Version of engine
35
+ * @type {string}
36
+ * @default
37
+ * @memberof Engine */
38
+ const engineVersion = '1.18.26';
39
+
40
+ /** Frames per second to update
41
+ * @type {number}
42
+ * @default
43
+ * @memberof Engine */
44
+ const frameRate = 60;
45
+
46
+ /** How many seconds each frame lasts, engine uses a fixed time step
47
+ * @type {number}
48
+ * @default 1/60
49
+ * @memberof Engine */
50
+ const timeDelta = 1/frameRate;
51
+
52
+ /** Array containing all engine objects
53
+ * @type {Array<EngineObject>}
54
+ * @memberof Engine */
55
+ let engineObjects = [];
56
+
57
+ /** Array with only objects set to collide with other objects this frame (for optimization)
58
+ * @type {Array<EngineObject>}
59
+ * @memberof Engine */
60
+ let engineObjectsCollide = [];
61
+
62
+ /** Current update frame, used to calculate time
63
+ * @type {number}
64
+ * @memberof Engine */
65
+ let frame = 0;
66
+
67
+ /** Current engine time since start in seconds
68
+ * @type {number}
69
+ * @memberof Engine */
70
+ let time = 0;
71
+
72
+ /** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
73
+ * @type {number}
74
+ * @memberof Engine */
75
+ let timeReal = 0;
76
+
77
+ /** Is the game paused? Causes time and objects to not be updated
78
+ * @type {boolean}
79
+ * @default false
80
+ * @memberof Engine */
81
+ let paused = false;
82
+
83
+ /** Get if game is paused
84
+ * @return {boolean}
85
+ * @memberof Engine */
86
+ function getPaused() { return paused; }
87
+
88
+ /** Set if game is paused
89
+ * @param {boolean} [isPaused]
90
+ * @memberof Engine */
91
+ function setPaused(isPaused=true) { paused = isPaused; }
92
+
93
+ // Engine internal variables
94
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
95
+ let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
96
+ let showEngineVersion = true;
97
+
98
+ ///////////////////////////////////////////////////////////////////////////////
99
+ // plugin hooks
100
+
101
+ const pluginList = [];
102
+ class EnginePlugin
103
+ {
104
+ constructor(update, render, glContextLost, glContextRestored)
105
+ {
106
+ this.update = update;
107
+ this.render = render;
108
+ this.glContextLost = glContextLost;
109
+ this.glContextRestored = glContextRestored;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * @callback PluginCallback - Update or render function for a plugin
115
+ * @memberof Engine
116
+ */
117
+
118
+ /** Add a new update function for a plugin
119
+ * @param {PluginCallback} [update]
120
+ * @param {PluginCallback} [render]
121
+ * @param {PluginCallback} [glContextLost]
122
+ * @param {PluginCallback} [glContextRestored]
123
+ * @memberof Engine */
124
+ function engineAddPlugin(update, render, glContextLost, glContextRestored)
125
+ {
126
+ // make sure plugin functions are unique
127
+ ASSERT(!pluginList.find(p=>
128
+ p.update === update && p.render === render &&
129
+ p.glContextLost === glContextLost &&
130
+ p.glContextRestored === glContextRestored));
131
+
132
+ const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
133
+ pluginList.push(plugin);
134
+ }
135
+
136
+ ///////////////////////////////////////////////////////////////////////////////
137
+ // Main Engine Functions
138
+
139
+ /**
140
+ * @callback GameInitCallback - Called after the engine starts, can be async
141
+ * @return {void|Promise<void>}
142
+ * @memberof Engine
143
+ */
144
+ /**
145
+ * @callback GameCallback - Update or render function for the game
146
+ * @memberof Engine
147
+ */
148
+
149
+ /** Startup LittleJS engine with your callback functions
150
+ * @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
151
+ * @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
152
+ * @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
153
+ * @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
154
+ * @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
155
+ * @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
156
+ * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
157
+ * @example
158
+ * // Basic engine startup
159
+ * engineInit(
160
+ * ()=> { LOG('Game initialized!'); }, // gameInit
161
+ * ()=> { updateGameLogic(); }, // gameUpdate
162
+ * ()=> { updateUI(); }, // gameUpdatePost
163
+ * ()=> { drawBackground(); }, // gameRender
164
+ * ()=> { drawHUD(); }, // gameRenderPost
165
+ * ['tiles.png', 'tilesLevel.png'] // images to load
166
+ * );
167
+ * @memberof Engine */
168
+ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
169
+ {
170
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
171
+ ASSERT(!mainContext, 'engine already initialized');
172
+ // runtime guard so release builds (where the assert is stripped) don't
173
+ // double-register listeners / double-add canvases on a second call
174
+ if (mainContext) return;
175
+ ASSERT(isArray(imageSources), 'pass in images as array');
176
+
177
+ // ensure body exists for minimal HTML where the script runs before <body> is parsed
178
+ if (!document.body)
179
+ document.documentElement.appendChild(document.createElement('body'));
180
+ rootElement ||= document.body;
181
+
182
+ // allow passing in empty functions
183
+ gameInit ||= ()=>{};
184
+ gameUpdate ||= ()=>{};
185
+ gameUpdatePost ||= ()=>{};
186
+ gameRender ||= ()=>{};
187
+ gameRenderPost ||= ()=>{};
188
+
189
+ // Called automatically by engine to setup render system
190
+ function enginePreRender()
191
+ {
192
+ // save canvas size
193
+ mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
194
+
195
+ // disable smoothing for pixel art
196
+ mainContext.imageSmoothingEnabled = !tilesPixelated;
197
+
198
+ // setup gl rendering if enabled
199
+ glPreRender();
200
+ }
201
+
202
+ // internal update loop for engine
203
+ function engineUpdate(frameTimeMS=0)
204
+ {
205
+ // update time keeping
206
+ let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
207
+ // skip delta on the very first frame so timeReal doesn't jump
208
+ // by ~page-load-time when RAF starts handing real timestamps
209
+ if (!frameTimeLastMS) frameTimeDeltaMS = 0;
210
+ frameTimeLastMS = frameTimeMS;
211
+ if (debug || debugWatermark)
212
+ averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
213
+ const debugSpeedUp = debug && keyIsDown('Equal'); // +
214
+ const debugSpeedDown = debug && keyIsDown('Minus'); // -
215
+ const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
216
+
217
+ // apply time deltas
218
+ timeReal += frameTimeDeltaMS * debugScale / 1e3;
219
+ const combinedScale = timeScale * debugScale;
220
+ frameTimeDeltaMS *= combinedScale;
221
+ frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
222
+ if (combinedScale <= 1)
223
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
224
+
225
+ let wasUpdated = false;
226
+ if (paused)
227
+ {
228
+ // update everything except the game and objects
229
+ wasUpdated = true;
230
+ updateCanvas();
231
+ inputUpdate();
232
+ pluginList.forEach(plugin=>plugin.update?.());
233
+
234
+ // update object transforms even when paused
235
+ for (const o of engineObjects)
236
+ o.parent || o.updateTransforms();
237
+
238
+ // do post update
239
+ debugUpdate();
240
+ gameUpdatePost();
241
+ inputUpdatePost();
242
+ if (debugVideoCaptureIsActive())
243
+ renderFrame();
244
+ }
245
+ else
246
+ {
247
+ // apply time delta smoothing, improves smoothness of framerate in some browsers
248
+ let deltaSmooth = 0;
249
+ if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
250
+ {
251
+ // force at least one update each frame since it is waiting for refresh
252
+ deltaSmooth = frameTimeBufferMS;
253
+ frameTimeBufferMS = 0;
254
+ }
255
+
256
+ // update multiple frames if necessary in case of slow framerate
257
+ for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
258
+ {
259
+ // increment frame and update time
260
+ time = frame++ / frameRate;
261
+
262
+ // update game and objects
263
+ wasUpdated = true;
264
+ updateCanvas();
265
+ inputUpdate();
266
+ gameUpdate();
267
+ pluginList.forEach(plugin=>plugin.update?.());
268
+ engineObjectsUpdate();
269
+
270
+ // do post update
271
+ debugUpdate();
272
+ gameUpdatePost();
273
+ inputUpdatePost();
274
+ if (debugVideoCaptureIsActive())
275
+ renderFrame();
276
+ }
277
+
278
+ // add the time smoothing back in
279
+ frameTimeBufferMS += deltaSmooth;
280
+ }
281
+
282
+ if (!debugVideoCaptureIsActive())
283
+ renderFrame();
284
+ if (!engineManualStep)
285
+ requestAnimationFrame(engineUpdate);
286
+
287
+ function renderFrame()
288
+ {
289
+ if (headlessMode) return;
290
+
291
+ // canvas must be updated before rendering
292
+ if (!wasUpdated)
293
+ updateCanvas();
294
+
295
+ // render the game and objects
296
+ enginePreRender();
297
+ gameRender();
298
+ engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
299
+ for (const o of engineObjects)
300
+ o.destroyed || o.render();
301
+
302
+ // post rendering
303
+ gameRenderPost();
304
+ pluginList.forEach(plugin=>plugin.render?.());
305
+ inputRender();
306
+ debugRender();
307
+ glFlush();
308
+ debugRenderPost();
309
+ drawCount = 0;
310
+ primitiveCount = 0;
311
+ }
312
+ }
313
+ engineUpdateInternal = engineUpdate;
314
+
315
+ function updateCanvas()
316
+ {
317
+ if (headlessMode) return;
318
+
319
+ if (canvasFixedSize.x)
320
+ {
321
+ // set canvas fixed size
322
+ mainCanvasSize = canvasFixedSize.copy();
323
+
324
+ // fit to window using css width and height
325
+ const innerAspect = innerWidth / innerHeight;
326
+ const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
327
+ const w = innerAspect < fixedAspect ? '100%' : '';
328
+ const h = innerAspect < fixedAspect ? '' : '100%';
329
+ mainCanvas.style.width = w;
330
+ mainCanvas.style.height = h;
331
+ if (glCanvas)
332
+ {
333
+ glCanvas.style.width = w;
334
+ glCanvas.style.height = h;
335
+ }
336
+ }
337
+ else
338
+ {
339
+ // apply device pixel ratio for crisp rendering
340
+ const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
341
+ const viewWidth = innerWidth * dpr | 0;
342
+ const viewHeight = innerHeight * dpr | 0;
343
+
344
+ // get main canvas size based on window size
345
+ mainCanvasSize.x = min(viewWidth, canvasMaxSize.x);
346
+ mainCanvasSize.y = min(viewHeight, canvasMaxSize.y);
347
+
348
+ // responsive aspect ratio with native resolution
349
+ const innerAspect = viewWidth / viewHeight;
350
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
351
+ if (canvasMaxAspect && innerAspect > canvasMaxAspect)
352
+ {
353
+ // full height
354
+ const w = mainCanvasSize.y * canvasMaxAspect | 0;
355
+ mainCanvasSize.x = min(w, canvasMaxSize.x);
356
+ }
357
+ else if (innerAspect < canvasMinAspect)
358
+ {
359
+ // full width
360
+ const h = mainCanvasSize.x / canvasMinAspect | 0;
361
+ mainCanvasSize.y = min(h, canvasMaxSize.y);
362
+ }
363
+
364
+ // set CSS display size so backing store renders at viewport size
365
+ const cssW = (mainCanvasSize.x / dpr | 0) + 'px';
366
+ const cssH = (mainCanvasSize.y / dpr | 0) + 'px';
367
+ mainCanvas.style.width = cssW;
368
+ mainCanvas.style.height = cssH;
369
+ if (glCanvas)
370
+ {
371
+ glCanvas.style.width = cssW;
372
+ glCanvas.style.height = cssH;
373
+ }
374
+ }
375
+
376
+ // clear main canvas and set size
377
+ mainCanvas.width = mainCanvasSize.x;
378
+ mainCanvas.height = mainCanvasSize.y;
379
+
380
+ // apply the clear color to main canvas
381
+ if (canvasClearColor.a > 0 && !glEnable)
382
+ {
383
+ mainContext.fillStyle = canvasClearColor.toString();
384
+ mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
385
+ mainContext.fillStyle = BLACK.toString();
386
+ }
387
+
388
+ // set default line join and cap
389
+ mainContext.lineJoin = 'round';
390
+ mainContext.lineCap = 'round';
391
+ }
392
+
393
+ // skip setup if headless
394
+ if (headlessMode) return startEngine();
395
+
396
+ // setup webgl
397
+ glInit(rootElement);
398
+
399
+ // setup html
400
+ const styleRoot =
401
+ 'margin:0;' + // fill the window
402
+ 'overflow:hidden;' + // no scroll bars
403
+ 'background:#000;' + // set background color
404
+ 'user-select:none;' + // prevent hold to select
405
+ '-webkit-user-select:none;' + // compatibility for ios
406
+ 'touch-action:none;' + // prevent mobile pinch to resize
407
+ '-webkit-touch-callout:none'; // compatibility for ios
408
+ rootElement.style.cssText = styleRoot;
409
+ mainCanvas = rootElement.appendChild(document.createElement('canvas'));
410
+ drawContext = mainContext = mainCanvas.getContext('2d');
411
+
412
+ // init stuff and start engine
413
+ inputInit();
414
+ audioInit();
415
+ debugInit();
416
+
417
+ // setup canvases
418
+ // transform way is still more reliable than flexbox or grid
419
+ const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
420
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
421
+ mainCanvas.style.cssText = styleCanvas;
422
+ if (glCanvas)
423
+ glCanvas.style.cssText = styleCanvas;
424
+ setCanvasPixelated(canvasPixelated);
425
+ updateCanvas();
426
+ glPreRender();
427
+
428
+ // create offscreen canvases for image processing
429
+ workCanvas = new OffscreenCanvas(64, 64);
430
+ workContext = workCanvas.getContext('2d');
431
+ workReadCanvas = new OffscreenCanvas(64, 64);
432
+ workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
433
+
434
+ // create promises for loading images
435
+ const promises = imageSources.map((src, i)=> loadTexture(i, src));
436
+
437
+ // no images to load
438
+ if (!imageSources.length)
439
+ promises.push(loadTexture(0));
440
+
441
+ // load engine font image
442
+ promises.push(imageFontInit());
443
+
444
+ if (showSplashScreen)
445
+ {
446
+ // draw splash screen
447
+ promises.push(new Promise(resolve =>
448
+ {
449
+ let t = 0;
450
+ updateSplash();
451
+ function updateSplash()
452
+ {
453
+ inputClear();
454
+ drawEngineLogo(t+=.01);
455
+ t>1 ? resolve() : setTimeout(updateSplash, 16);
456
+ }
457
+ }));
458
+ }
459
+
460
+ // wait for all the promises to finish
461
+ await Promise.all(promises);
462
+ return startEngine();
463
+
464
+ async function startEngine()
465
+ {
466
+ // wait for gameInit to load
467
+ await gameInit();
468
+ engineManualStep || engineUpdate();
469
+ }
470
+ }
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
+
501
+ /** Update each engine object, remove destroyed objects, and update time
502
+ * can be called manually if objects need to be updated outside of main loop
503
+ * @memberof Engine */
504
+ function engineObjectsUpdate()
505
+ {
506
+ // get list of solid objects for physics optimization
507
+ engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
508
+
509
+ // update physics before object update
510
+ for (const o of engineObjects)
511
+ if (!o.parent && !o.destroyed)
512
+ o.updatePhysics();
513
+
514
+ // recursive object update
515
+ function updateChildObject(o)
516
+ {
517
+ if (o.destroyed) return;
518
+
519
+ o.update();
520
+ for (const child of o.children)
521
+ updateChildObject(child);
522
+ }
523
+ for (const o of engineObjects)
524
+ {
525
+ if (o.parent || o.destroyed) continue;
526
+
527
+ // update top level objects
528
+ o.update();
529
+ for (const child of o.children)
530
+ updateChildObject(child);
531
+ o.updateTransforms();
532
+ }
533
+
534
+ // remove destroyed objects
535
+ engineObjects = engineObjects.filter(o=>!o.destroyed);
536
+ }
537
+
538
+ /** Destroy and remove all objects
539
+ * - This can be used to clear out all objects when restarting a level
540
+ * - Objects can override their destroy function to do cleanup or stick around
541
+ * @param {boolean} [immediate] - should attached effects be allowed to die off?
542
+ * @memberof Engine */
543
+ function engineObjectsDestroy(immediate=true)
544
+ {
545
+ for (const o of engineObjects)
546
+ o.parent || o.destroy(immediate);
547
+ engineObjects = engineObjects.filter(o=>!o.destroyed);
548
+ }
549
+
550
+ /** Collects all object within a given area
551
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
552
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
553
+ * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
554
+ * @return {Array<EngineObject>} - List of collected objects
555
+ * @memberof Engine */
556
+ function engineObjectsCollect(pos, size, objects=engineObjects)
557
+ {
558
+ const collectedObjects = [];
559
+ if (!pos)
560
+ {
561
+ // all objects
562
+ for (const o of objects)
563
+ collectedObjects.push(o);
564
+ }
565
+ else if (size instanceof Vector2)
566
+ {
567
+ // bounding box test
568
+ for (const o of objects)
569
+ o.isOverlapping(pos, size) && collectedObjects.push(o);
570
+ }
571
+ else
572
+ {
573
+ // circle test
574
+ const sizeSquared = size*size;
575
+ for (const o of objects)
576
+ pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
577
+ }
578
+ return collectedObjects;
579
+ }
580
+
581
+ /**
582
+ * @callback ObjectCallbackFunction - Function that processes an object
583
+ * @param {EngineObject} object
584
+ * @memberof Engine
585
+ */
586
+
587
+ /** Triggers a callback for each object within a given area
588
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
589
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
590
+ * @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
591
+ * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
592
+ * @memberof Engine */
593
+ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
594
+ { engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
595
+
596
+ /** Return a list of objects intersecting a ray
597
+ * @param {Vector2} start
598
+ * @param {Vector2} end
599
+ * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
600
+ * @return {Array<EngineObject>} - List of objects hit
601
+ * @memberof Engine */
602
+ function engineObjectsRaycast(start, end, objects=engineObjects)
603
+ {
604
+ const hitObjects = [];
605
+ for (const o of objects)
606
+ {
607
+ if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
608
+ {
609
+ debugRaycast && debugRect(o.pos, o.size, '#f00');
610
+ hitObjects.push(o);
611
+ }
612
+ }
613
+
614
+ debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
615
+ return hitObjects;
616
616
  }
617
617
  /**
618
618
  * LittleJS Debug System
@@ -5525,9 +5525,15 @@ class ImageFont
5525
5525
  tileInfo.pos.x = x*sizePaddedX + padding;
5526
5526
  tileInfo.pos.y = y*sizePaddedY + padding;
5527
5527
 
5528
- // draw the tile
5529
- drawPos.x = pos.x + i * size.x - centerOffset |0;
5530
- drawPos.y = pos.y + j * size.y |0;
5528
+ // snap the glyph edges to whole pixels
5529
+ // tiles are drawn from their center, so snapping the center
5530
+ // to a whole pixel puts the edges on half pixels when the
5531
+ // size is even, and a row or column of the glyph then has
5532
+ // no pixel center inside it and is not rasterized at all
5533
+ // ceil picks the nearest aligned position, breaking ties
5534
+ // downward to match how this used to truncate
5535
+ drawPos.x = ceil(pos.x + i * size.x - centerOffset - size.x/2) + size.x/2 - .5;
5536
+ drawPos.y = ceil(pos.y + j * size.y - size.y/2) + size.y/2 - .5;
5531
5537
  drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
5532
5538
  }
5533
5539
  });