• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.14
/src/engine/Engine.ts
1
import { EX_VERSION } from './';
2
import { Future } from './Util/Future';
3
import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter';
4
import { PointerScope } from './Input/PointerScope';
5
import { Flags } from './Flags';
6
import { polyfill } from './Polyfill';
7
polyfill();
1✔
8
import { CanUpdate, CanDraw, CanInitialize } from './Interfaces/LifecycleEvents';
9
import { Vector } from './Math/vector';
10
import { Screen, DisplayMode, Resolution, ViewportDimension } from './Screen';
11
import { ScreenElement } from './ScreenElement';
12
import { Actor } from './Actor';
13
import { Timer } from './Timer';
14
import { TileMap } from './TileMap';
15
import { DefaultLoader } from './Director/DefaultLoader';
16
import { Loader } from './Director/Loader';
17
import { Detector } from './Util/Detector';
18
import {
19
  VisibleEvent,
20
  HiddenEvent,
21
  GameStartEvent,
22
  GameStopEvent,
23
  PreUpdateEvent,
24
  PostUpdateEvent,
25
  PreFrameEvent,
26
  PostFrameEvent,
27
  PreDrawEvent,
28
  PostDrawEvent,
29
  InitializeEvent
30
} from './Events';
31
import { Logger, LogLevel } from './Util/Log';
32
import { Color } from './Color';
33
import { Scene, SceneConstructor, isSceneConstructor } from './Scene';
34
import { Entity } from './EntityComponentSystem/Entity';
35
import { DebugConfig, DebugStats } from './Debug/DebugConfig';
36
import { BrowserEvents } from './Util/Browser';
37
import {
38
  AntialiasOptions,
39
  DefaultAntialiasOptions,
40
  DefaultPixelArtOptions,
41
  ExcaliburGraphicsContext,
42
  ExcaliburGraphicsContext2DCanvas,
43
  ExcaliburGraphicsContextWebGL,
44
  TextureLoader
45
} from './Graphics';
46
import { Clock, StandardClock } from './Util/Clock';
47
import { ImageFiltering } from './Graphics/Filtering';
48
import { GraphicsDiagnostics } from './Graphics/GraphicsDiagnostics';
49
import { Toaster } from './Util/Toaster';
50
import { InputMapper } from './Input/InputMapper';
51
import { GoToOptions, SceneMap, Director, StartOptions, SceneWithOptions, WithRoot, DirectorEvents } from './Director/Director';
52
import { InputHost } from './Input/InputHost';
53
import { getDefaultPhysicsConfig, PhysicsConfig } from './Collision/PhysicsConfig';
54
import { DeepRequired } from './Util/Required';
55
import { Context, createContext, useContext } from './Context';
56
import { DefaultGarbageCollectionOptions, GarbageCollectionOptions, GarbageCollector } from './GarbageCollector';
57
import { mergeDeep } from './Util/Util';
58

59
export type EngineEvents = DirectorEvents & {
60
  fallbackgraphicscontext: ExcaliburGraphicsContext2DCanvas;
61
  initialize: InitializeEvent<Engine>;
62
  visible: VisibleEvent;
63
  hidden: HiddenEvent;
64
  start: GameStartEvent;
65
  stop: GameStopEvent;
66
  preupdate: PreUpdateEvent<Engine>;
67
  postupdate: PostUpdateEvent<Engine>;
68
  preframe: PreFrameEvent;
69
  postframe: PostFrameEvent;
70
  predraw: PreDrawEvent;
71
  postdraw: PostDrawEvent;
72
};
73

74
export const EngineEvents = {
1✔
75
  FallbackGraphicsContext: 'fallbackgraphicscontext',
76
  Initialize: 'initialize',
77
  Visible: 'visible',
78
  Hidden: 'hidden',
79
  Start: 'start',
80
  Stop: 'stop',
81
  PreUpdate: 'preupdate',
82
  PostUpdate: 'postupdate',
83
  PreFrame: 'preframe',
84
  PostFrame: 'postframe',
85
  PreDraw: 'predraw',
86
  PostDraw: 'postdraw',
87
  ...DirectorEvents
88
} as const;
89

90
/**
91
 * Enum representing the different mousewheel event bubble prevention
92
 */
93
export enum ScrollPreventionMode {
1✔
94
  /**
95
   * Do not prevent any page scrolling
96
   */
97
  None,
1✔
98
  /**
99
   * Prevent page scroll if mouse is over the game canvas
100
   */
101
  Canvas,
1✔
102
  /**
103
   * Prevent all page scrolling via mouse wheel
104
   */
105
  All
1✔
106
}
107

108
/**
109
 * Defines the available options to configure the Excalibur engine at constructor time.
110
 */
111
export interface EngineOptions<TKnownScenes extends string = any> {
112
  /**
113
   * Optionally configure the width of the viewport in css pixels
114
   */
115
  width?: number;
116

117
  /**
118
   * Optionally configure the height of the viewport in css pixels
119
   */
120
  height?: number;
121

122
  /**
123
   * Optionally configure the width & height of the viewport in css pixels.
124
   * Use `viewport` instead of {@apilink EngineOptions.width} and {@apilink EngineOptions.height}, or vice versa.
125
   */
126
  viewport?: ViewportDimension;
127

128
  /**
129
   * Optionally specify the size the logical pixel resolution, if not specified it will be width x height.
130
   * See {@apilink Resolution} for common presets.
131
   */
132
  resolution?: Resolution;
133

134
  /**
135
   * Optionally specify antialiasing (smoothing), by default true (smooth pixels)
136
   *
137
   *  * `true` - useful for high resolution art work you would like smoothed, this also hints excalibur to load images
138
   * with default blending {@apilink ImageFiltering.Blended}
139
   *
140
   *  * `false` - useful for pixel art style art work you would like sharp, this also hints excalibur to load images
141
   * with default blending {@apilink ImageFiltering.Pixel}
142
   *
143
   * * {@apilink AntialiasOptions} Optionally deeply configure the different antialiasing settings, **WARNING** thar be dragons here.
144
   * It is recommended you stick to `true` or `false` unless you understand what you're doing and need to control rendering to
145
   * a high degree.
146
   */
147
  antialiasing?: boolean | AntialiasOptions;
148

149
  /**
150
   * Optionally specify excalibur garbage collection, by default true.
151
   *
152
   * * `true` - garbage collection defaults are enabled (default)
153
   *
154
   * * `false` - garbage collection is completely disabled (not recommended)
155
   *
156
   * * {@apilink GarbageCollectionOptions} Optionally deeply configure garbage collection settings, **WARNING** thar be dragons here.
157
   * It is recommended you stick to `true` or `false` unless you understand what you're doing, it is possible to get into a downward
158
   * spiral if collection timings are set too low where you are stuck in repeated collection.
159
   */
160
  garbageCollection?: boolean | GarbageCollectionOptions;
161

162
  /**
163
   * Quick convenience property to configure Excalibur to use special settings for "pretty" anti-aliased pixel art
164
   *
165
   * 1. Turns on special shader condition to blend for pixel art and enables various antialiasing settings,
166
   *  notice blending is ON for this special mode.
167
   *
168
   * Equivalent to:
169
   * ```javascript
170
   * antialiasing: {
171
   *  pixelArtSampler: true,
172
   *  canvasImageRendering: 'auto',
173
   *  filtering: ImageFiltering.Blended,
174
   *  webglAntialiasing: true
175
   * }
176
   * ```
177
   */
178
  pixelArt?: boolean;
179

180
  /**
181
   * Specify any UV padding you want use in pixels, this brings sampling into the texture if you're using
182
   * a sprite sheet in one image to prevent sampling bleed.
183
   *
184
   * Defaults:
185
   * * `antialiasing: false` or `filtering: ImageFiltering.Pixel` - 0.0;
186
   * * `pixelArt: true` - 0.25
187
   * * All else 0.01
188
   */
189
  uvPadding?: number;
190

191
  /**
192
   * Optionally hint the graphics context into a specific power profile
193
   *
194
   * Default "high-performance"
195
   */
196
  powerPreference?: 'default' | 'high-performance' | 'low-power';
197

198
  /**
199
   * Optionally upscale the number of pixels in the canvas. Normally only useful if you need a smoother look to your assets, especially
200
   * {@apilink Text} or Pixel Art assets.
201
   *
202
   * **WARNING** It is recommended you try using `antialiasing: true` before adjusting pixel ratio. Pixel ratio will consume more memory
203
   * and on mobile may break if the internal size of the canvas exceeds 4k pixels in width or height.
204
   *
205
   * Default is based the display's pixel ratio, for example a HiDPI screen might have the value 2;
206
   */
207
  pixelRatio?: number;
208

209
  /**
210
   * Optionally configure the native canvas transparent backdrop
211
   */
212
  enableCanvasTransparency?: boolean;
213

214
  /**
215
   * Optionally specify the target canvas DOM element to render the game in
216
   */
217
  canvasElementId?: string;
218

219
  /**
220
   * Optionally specify the target canvas DOM element directly
221
   */
222
  canvasElement?: HTMLCanvasElement;
223

224
  /**
225
   * Optionally enable the right click context menu on the canvas
226
   *
227
   * Default if unset is false
228
   */
229
  enableCanvasContextMenu?: boolean;
230

231
  /**
232
   * Optionally snap graphics to nearest pixel, default is false
233
   */
234
  snapToPixel?: boolean;
235

236
  /**
237
   * The {@apilink DisplayMode} of the game, by default {@apilink DisplayMode.FitScreen} with aspect ratio 4:3 (800x600).
238
   * Depending on this value, {@apilink width} and {@apilink height} may be ignored.
239
   */
240
  displayMode?: DisplayMode;
241

242
  /**
243
   * Configures the pointer scope. Pointers scoped to the 'Canvas' can only fire events within the canvas viewport; whereas, 'Document'
244
   * (default) scoped will fire anywhere on the page.
245
   */
246
  pointerScope?: PointerScope;
247

248
  /**
249
   * Suppress boot up console message, which contains the "powered by Excalibur message"
250
   */
251
  suppressConsoleBootMessage?: boolean;
252

253
  /**
254
   * Suppress minimum browser feature detection, it is not recommended users of excalibur switch this off. This feature ensures that
255
   * the currently running browser meets the minimum requirements for running excalibur. This can be useful if running on non-standard
256
   * browsers or if there is a bug in excalibur preventing execution.
257
   */
258
  suppressMinimumBrowserFeatureDetection?: boolean;
259

260
  /**
261
   * Suppress HiDPI auto detection and scaling, it is not recommended users of excalibur switch off this feature. This feature detects
262
   * and scales the drawing canvas appropriately to accommodate HiDPI screens.
263
   */
264
  suppressHiDPIScaling?: boolean;
265

266
  /**
267
   * Suppress play button, it is not recommended users of excalibur switch this feature. Some browsers require a user gesture (like a click)
268
   * for certain browser features to work like web audio.
269
   */
270
  suppressPlayButton?: boolean;
271

272
  /**
273
   * Sets the focus of the window, this is needed when hosting excalibur in a cross-origin iframe in order for certain events
274
   * (like keyboard) to work.
275
   * For example: itch.io or codesandbox.io
276
   *
277
   * By default set to true,
278
   */
279
  grabWindowFocus?: boolean;
280

281
  /**
282
   * Scroll prevention method.
283
   */
284
  scrollPreventionMode?: ScrollPreventionMode;
285

286
  /**
287
   * Optionally set the background color
288
   */
289
  backgroundColor?: Color;
290

291
  /**
292
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
293
   *
294
   * You may want to constrain max fps if your game cannot maintain fps consistently, it can look and feel better to have a 30fps game than
295
   * one that bounces between 30fps and 60fps
296
   */
297
  maxFps?: number;
298

299
  /**
300
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
301
   * set the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
302
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
303
   * there could be X updates and 1 draw each clock step.
304
   *
305
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
306
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
307
   *
308
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
309
   *
310
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
311
   */
312
  fixedUpdateTimestep?: number;
313

314
  /**
315
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
316
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
317
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
318
   * there could be X updates and 1 draw each clock step.
319
   *
320
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
321
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
322
   *
323
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
324
   *
325
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
326
   */
327
  fixedUpdateFps?: number;
328

329
  /**
330
   * Default `true`, optionally configure excalibur to use optimal draw call sorting, to opt out set this to `false`.
331
   *
332
   * Excalibur will automatically sort draw calls by z and priority into renderer batches for maximal draw performance,
333
   * this can disrupt a specific desired painter order.
334
   *
335
   */
336
  useDrawSorting?: boolean;
337

338
  /**
339
   * Optionally provide a custom handler for the webgl context lost event
340
   */
341
  handleContextLost?: (e: Event) => void;
342

343
  /**
344
   * Optionally provide a custom handler for the webgl context restored event
345
   */
346
  handleContextRestored?: (e: Event) => void;
347

348
  /**
349
   * Optionally configure how excalibur handles poor performance on a player's browser
350
   */
351
  configurePerformanceCanvas2DFallback?: {
352
    /**
353
     * By default `false`, this will switch the internal graphics context to Canvas2D which can improve performance on non hardware
354
     * accelerated browsers.
355
     */
356
    allow: boolean;
357
    /**
358
     * By default `false`, if set to `true` a dialogue will be presented to the player about their browser and how to potentially
359
     * address any issues.
360
     */
361
    showPlayerMessage?: boolean;
362
    /**
363
     * Default `{ numberOfFrames: 100, fps: 20 }`, optionally configure excalibur to fallback to the 2D Canvas renderer
364
     * if bad performance is detected.
365
     *
366
     * In this example of the default if excalibur is running at 20fps or less for 100 frames it will trigger the fallback to the 2D
367
     * Canvas renderer.
368
     */
369
    threshold?: { numberOfFrames: number; fps: number };
370
  };
371

372
  /**
373
   * Optionally configure the physics simulation in excalibur
374
   *
375
   * If false, Excalibur will not produce a physics simulation.
376
   *
377
   * Default is configured to use {@apilink SolverStrategy.Arcade} physics simulation
378
   */
379
  physics?: boolean | PhysicsConfig;
380

381
  /**
382
   * Optionally specify scenes with their transitions and loaders to excalibur's scene {@apilink Director}
383
   *
384
   * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goToScene`
385
   */
386
  scenes?: SceneMap<TKnownScenes>;
387
}
388

389
/**
390
 * The Excalibur Engine
391
 *
392
 * The {@apilink Engine} is the main driver for a game. It is responsible for
393
 * starting/stopping the game, maintaining state, transmitting events,
394
 * loading resources, and managing the scene.
395
 */
396
export class Engine<TKnownScenes extends string = any> implements CanInitialize, CanUpdate, CanDraw {
397
  static Context: Context<Engine | null> = createContext<Engine | null>();
1✔
398
  static useEngine(): Engine {
UNCOV
399
    const value = useContext(Engine.Context);
×
400

UNCOV
401
    if (!value) {
×
UNCOV
402
      throw new Error('Cannot inject engine with `useEngine()`, `useEngine()` was called outside of Engine lifecycle scope.');
×
403
    }
404

UNCOV
405
    return value;
×
406
  }
407
  static InstanceCount = 0;
1✔
408

409
  /**
410
   * Anything run under scope can use `useEngine()` to inject the current engine
411
   * @param cb
412
   */
UNCOV
413
  scope = <TReturn>(cb: () => TReturn) => Engine.Context.scope(this, cb);
×
414

415
  private _garbageCollector: GarbageCollector;
416

417
  public readonly garbageCollectorConfig: GarbageCollectionOptions | null;
418

419
  /**
420
   * Current Excalibur version string
421
   *
422
   * Useful for plugins or other tools that need to know what features are available
423
   */
UNCOV
424
  public readonly version = EX_VERSION;
×
425

426
  /**
427
   * Listen to and emit events on the Engine
428
   */
UNCOV
429
  public events = new EventEmitter<EngineEvents>();
×
430

431
  /**
432
   * Excalibur browser events abstraction used for wiring to native browser events safely
433
   */
434
  public browser: BrowserEvents;
435

436
  /**
437
   * Screen abstraction
438
   */
439
  public screen: Screen;
440

441
  /**
442
   * Scene director, manages all scenes, scene transitions, and loaders in excalibur
443
   */
444
  public director: Director<TKnownScenes>;
445

446
  /**
447
   * Direct access to the engine's canvas element
448
   */
449
  public canvas: HTMLCanvasElement;
450

451
  /**
452
   * Direct access to the ExcaliburGraphicsContext used for drawing things to the screen
453
   */
454
  public graphicsContext: ExcaliburGraphicsContext;
455

456
  /**
457
   * Direct access to the canvas element ID, if an ID exists
458
   */
459
  public canvasElementId: string;
460

461
  /**
462
   * Direct access to the physics configuration for excalibur
463
   */
464
  public physics: DeepRequired<PhysicsConfig>;
465

466
  /**
467
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
468
   *
469
   * You may want to constrain max fps if your game cannot maintain fps consistently, it can look and feel better to have a 30fps game than
470
   * one that bounces between 30fps and 60fps
471
   */
UNCOV
472
  public maxFps: number = Number.POSITIVE_INFINITY;
×
473

474
  /**
475
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
476
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
477
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
478
   * there could be X updates and 1 draw each clock step.
479
   *
480
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
481
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
482
   *
483
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
484
   *
485
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
486
   */
487
  public readonly fixedUpdateFps?: number;
488

489
  /**
490
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
491
   * set the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
492
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
493
   * there could be X updates and 1 draw each clock step.
494
   *
495
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
496
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
497
   *
498
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
499
   *
500
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
501
   */
502
  public readonly fixedUpdateTimestep?: number;
503

504
  /**
505
   * Direct access to the excalibur clock
506
   */
507
  public clock: Clock;
508

509
  public readonly pointerScope: PointerScope;
510
  public readonly grabWindowFocus: boolean;
511

512
  /**
513
   * The width of the game canvas in pixels (physical width component of the
514
   * resolution of the canvas element)
515
   */
516
  public get canvasWidth(): number {
UNCOV
517
    return this.screen.canvasWidth;
×
518
  }
519

520
  /**
521
   * Returns half width of the game canvas in pixels (half physical width component)
522
   */
523
  public get halfCanvasWidth(): number {
UNCOV
524
    return this.screen.halfCanvasWidth;
×
525
  }
526

527
  /**
528
   * The height of the game canvas in pixels, (physical height component of
529
   * the resolution of the canvas element)
530
   */
531
  public get canvasHeight(): number {
UNCOV
532
    return this.screen.canvasHeight;
×
533
  }
534

535
  /**
536
   * Returns half height of the game canvas in pixels (half physical height component)
537
   */
538
  public get halfCanvasHeight(): number {
UNCOV
539
    return this.screen.halfCanvasHeight;
×
540
  }
541

542
  /**
543
   * Returns the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
544
   */
545
  public get drawWidth(): number {
UNCOV
546
    return this.screen.drawWidth;
×
547
  }
548

549
  /**
550
   * Returns half the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
551
   */
552
  public get halfDrawWidth(): number {
UNCOV
553
    return this.screen.halfDrawWidth;
×
554
  }
555

556
  /**
557
   * Returns the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
558
   */
559
  public get drawHeight(): number {
UNCOV
560
    return this.screen.drawHeight;
×
561
  }
562

563
  /**
564
   * Returns half the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
565
   */
566
  public get halfDrawHeight(): number {
UNCOV
567
    return this.screen.halfDrawHeight;
×
568
  }
569

570
  /**
571
   * Returns whether excalibur detects the current screen to be HiDPI
572
   */
573
  public get isHiDpi(): boolean {
UNCOV
574
    return this.screen.isHiDpi;
×
575
  }
576

577
  /**
578
   * Access engine input like pointer, keyboard, or gamepad
579
   */
580
  public input: InputHost;
581

582
  /**
583
   * Map multiple input sources to specific game actions actions
584
   */
585
  public inputMapper: InputMapper;
586

UNCOV
587
  private _inputEnabled: boolean = true;
×
588

589
  /**
590
   * Access Excalibur debugging functionality.
591
   *
592
   * Useful when you want to debug different aspects of built in engine features like
593
   *   * Transform
594
   *   * Graphics
595
   *   * Colliders
596
   */
597
  public debug: DebugConfig;
598

599
  /**
600
   * Access {@apilink stats} that holds frame statistics.
601
   */
602
  public get stats(): DebugStats {
UNCOV
603
    return this.debug.stats;
×
604
  }
605

606
  /**
607
   * The current {@apilink Scene} being drawn and updated on screen
608
   */
609
  public get currentScene(): Scene {
UNCOV
610
    return this.director.currentScene;
×
611
  }
612

613
  /**
614
   * The current {@apilink Scene} being drawn and updated on screen
615
   */
616
  public get currentSceneName(): string {
UNCOV
617
    return this.director.currentSceneName;
×
618
  }
619

620
  /**
621
   * The default {@apilink Scene} of the game, use {@apilink Engine.goToScene} to transition to different scenes.
622
   */
623
  public get rootScene(): Scene {
UNCOV
624
    return this.director.rootScene;
×
625
  }
626

627
  /**
628
   * Contains all the scenes currently registered with Excalibur
629
   */
630
  public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } {
UNCOV
631
    return this.director.scenes;
×
632
  }
633

634
  /**
635
   * Indicates whether the engine is set to fullscreen or not
636
   */
637
  public get isFullscreen(): boolean {
UNCOV
638
    return this.screen.isFullScreen;
×
639
  }
640

641
  /**
642
   * Indicates the current {@apilink DisplayMode} of the engine.
643
   */
644
  public get displayMode(): DisplayMode {
645
    return this.screen.displayMode;
×
646
  }
647

UNCOV
648
  private _suppressPlayButton: boolean = false;
×
649
  /**
650
   * Returns the calculated pixel ration for use in rendering
651
   */
652
  public get pixelRatio(): number {
UNCOV
653
    return this.screen.pixelRatio;
×
654
  }
655

656
  /**
657
   * Indicates whether audio should be paused when the game is no longer visible.
658
   */
UNCOV
659
  public pauseAudioWhenHidden: boolean = true;
×
660

661
  /**
662
   * Indicates whether the engine should draw with debug information
663
   */
UNCOV
664
  private _isDebug: boolean = false;
×
665
  public get isDebug(): boolean {
UNCOV
666
    return this._isDebug;
×
667
  }
668

669
  /**
670
   * Sets the background color for the engine.
671
   */
672
  public backgroundColor: Color;
673

674
  /**
675
   * Sets the Transparency for the engine.
676
   */
UNCOV
677
  public enableCanvasTransparency: boolean = true;
×
678

679
  /**
680
   * Hints the graphics context to truncate fractional world space coordinates
681
   */
682
  public get snapToPixel(): boolean {
UNCOV
683
    return this.graphicsContext.snapToPixel;
×
684
  }
685

686
  public set snapToPixel(shouldSnapToPixel: boolean) {
UNCOV
687
    this.graphicsContext.snapToPixel = shouldSnapToPixel;
×
688
  }
689

690
  /**
691
   * The action to take when a fatal exception is thrown
692
   */
UNCOV
693
  public onFatalException = (e: any) => {
×
UNCOV
694
    Logger.getInstance().fatal(e, e.stack);
×
695
  };
696

697
  /**
698
   * The mouse wheel scroll prevention mode
699
   */
700
  public pageScrollPreventionMode: ScrollPreventionMode;
701

702
  private _logger: Logger;
703

UNCOV
704
  private _toaster: Toaster = new Toaster();
×
705

706
  // this determines whether excalibur is compatible with your browser
707
  private _compatible: boolean;
708

UNCOV
709
  private _timescale: number = 1.0;
×
710

711
  // loading
712
  private _loader: DefaultLoader;
713

UNCOV
714
  private _isInitialized: boolean = false;
×
715

716
  public emit<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, event: EngineEvents[TEventName]): void;
717
  public emit(eventName: string, event?: any): void;
718
  public emit<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, event?: any): void {
UNCOV
719
    this.events.emit(eventName, event);
×
720
  }
721

722
  public on<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
723
  public on(eventName: string, handler: Handler<unknown>): Subscription;
724
  public on<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
UNCOV
725
    return this.events.on(eventName, handler);
×
726
  }
727

728
  public once<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
729
  public once(eventName: string, handler: Handler<unknown>): Subscription;
730
  public once<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
UNCOV
731
    return this.events.once(eventName, handler);
×
732
  }
733

734
  public off<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): void;
735
  public off(eventName: string, handler: Handler<unknown>): void;
736
  public off(eventName: string): void;
737
  public off<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
738
    this.events.off(eventName, handler);
×
739
  }
740

741
  /**
742
   * Default {@apilink EngineOptions}
743
   */
744
  private static _DEFAULT_ENGINE_OPTIONS: EngineOptions = {
1✔
745
    width: 0,
746
    height: 0,
747
    enableCanvasTransparency: true,
748
    useDrawSorting: true,
749
    configurePerformanceCanvas2DFallback: {
750
      allow: false,
751
      showPlayerMessage: false,
752
      threshold: { fps: 20, numberOfFrames: 100 }
753
    },
754
    canvasElementId: '',
755
    canvasElement: undefined,
756
    enableCanvasContextMenu: false,
757
    snapToPixel: false,
758
    antialiasing: true,
759
    pixelArt: false,
760
    garbageCollection: true,
761
    powerPreference: 'high-performance',
762
    pointerScope: PointerScope.Canvas,
763
    suppressConsoleBootMessage: null,
764
    suppressMinimumBrowserFeatureDetection: null,
765
    suppressHiDPIScaling: null,
766
    suppressPlayButton: null,
767
    grabWindowFocus: true,
768
    scrollPreventionMode: ScrollPreventionMode.Canvas,
769
    backgroundColor: Color.fromHex('#2185d0') // Excalibur blue
770
  };
771

UNCOV
772
  private _originalOptions: EngineOptions = {};
×
773
  public readonly _originalDisplayMode: DisplayMode;
774

775
  /**
776
   * Creates a new game using the given {@apilink EngineOptions}. By default, if no options are provided,
777
   * the game will be rendered full screen (taking up all available browser window space).
778
   * You can customize the game rendering through {@apilink EngineOptions}.
779
   *
780
   * Example:
781
   *
782
   * ```js
783
   * var game = new ex.Engine({
784
   *   width: 0, // the width of the canvas
785
   *   height: 0, // the height of the canvas
786
   *   enableCanvasTransparency: true, // the transparencySection of the canvas
787
   *   canvasElementId: '', // the DOM canvas element ID, if you are providing your own
788
   *   displayMode: ex.DisplayMode.FullScreen, // the display mode
789
   *   pointerScope: ex.PointerScope.Document, // the scope of capturing pointer (mouse/touch) events
790
   *   backgroundColor: ex.Color.fromHex('#2185d0') // background color of the engine
791
   * });
792
   *
793
   * // call game.start, which is a Promise
794
   * game.start().then(function () {
795
   *   // ready, set, go!
796
   * });
797
   * ```
798
   */
799
  constructor(options?: EngineOptions<TKnownScenes>) {
UNCOV
800
    options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options };
×
UNCOV
801
    this._originalOptions = options;
×
802

UNCOV
803
    Flags.freeze();
×
804

805
    // Initialize browser events facade
UNCOV
806
    this.browser = new BrowserEvents(window, document);
×
807

808
    // Check compatibility
UNCOV
809
    const detector = new Detector();
×
UNCOV
810
    if (!options.suppressMinimumBrowserFeatureDetection && !(this._compatible = detector.test())) {
×
811
      const message = document.createElement('div');
×
812
      message.innerText = 'Sorry, your browser does not support all the features needed for Excalibur';
×
813
      document.body.appendChild(message);
×
814

815
      detector.failedTests.forEach(function (test) {
×
816
        const testMessage = document.createElement('div');
×
817
        testMessage.innerText = 'Browser feature missing ' + test;
×
818
        document.body.appendChild(testMessage);
×
819
      });
820

821
      if (options.canvasElementId) {
×
822
        const canvas = document.getElementById(options.canvasElementId);
×
823
        if (canvas) {
×
824
          canvas.parentElement.removeChild(canvas);
×
825
        }
826
      }
827

828
      return;
×
829
    } else {
UNCOV
830
      this._compatible = true;
×
831
    }
832

833
    // Use native console API for color fun
834
    // eslint-disable-next-line no-console
UNCOV
835
    if (console.log && !options.suppressConsoleBootMessage) {
×
836
      // eslint-disable-next-line no-console
UNCOV
837
      console.log(
×
838
        `%cPowered by Excalibur.js (v${EX_VERSION})`,
839
        'background: #176BAA; color: white; border-radius: 5px; padding: 15px; font-size: 1.5em; line-height: 80px;'
840
      );
841
      // eslint-disable-next-line no-console
UNCOV
842
      console.log(
×
843
        '\n\
844
      /| ________________\n\
845
O|===|* >________________>\n\
846
      \\|'
847
      );
848
      // eslint-disable-next-line no-console
UNCOV
849
      console.log('Visit', 'http://excaliburjs.com', 'for more information');
×
850
    }
851

852
    // Suppress play button
UNCOV
853
    if (options.suppressPlayButton) {
×
UNCOV
854
      this._suppressPlayButton = true;
×
855
    }
856

UNCOV
857
    this._logger = Logger.getInstance();
×
858

859
    // If debug is enabled, let's log browser features to the console.
UNCOV
860
    if (this._logger.defaultLevel === LogLevel.Debug) {
×
861
      detector.logBrowserFeatures();
×
862
    }
863

UNCOV
864
    this._logger.debug('Building engine...');
×
UNCOV
865
    if (options.garbageCollection === true) {
×
UNCOV
866
      this.garbageCollectorConfig = {
×
867
        ...DefaultGarbageCollectionOptions
868
      };
869
    } else if (options.garbageCollection === false) {
×
870
      this._logger.warn(
×
871
        'WebGL Garbage Collection Disabled!!! If you leak any images over time your game will crash when GPU memory is exhausted'
872
      );
873
      this.garbageCollectorConfig = null;
×
874
    } else {
875
      this.garbageCollectorConfig = {
×
876
        ...DefaultGarbageCollectionOptions,
877
        ...options.garbageCollection
878
      };
879
    }
UNCOV
880
    this._garbageCollector = new GarbageCollector({ getTimestamp: Date.now });
×
881

UNCOV
882
    this.canvasElementId = options.canvasElementId;
×
883

UNCOV
884
    if (options.canvasElementId) {
×
885
      this._logger.debug('Using Canvas element specified: ' + options.canvasElementId);
×
886

887
      //test for existence of element
888
      if (document.getElementById(options.canvasElementId) === null) {
×
889
        throw new Error('Cannot find existing element in the DOM, please ensure element is created prior to engine creation.');
×
890
      }
891

892
      this.canvas = <HTMLCanvasElement>document.getElementById(options.canvasElementId);
×
UNCOV
893
    } else if (options.canvasElement) {
×
894
      this._logger.debug('Using Canvas element specified:', options.canvasElement);
×
895
      this.canvas = options.canvasElement;
×
896
    } else {
UNCOV
897
      this._logger.debug('Using generated canvas element');
×
UNCOV
898
      this.canvas = <HTMLCanvasElement>document.createElement('canvas');
×
899
    }
900

UNCOV
901
    if (this.canvas && !options.enableCanvasContextMenu) {
×
UNCOV
902
      this.canvas.addEventListener('contextmenu', (evt) => {
×
UNCOV
903
        evt.preventDefault();
×
904
      });
905
    }
906

UNCOV
907
    let displayMode = options.displayMode ?? DisplayMode.Fixed;
×
UNCOV
908
    if ((options.width && options.height) || options.viewport) {
×
UNCOV
909
      if (options.displayMode === undefined) {
×
UNCOV
910
        displayMode = DisplayMode.Fixed;
×
911
      }
UNCOV
912
      this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height);
×
UNCOV
913
    } else if (!options.displayMode) {
×
UNCOV
914
      this._logger.debug('Engine viewport is fit');
×
UNCOV
915
      displayMode = DisplayMode.FitScreen;
×
916
    }
917

UNCOV
918
    this.grabWindowFocus = options.grabWindowFocus;
×
UNCOV
919
    this.pointerScope = options.pointerScope;
×
920

UNCOV
921
    this._originalDisplayMode = displayMode;
×
922

923
    let pixelArtSampler: boolean;
924
    let uvPadding: number;
925
    let nativeContextAntialiasing: boolean;
926
    let canvasImageRendering: 'pixelated' | 'auto';
927
    let filtering: ImageFiltering;
928
    let multiSampleAntialiasing: boolean | { samples: number };
UNCOV
929
    if (typeof options.antialiasing === 'object') {
×
930
      ({ pixelArtSampler, nativeContextAntialiasing, multiSampleAntialiasing, filtering, canvasImageRendering } = {
×
931
        ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions),
×
932
        ...options.antialiasing
933
      });
934
    } else {
UNCOV
935
      pixelArtSampler = !!options.pixelArt;
×
UNCOV
936
      nativeContextAntialiasing = false;
×
UNCOV
937
      multiSampleAntialiasing = options.antialiasing;
×
UNCOV
938
      canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated';
×
UNCOV
939
      filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel;
×
940
    }
941

UNCOV
942
    if (nativeContextAntialiasing && multiSampleAntialiasing) {
×
943
      this._logger.warnOnce(
×
944
        `Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` +
945
          ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing`
946
      );
947
    }
948

UNCOV
949
    if (options.pixelArt) {
×
UNCOV
950
      uvPadding = 0.25;
×
951
    }
952

UNCOV
953
    if (!options.antialiasing || filtering === ImageFiltering.Pixel) {
×
UNCOV
954
      uvPadding = 0;
×
955
    }
956

957
    // Override with any user option, if non default to .25 for pixel art, 0.01 for everything else
UNCOV
958
    uvPadding = options.uvPadding ?? uvPadding ?? 0.01;
×
959

960
    // Canvas 2D fallback can be flagged on
UNCOV
961
    let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context');
×
UNCOV
962
    if (!useCanvasGraphicsContext) {
×
963
      // Attempt webgl first
UNCOV
964
      try {
×
UNCOV
965
        this.graphicsContext = new ExcaliburGraphicsContextWebGL({
×
966
          canvasElement: this.canvas,
967
          enableTransparency: this.enableCanvasTransparency,
968
          pixelArtSampler: pixelArtSampler,
969
          antialiasing: nativeContextAntialiasing,
970
          multiSampleAntialiasing: multiSampleAntialiasing,
971
          uvPadding: uvPadding,
972
          powerPreference: options.powerPreference,
973
          backgroundColor: options.backgroundColor,
974
          snapToPixel: options.snapToPixel,
975
          useDrawSorting: options.useDrawSorting,
976
          garbageCollector: this.garbageCollectorConfig
977
            ? {
×
978
                garbageCollector: this._garbageCollector,
979
                collectionInterval: this.garbageCollectorConfig.textureCollectInterval
980
              }
981
            : null,
982
          handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost,
×
983
          handleContextRestored: options.handleContextRestored
984
        });
985
      } catch (e) {
986
        this._logger.warn(
×
987
          `Excalibur could not load webgl for some reason (${(e as Error).message}) and loaded a Canvas 2D fallback. ` +
988
            `Some features of Excalibur will not work in this mode. \n\n` +
989
            'Read more about this issue at https://excaliburjs.com/docs/performance'
990
        );
991
        // fallback to canvas in case of failure
992
        useCanvasGraphicsContext = true;
×
993
      }
994
    }
995

UNCOV
996
    if (useCanvasGraphicsContext) {
×
UNCOV
997
      this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
×
998
        canvasElement: this.canvas,
999
        enableTransparency: this.enableCanvasTransparency,
1000
        antialiasing: nativeContextAntialiasing,
1001
        backgroundColor: options.backgroundColor,
1002
        snapToPixel: options.snapToPixel,
1003
        useDrawSorting: options.useDrawSorting
1004
      });
1005
    }
1006

UNCOV
1007
    this.screen = new Screen({
×
1008
      canvas: this.canvas,
1009
      context: this.graphicsContext,
1010
      antialiasing: nativeContextAntialiasing,
1011
      canvasImageRendering: canvasImageRendering,
1012
      browser: this.browser,
1013
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
×
1014
      resolution: options.resolution,
1015
      displayMode,
1016
      pixelRatio: options.suppressHiDPIScaling ? 1 : options.pixelRatio ?? null
×
1017
    });
1018

1019
    // TODO REMOVE STATIC!!!
1020
    // Set default filtering based on antialiasing
UNCOV
1021
    TextureLoader.filtering = filtering;
×
1022

UNCOV
1023
    if (options.backgroundColor) {
×
UNCOV
1024
      this.backgroundColor = options.backgroundColor.clone();
×
1025
    }
1026

UNCOV
1027
    this.maxFps = options.maxFps ?? this.maxFps;
×
1028

UNCOV
1029
    this.fixedUpdateTimestep = options.fixedUpdateTimestep ?? this.fixedUpdateTimestep;
×
UNCOV
1030
    this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps;
×
UNCOV
1031
    this.fixedUpdateTimestep = this.fixedUpdateTimestep || 1000 / this.fixedUpdateFps;
×
1032

UNCOV
1033
    this.clock = new StandardClock({
×
1034
      maxFps: this.maxFps,
1035
      tick: this._mainloop.bind(this),
UNCOV
1036
      onFatalException: (e) => this.onFatalException(e)
×
1037
    });
1038

UNCOV
1039
    this.enableCanvasTransparency = options.enableCanvasTransparency;
×
1040

UNCOV
1041
    if (typeof options.physics === 'boolean') {
×
1042
      this.physics = {
×
1043
        ...getDefaultPhysicsConfig(),
1044
        enabled: options.physics
1045
      };
1046
    } else {
UNCOV
1047
      this.physics = {
×
1048
        ...getDefaultPhysicsConfig()
1049
      };
UNCOV
1050
      mergeDeep(this.physics, options.physics);
×
1051
    }
1052

UNCOV
1053
    this.debug = new DebugConfig(this);
×
1054

UNCOV
1055
    this.director = new Director(this, options.scenes);
×
UNCOV
1056
    this.director.events.pipe(this.events);
×
1057

UNCOV
1058
    this._initialize(options);
×
1059

UNCOV
1060
    (window as any).___EXCALIBUR_DEVTOOL = this;
×
UNCOV
1061
    Engine.InstanceCount++;
×
1062
  }
1063

UNCOV
1064
  private _handleWebGLContextLost = (e: Event) => {
×
UNCOV
1065
    e.preventDefault();
×
UNCOV
1066
    this.clock.stop();
×
UNCOV
1067
    this._logger.fatalOnce('WebGL Graphics Lost', e);
×
UNCOV
1068
    const container = document.createElement('div');
×
UNCOV
1069
    container.id = 'ex-webgl-graphics-context-lost';
×
UNCOV
1070
    container.style.position = 'absolute';
×
UNCOV
1071
    container.style.zIndex = '99';
×
UNCOV
1072
    container.style.left = '50%';
×
UNCOV
1073
    container.style.top = '50%';
×
UNCOV
1074
    container.style.display = 'flex';
×
UNCOV
1075
    container.style.flexDirection = 'column';
×
UNCOV
1076
    container.style.transform = 'translate(-50%, -50%)';
×
UNCOV
1077
    container.style.backgroundColor = 'white';
×
UNCOV
1078
    container.style.padding = '10px';
×
UNCOV
1079
    container.style.borderStyle = 'solid 1px';
×
1080

UNCOV
1081
    const div = document.createElement('div');
×
UNCOV
1082
    div.innerHTML = `
×
1083
      <h1>There was an issue rendering, please refresh the page.</h1>
1084
      <div>
1085
        <p>WebGL Graphics Context Lost</p>
1086

1087
        <button id="ex-webgl-graphics-reload">Refresh Page</button>
1088

1089
        <p>There are a few reasons this might happen:</p>
1090
        <ul>
1091
          <li>Two or more pages are placing a high demand on the GPU</li>
1092
          <li>Another page or operation has stalled the GPU and the browser has decided to reset the GPU</li>
1093
          <li>The computer has multiple GPUs and the user has switched between them</li>
1094
          <li>Graphics driver has crashed or restarted</li>
1095
          <li>Graphics driver was updated</li>
1096
        </ul>
1097
      </div>
1098
    `;
UNCOV
1099
    container.appendChild(div);
×
UNCOV
1100
    if (this.canvas?.parentElement) {
×
UNCOV
1101
      this.canvas.parentElement.appendChild(container);
×
UNCOV
1102
      const button = div.querySelector('#ex-webgl-graphics-reload');
×
UNCOV
1103
      button?.addEventListener('click', () => location.reload());
×
1104
    }
1105
  };
1106

UNCOV
1107
  private _performanceThresholdTriggered = false;
×
UNCOV
1108
  private _fpsSamples: number[] = [];
×
1109
  private _monitorPerformanceThresholdAndTriggerFallback() {
UNCOV
1110
    const { allow } = this._originalOptions.configurePerformanceCanvas2DFallback;
×
UNCOV
1111
    let { threshold, showPlayerMessage } = this._originalOptions.configurePerformanceCanvas2DFallback;
×
UNCOV
1112
    if (threshold === undefined) {
×
UNCOV
1113
      threshold = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.threshold;
×
1114
    }
UNCOV
1115
    if (showPlayerMessage === undefined) {
×
UNCOV
1116
      showPlayerMessage = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.showPlayerMessage;
×
1117
    }
UNCOV
1118
    if (!Flags.isEnabled('use-canvas-context') && allow && this.ready && !this._performanceThresholdTriggered) {
×
1119
      // Calculate Average fps for last X number of frames after start
UNCOV
1120
      if (this._fpsSamples.length === threshold.numberOfFrames) {
×
1121
        this._fpsSamples.splice(0, 1);
×
1122
      }
UNCOV
1123
      this._fpsSamples.push(this.clock.fpsSampler.fps);
×
UNCOV
1124
      let total = 0;
×
UNCOV
1125
      for (let i = 0; i < this._fpsSamples.length; i++) {
×
UNCOV
1126
        total += this._fpsSamples[i];
×
1127
      }
UNCOV
1128
      const average = total / this._fpsSamples.length;
×
1129

UNCOV
1130
      if (this._fpsSamples.length === threshold.numberOfFrames) {
×
UNCOV
1131
        if (average <= threshold.fps) {
×
UNCOV
1132
          this._performanceThresholdTriggered = true;
×
UNCOV
1133
          this._logger.warn(
×
1134
            `Switching to browser 2D Canvas fallback due to performance. Some features of Excalibur will not work in this mode.\n` +
1135
              "this might mean your browser doesn't have webgl enabled or hardware acceleration is unavailable.\n\n" +
1136
              'If in Chrome:\n' +
1137
              '  * Visit Settings > Advanced > System, and ensure "Use Hardware Acceleration" is checked.\n' +
1138
              '  * Visit chrome://flags/#ignore-gpu-blocklist and ensure "Override software rendering list" is "enabled"\n' +
1139
              'If in Firefox, visit about:config\n' +
1140
              '  * Ensure webgl.disabled = false\n' +
1141
              '  * Ensure webgl.force-enabled = true\n' +
1142
              '  * Ensure layers.acceleration.force-enabled = true\n\n' +
1143
              'Read more about this issue at https://excaliburjs.com/docs/performance'
1144
          );
1145

UNCOV
1146
          if (showPlayerMessage) {
×
1147
            this._toaster.toast(
×
1148
              'Excalibur is encountering performance issues. ' +
1149
                "It's possible that your browser doesn't have hardware acceleration enabled. " +
1150
                'Visit [LINK] for more information and potential solutions.',
1151
              'https://excaliburjs.com/docs/performance'
1152
            );
1153
          }
UNCOV
1154
          this.useCanvas2DFallback();
×
UNCOV
1155
          this.emit('fallbackgraphicscontext', this.graphicsContext);
×
1156
        }
1157
      }
1158
    }
1159
  }
1160

1161
  /**
1162
   * Switches the engine's graphics context to the 2D Canvas.
1163
   * @warning Some features of Excalibur will not work in this mode.
1164
   */
1165
  public useCanvas2DFallback() {
1166
    // Swap out the canvas
UNCOV
1167
    const newCanvas = this.canvas.cloneNode(false) as HTMLCanvasElement;
×
UNCOV
1168
    this.canvas.parentNode.replaceChild(newCanvas, this.canvas);
×
UNCOV
1169
    this.canvas = newCanvas;
×
1170

UNCOV
1171
    const options = { ...this._originalOptions, antialiasing: this.screen.antialiasing };
×
UNCOV
1172
    const displayMode = this._originalDisplayMode;
×
1173

1174
    // New graphics context
UNCOV
1175
    this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
×
1176
      canvasElement: this.canvas,
1177
      enableTransparency: this.enableCanvasTransparency,
1178
      antialiasing: options.antialiasing,
1179
      backgroundColor: options.backgroundColor,
1180
      snapToPixel: options.snapToPixel,
1181
      useDrawSorting: options.useDrawSorting
1182
    });
1183

1184
    // Reset screen
UNCOV
1185
    if (this.screen) {
×
UNCOV
1186
      this.screen.dispose();
×
1187
    }
1188

UNCOV
1189
    this.screen = new Screen({
×
1190
      canvas: this.canvas,
1191
      context: this.graphicsContext,
1192
      antialiasing: options.antialiasing ?? true,
×
1193
      browser: this.browser,
1194
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
×
1195
      resolution: options.resolution,
1196
      displayMode,
1197
      pixelRatio: options.suppressHiDPIScaling ? 1 : options.pixelRatio ?? null
×
1198
    });
UNCOV
1199
    this.screen.setCurrentCamera(this.currentScene.camera);
×
1200

1201
    // Reset pointers
UNCOV
1202
    this.input.pointers.detach();
×
UNCOV
1203
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
×
UNCOV
1204
    this.input.pointers = this.input.pointers.recreate(pointerTarget, this);
×
UNCOV
1205
    this.input.pointers.init();
×
1206
  }
1207

UNCOV
1208
  private _disposed = false;
×
1209
  /**
1210
   * Attempts to completely clean up excalibur resources, including removing the canvas from the dom.
1211
   *
1212
   * To start again you will need to new up an Engine.
1213
   */
1214
  public dispose() {
UNCOV
1215
    if (!this._disposed) {
×
UNCOV
1216
      this._disposed = true;
×
UNCOV
1217
      this.stop();
×
UNCOV
1218
      this._garbageCollector.forceCollectAll();
×
UNCOV
1219
      this.input.toggleEnabled(false);
×
UNCOV
1220
      this.canvas.parentNode.removeChild(this.canvas);
×
UNCOV
1221
      this.canvas = null;
×
UNCOV
1222
      this.screen.dispose();
×
UNCOV
1223
      this.graphicsContext.dispose();
×
UNCOV
1224
      this.graphicsContext = null;
×
UNCOV
1225
      Engine.InstanceCount--;
×
1226
    }
1227
  }
1228

1229
  public isDisposed() {
UNCOV
1230
    return this._disposed;
×
1231
  }
1232

1233
  /**
1234
   * Returns a BoundingBox of the top left corner of the screen
1235
   * and the bottom right corner of the screen.
1236
   */
1237
  public getWorldBounds() {
UNCOV
1238
    return this.screen.getWorldBounds();
×
1239
  }
1240

1241
  /**
1242
   * Gets the current engine timescale factor (default is 1.0 which is 1:1 time)
1243
   */
1244
  public get timescale() {
UNCOV
1245
    return this._timescale;
×
1246
  }
1247

1248
  /**
1249
   * Sets the current engine timescale factor. Useful for creating slow-motion effects or fast-forward effects
1250
   * when using time-based movement.
1251
   */
1252
  public set timescale(value: number) {
UNCOV
1253
    if (value < 0) {
×
1254
      Logger.getInstance().warnOnce('engine.timescale to a value less than 0 are ignored');
×
1255
      return;
×
1256
    }
1257

UNCOV
1258
    this._timescale = value;
×
1259
  }
1260

1261
  /**
1262
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1263
   * @param timer  The timer to add to the {@apilink currentScene}.
1264
   */
1265
  public addTimer(timer: Timer): Timer {
1266
    return this.currentScene.addTimer(timer);
×
1267
  }
1268

1269
  /**
1270
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1271
   * @param timer  The timer to remove to the {@apilink currentScene}.
1272
   */
1273
  public removeTimer(timer: Timer): Timer {
1274
    return this.currentScene.removeTimer(timer);
×
1275
  }
1276

1277
  /**
1278
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1279
   * would levels or menus.
1280
   * @param key  The name of the scene, must be unique
1281
   * @param scene The scene to add to the engine
1282
   */
1283
  public addScene<TScene extends string>(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine<TKnownScenes | TScene> {
UNCOV
1284
    this.director.add(key, scene);
×
UNCOV
1285
    return this as Engine<TKnownScenes | TScene>;
×
1286
  }
1287

1288
  /**
1289
   * Removes a {@apilink Scene} instance from the engine
1290
   * @param scene  The scene to remove
1291
   */
1292
  public removeScene(scene: Scene | SceneConstructor): void;
1293
  /**
1294
   * Removes a scene from the engine by key
1295
   * @param key  The scene key to remove
1296
   */
1297
  public removeScene(key: string): void;
1298
  /**
1299
   * @internal
1300
   */
1301
  public removeScene(entity: any): void {
UNCOV
1302
    this.director.remove(entity);
×
1303
  }
1304

1305
  /**
1306
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1307
   * would levels or menus.
1308
   * @param sceneKey  The key of the scene, must be unique
1309
   * @param scene     The scene to add to the engine
1310
   */
1311
  public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void;
1312
  /**
1313
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1314
   * @param timer  The timer to add to the {@apilink currentScene}.
1315
   */
1316
  public add(timer: Timer): void;
1317
  /**
1318
   * Adds a {@apilink TileMap} to the {@apilink currentScene}, once this is done the TileMap
1319
   * will be drawn and updated.
1320
   */
1321
  public add(tileMap: TileMap): void;
1322
  /**
1323
   * Adds an actor to the {@apilink currentScene} of the game. This is synonymous
1324
   * to calling `engine.currentScene.add(actor)`.
1325
   *
1326
   * Actors can only be drawn if they are a member of a scene, and only
1327
   * the {@apilink currentScene} may be drawn or updated.
1328
   * @param actor  The actor to add to the {@apilink currentScene}
1329
   */
1330
  public add(actor: Actor): void;
1331

1332
  public add(entity: Entity): void;
1333

1334
  /**
1335
   * Adds a {@apilink ScreenElement} to the {@apilink currentScene} of the game,
1336
   * ScreenElements do not participate in collisions, instead the
1337
   * remain in the same place on the screen.
1338
   * @param screenElement  The ScreenElement to add to the {@apilink currentScene}
1339
   */
1340
  public add(screenElement: ScreenElement): void;
1341
  public add(entity: any): void {
UNCOV
1342
    if (arguments.length === 2) {
×
UNCOV
1343
      this.director.add(<string>arguments[0], <Scene | SceneConstructor | SceneWithOptions>arguments[1]);
×
UNCOV
1344
      return;
×
1345
    }
UNCOV
1346
    const maybeDeferred = this.director.getDeferredScene();
×
UNCOV
1347
    if (maybeDeferred instanceof Scene) {
×
1348
      maybeDeferred.add(entity);
×
1349
    } else {
UNCOV
1350
      this.currentScene.add(entity);
×
1351
    }
1352
  }
1353

1354
  /**
1355
   * Removes a scene instance from the engine
1356
   * @param scene  The scene to remove
1357
   */
1358
  public remove(scene: Scene | SceneConstructor): void;
1359
  /**
1360
   * Removes a scene from the engine by key
1361
   * @param sceneKey  The scene to remove
1362
   */
1363
  public remove(sceneKey: string): void;
1364
  /**
1365
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1366
   * @param timer  The timer to remove to the {@apilink currentScene}.
1367
   */
1368
  public remove(timer: Timer): void;
1369
  /**
1370
   * Removes a {@apilink TileMap} from the {@apilink currentScene}, it will no longer be drawn or updated.
1371
   */
1372
  public remove(tileMap: TileMap): void;
1373
  /**
1374
   * Removes an actor from the {@apilink currentScene} of the game. This is synonymous
1375
   * to calling `engine.currentScene.removeChild(actor)`.
1376
   * Actors that are removed from a scene will no longer be drawn or updated.
1377
   * @param actor  The actor to remove from the {@apilink currentScene}.
1378
   */
1379
  public remove(actor: Actor): void;
1380
  /**
1381
   * Removes a {@apilink ScreenElement} to the scene, it will no longer be drawn or updated
1382
   * @param screenElement  The ScreenElement to remove from the {@apilink currentScene}
1383
   */
1384
  public remove(screenElement: ScreenElement): void;
1385
  public remove(entity: any): void {
UNCOV
1386
    if (entity instanceof Entity) {
×
UNCOV
1387
      this.currentScene.remove(entity);
×
1388
    }
1389

UNCOV
1390
    if (entity instanceof Scene || isSceneConstructor(entity)) {
×
UNCOV
1391
      this.removeScene(entity);
×
1392
    }
1393

UNCOV
1394
    if (typeof entity === 'string') {
×
UNCOV
1395
      this.removeScene(entity);
×
1396
    }
1397
  }
1398

1399
  /**
1400
   * Changes the current scene with optionally supplied:
1401
   * * Activation data
1402
   * * Transitions
1403
   * * Loaders
1404
   *
1405
   * Example:
1406
   * ```typescript
1407
   * game.goToScene('myScene', {
1408
   *   sceneActivationData: {any: 'thing at all'},
1409
   *   destinationIn: new FadeInOut({duration: 1000, direction: 'in'}),
1410
   *   sourceOut: new FadeInOut({duration: 1000, direction: 'out'}),
1411
   *   loader: MyLoader
1412
   * });
1413
   * ```
1414
   *
1415
   * Scenes are defined in the Engine constructor
1416
   * ```typescript
1417
   * const engine = new ex.Engine({
1418
      scenes: {...}
1419
    });
1420
   * ```
1421
   * Or by adding dynamically
1422
   *
1423
   * ```typescript
1424
   * engine.addScene('myScene', new ex.Scene());
1425
   * ```
1426
   * @param destinationScene
1427
   * @param options
1428
   */
1429
  public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> {
UNCOV
1430
    await this.scope(async () => {
×
UNCOV
1431
      await this.director.goToScene(destinationScene, options);
×
1432
    });
1433
  }
1434

1435
  /**
1436
   * Transforms the current x, y from screen coordinates to world coordinates
1437
   * @param point  Screen coordinate to convert
1438
   */
1439
  public screenToWorldCoordinates(point: Vector): Vector {
UNCOV
1440
    return this.screen.screenToWorldCoordinates(point);
×
1441
  }
1442

1443
  /**
1444
   * Transforms a world coordinate, to a screen coordinate
1445
   * @param point  World coordinate to convert
1446
   */
1447
  public worldToScreenCoordinates(point: Vector): Vector {
UNCOV
1448
    return this.screen.worldToScreenCoordinates(point);
×
1449
  }
1450

1451
  /**
1452
   * Initializes the internal canvas, rendering context, display mode, and native event listeners
1453
   */
1454
  private _initialize(options?: EngineOptions) {
UNCOV
1455
    this.pageScrollPreventionMode = options.scrollPreventionMode;
×
1456

1457
    // initialize inputs
UNCOV
1458
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
×
UNCOV
1459
    const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true;
×
UNCOV
1460
    this.input = new InputHost({
×
1461
      pointerTarget,
1462
      grabWindowFocus,
1463
      engine: this
1464
    });
UNCOV
1465
    this.inputMapper = this.input.inputMapper;
×
1466

1467
    // Issue #385 make use of the visibility api
1468
    // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API
1469

UNCOV
1470
    this.browser.document.on('visibilitychange', () => {
×
UNCOV
1471
      if (document.visibilityState === 'hidden') {
×
UNCOV
1472
        this.events.emit('hidden', new HiddenEvent(this));
×
UNCOV
1473
        this._logger.debug('Window hidden');
×
UNCOV
1474
      } else if (document.visibilityState === 'visible') {
×
UNCOV
1475
        this.events.emit('visible', new VisibleEvent(this));
×
UNCOV
1476
        this._logger.debug('Window visible');
×
1477
      }
1478
    });
1479

UNCOV
1480
    if (!this.canvasElementId && !options.canvasElement) {
×
UNCOV
1481
      document.body.appendChild(this.canvas);
×
1482
    }
1483
  }
1484

1485
  public toggleInputEnabled(enabled: boolean) {
1486
    this._inputEnabled = enabled;
×
1487
    this.input.toggleEnabled(this._inputEnabled);
×
1488
  }
1489

1490
  public onInitialize(engine: Engine) {
1491
    // Override me
1492
  }
1493

1494
  /**
1495
   * Gets whether the actor is Initialized
1496
   */
1497
  public get isInitialized(): boolean {
UNCOV
1498
    return this._isInitialized;
×
1499
  }
1500

1501
  private async _overrideInitialize(engine: Engine) {
UNCOV
1502
    if (!this.isInitialized) {
×
UNCOV
1503
      await this.director.onInitialize();
×
UNCOV
1504
      await this.onInitialize(engine);
×
UNCOV
1505
      this.events.emit('initialize', new InitializeEvent(engine, this));
×
UNCOV
1506
      this._isInitialized = true;
×
1507
    }
1508
  }
1509

1510
  /**
1511
   * Updates the entire state of the game
1512
   * @param elapsed  Number of milliseconds elapsed since the last update.
1513
   */
1514
  private _update(elapsed: number) {
UNCOV
1515
    if (this._isLoading) {
×
1516
      // suspend updates until loading is finished
UNCOV
1517
      this._loader?.onUpdate(this, elapsed);
×
1518
      // Update input listeners
UNCOV
1519
      this.input.update();
×
UNCOV
1520
      return;
×
1521
    }
1522

1523
    // Publish preupdate events
UNCOV
1524
    this.clock.__runScheduledCbs('preupdate');
×
UNCOV
1525
    this._preupdate(elapsed);
×
1526

1527
    // process engine level events
UNCOV
1528
    this.currentScene.update(this, elapsed);
×
1529

1530
    // Update graphics postprocessors
UNCOV
1531
    this.graphicsContext.updatePostProcessors(elapsed);
×
1532

1533
    // Publish update event
UNCOV
1534
    this.clock.__runScheduledCbs('postupdate');
×
UNCOV
1535
    this._postupdate(elapsed);
×
1536

1537
    // Update input listeners
UNCOV
1538
    this.input.update();
×
1539
  }
1540

1541
  /**
1542
   * @internal
1543
   */
1544
  public _preupdate(elapsed: number) {
UNCOV
1545
    this.emit('preupdate', new PreUpdateEvent(this, elapsed, this));
×
UNCOV
1546
    this.onPreUpdate(this, elapsed);
×
1547
  }
1548

1549
  /**
1550
   * Safe to override method
1551
   * @param engine The reference to the current game engine
1552
   * @param elapsed  The time elapsed since the last update in milliseconds
1553
   */
1554
  public onPreUpdate(engine: Engine, elapsed: number) {
1555
    // Override me
1556
  }
1557

1558
  /**
1559
   * @internal
1560
   */
1561
  public _postupdate(elapsed: number) {
UNCOV
1562
    this.emit('postupdate', new PostUpdateEvent(this, elapsed, this));
×
UNCOV
1563
    this.onPostUpdate(this, elapsed);
×
1564
  }
1565

1566
  /**
1567
   * Safe to override method
1568
   * @param engine The reference to the current game engine
1569
   * @param elapsed  The time elapsed since the last update in milliseconds
1570
   */
1571
  public onPostUpdate(engine: Engine, elapsed: number) {
1572
    // Override me
1573
  }
1574

1575
  /**
1576
   * Draws the entire game
1577
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1578
   */
1579
  private _draw(elapsed: number) {
1580
    // Use scene background color if present, fallback to engine
UNCOV
1581
    this.graphicsContext.backgroundColor = this.currentScene.backgroundColor ?? this.backgroundColor;
×
UNCOV
1582
    this.graphicsContext.beginDrawLifecycle();
×
UNCOV
1583
    this.graphicsContext.clear();
×
UNCOV
1584
    this.clock.__runScheduledCbs('predraw');
×
UNCOV
1585
    this._predraw(this.graphicsContext, elapsed);
×
1586

1587
    // Drawing nothing else while loading
UNCOV
1588
    if (this._isLoading) {
×
UNCOV
1589
      if (!this._hideLoader) {
×
UNCOV
1590
        this._loader?.canvas.draw(this.graphicsContext, 0, 0);
×
UNCOV
1591
        this.clock.__runScheduledCbs('postdraw');
×
UNCOV
1592
        this.graphicsContext.flush();
×
UNCOV
1593
        this.graphicsContext.endDrawLifecycle();
×
1594
      }
UNCOV
1595
      return;
×
1596
    }
1597

UNCOV
1598
    this.currentScene.draw(this.graphicsContext, elapsed);
×
1599

UNCOV
1600
    this.clock.__runScheduledCbs('postdraw');
×
UNCOV
1601
    this._postdraw(this.graphicsContext, elapsed);
×
1602

1603
    // Flush any pending drawings
UNCOV
1604
    this.graphicsContext.flush();
×
UNCOV
1605
    this.graphicsContext.endDrawLifecycle();
×
1606

UNCOV
1607
    this._checkForScreenShots();
×
1608
  }
1609

1610
  /**
1611
   * @internal
1612
   */
1613
  public _predraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
UNCOV
1614
    this.emit('predraw', new PreDrawEvent(ctx, elapsed, this));
×
UNCOV
1615
    this.onPreDraw(ctx, elapsed);
×
1616
  }
1617

1618
  /**
1619
   * Safe to override method to hook into pre draw
1620
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1621
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1622
   */
1623
  public onPreDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1624
    // Override me
1625
  }
1626

1627
  /**
1628
   * @internal
1629
   */
1630
  public _postdraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
UNCOV
1631
    this.emit('postdraw', new PostDrawEvent(ctx, elapsed, this));
×
UNCOV
1632
    this.onPostDraw(ctx, elapsed);
×
1633
  }
1634

1635
  /**
1636
   * Safe to override method to hook into pre draw
1637
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1638
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1639
   */
1640
  public onPostDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1641
    // Override me
1642
  }
1643

1644
  /**
1645
   * Enable or disable Excalibur debugging functionality.
1646
   * @param toggle a value that debug drawing will be changed to
1647
   */
1648
  public showDebug(toggle: boolean): void {
UNCOV
1649
    this._isDebug = toggle;
×
1650
  }
1651

1652
  /**
1653
   * Toggle Excalibur debugging functionality.
1654
   */
1655
  public toggleDebug(): boolean {
UNCOV
1656
    this._isDebug = !this._isDebug;
×
UNCOV
1657
    return this._isDebug;
×
1658
  }
1659

1660
  /**
1661
   * Returns true when loading is totally complete and the player has clicked start
1662
   */
1663
  public get loadingComplete() {
UNCOV
1664
    return !this._isLoading;
×
1665
  }
1666

UNCOV
1667
  private _isLoading = false;
×
UNCOV
1668
  private _hideLoader = false;
×
UNCOV
1669
  private _isReadyFuture = new Future<void>();
×
1670
  public get ready() {
UNCOV
1671
    return this._isReadyFuture.isCompleted;
×
1672
  }
1673
  public isReady(): Promise<void> {
UNCOV
1674
    return this._isReadyFuture.promise;
×
1675
  }
1676

1677
  /**
1678
   * Starts the internal game loop for Excalibur after loading
1679
   * any provided assets.
1680
   * @param loader  Optional {@apilink Loader} to use to load resources. The default loader is {@apilink Loader},
1681
   * override to provide your own custom loader.
1682
   *
1683
   * Note: start() only resolves AFTER the user has clicked the play button
1684
   */
1685
  public async start(loader?: DefaultLoader): Promise<void>;
1686
  /**
1687
   * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions
1688
   * @param startOptions Optional {@apilink StartOptions} to configure the routes for scenes in Excalibur
1689
   *
1690
   * Note: start() only resolves AFTER the user has clicked the play button
1691
   */
1692
  public async start(sceneName: WithRoot<TKnownScenes>, options?: StartOptions): Promise<void>;
1693
  /**
1694
   * Starts the internal game loop after any loader is finished
1695
   * @param loader
1696
   */
1697
  public async start(loader?: DefaultLoader): Promise<void>;
1698
  public async start(sceneNameOrLoader?: WithRoot<TKnownScenes> | DefaultLoader, options?: StartOptions): Promise<void> {
UNCOV
1699
    await this.scope(async () => {
×
UNCOV
1700
      if (!this._compatible) {
×
1701
        throw new Error('Excalibur is incompatible with your browser');
×
1702
      }
UNCOV
1703
      this._isLoading = true;
×
1704
      let loader: DefaultLoader;
UNCOV
1705
      if (sceneNameOrLoader instanceof DefaultLoader) {
×
UNCOV
1706
        loader = sceneNameOrLoader;
×
UNCOV
1707
      } else if (typeof sceneNameOrLoader === 'string') {
×
1708
        this.director.configureStart(sceneNameOrLoader, options);
×
1709
        loader = this.director.mainLoader;
×
1710
      }
1711

1712
      // Start the excalibur clock which drives the mainloop
UNCOV
1713
      this._logger.debug('Starting game clock...');
×
UNCOV
1714
      this.browser.resume();
×
UNCOV
1715
      this.clock.start();
×
UNCOV
1716
      if (this.garbageCollectorConfig) {
×
UNCOV
1717
        this._garbageCollector.start();
×
1718
      }
UNCOV
1719
      this._logger.debug('Game clock started');
×
1720

UNCOV
1721
      await this.load(loader ?? new Loader());
×
1722

1723
      // Initialize before ready
UNCOV
1724
      await this._overrideInitialize(this);
×
1725

UNCOV
1726
      this._isReadyFuture.resolve();
×
UNCOV
1727
      this.emit('start', new GameStartEvent(this));
×
UNCOV
1728
      return this._isReadyFuture.promise;
×
1729
    });
1730
  }
1731

1732
  /**
1733
   * Returns the current frames elapsed milliseconds
1734
   */
UNCOV
1735
  public currentFrameElapsedMs = 0;
×
1736

1737
  /**
1738
   * Returns the current frame lag when in fixed update mode
1739
   */
UNCOV
1740
  public currentFrameLagMs = 0;
×
1741

UNCOV
1742
  private _lagMs = 0;
×
1743
  private _mainloop(elapsed: number) {
UNCOV
1744
    this.scope(() => {
×
UNCOV
1745
      this.emit('preframe', new PreFrameEvent(this, this.stats.prevFrame));
×
UNCOV
1746
      const elapsedMs = elapsed * this.timescale;
×
UNCOV
1747
      this.currentFrameElapsedMs = elapsedMs;
×
1748

1749
      // reset frame stats (reuse existing instances)
UNCOV
1750
      const frameId = this.stats.prevFrame.id + 1;
×
UNCOV
1751
      this.stats.currFrame.reset();
×
UNCOV
1752
      this.stats.currFrame.id = frameId;
×
UNCOV
1753
      this.stats.currFrame.elapsedMs = elapsedMs;
×
UNCOV
1754
      this.stats.currFrame.fps = this.clock.fpsSampler.fps;
×
UNCOV
1755
      GraphicsDiagnostics.clear();
×
1756

UNCOV
1757
      const beforeUpdate = this.clock.now();
×
UNCOV
1758
      const fixedTimestepMs = this.fixedUpdateTimestep;
×
UNCOV
1759
      if (this.fixedUpdateTimestep) {
×
UNCOV
1760
        this._lagMs += elapsedMs;
×
UNCOV
1761
        while (this._lagMs >= fixedTimestepMs) {
×
UNCOV
1762
          this._update(fixedTimestepMs);
×
UNCOV
1763
          this._lagMs -= fixedTimestepMs;
×
1764
        }
1765
      } else {
UNCOV
1766
        this._update(elapsedMs);
×
1767
      }
UNCOV
1768
      const afterUpdate = this.clock.now();
×
UNCOV
1769
      this.currentFrameLagMs = this._lagMs;
×
UNCOV
1770
      this._draw(elapsedMs);
×
UNCOV
1771
      const afterDraw = this.clock.now();
×
1772

UNCOV
1773
      this.stats.currFrame.duration.update = afterUpdate - beforeUpdate;
×
UNCOV
1774
      this.stats.currFrame.duration.draw = afterDraw - afterUpdate;
×
UNCOV
1775
      this.stats.currFrame.graphics.drawnImages = GraphicsDiagnostics.DrawnImagesCount;
×
UNCOV
1776
      this.stats.currFrame.graphics.drawCalls = GraphicsDiagnostics.DrawCallCount;
×
1777

UNCOV
1778
      this.emit('postframe', new PostFrameEvent(this, this.stats.currFrame));
×
UNCOV
1779
      this.stats.prevFrame.reset(this.stats.currFrame);
×
1780

UNCOV
1781
      this._monitorPerformanceThresholdAndTriggerFallback();
×
1782
    });
1783
  }
1784

1785
  /**
1786
   * Stops Excalibur's main loop, useful for pausing the game.
1787
   */
1788
  public stop() {
UNCOV
1789
    if (this.clock.isRunning()) {
×
UNCOV
1790
      this.emit('stop', new GameStopEvent(this));
×
UNCOV
1791
      this.browser.pause();
×
UNCOV
1792
      this.clock.stop();
×
UNCOV
1793
      this._garbageCollector.stop();
×
UNCOV
1794
      this._logger.debug('Game stopped');
×
1795
    }
1796
  }
1797

1798
  /**
1799
   * Returns the Engine's running status, Useful for checking whether engine is running or paused.
1800
   */
1801
  public isRunning() {
UNCOV
1802
    return this.clock.isRunning();
×
1803
  }
1804

UNCOV
1805
  private _screenShotRequests: { preserveHiDPIResolution: boolean; resolve: (image: HTMLImageElement) => void }[] = [];
×
1806
  /**
1807
   * Takes a screen shot of the current viewport and returns it as an
1808
   * HTML Image Element.
1809
   * @param preserveHiDPIResolution in the case of HiDPI return the full scaled backing image, by default false
1810
   */
1811
  public screenshot(preserveHiDPIResolution = false): Promise<HTMLImageElement> {
×
UNCOV
1812
    const screenShotPromise = new Promise<HTMLImageElement>((resolve) => {
×
UNCOV
1813
      this._screenShotRequests.push({ preserveHiDPIResolution, resolve });
×
1814
    });
UNCOV
1815
    return screenShotPromise;
×
1816
  }
1817

1818
  private _checkForScreenShots() {
1819
    // We must grab the draw buffer before we yield to the browser
1820
    // the draw buffer is cleared after compositing
1821
    // the reason for the asynchrony is setting `preserveDrawingBuffer: true`
1822
    // forces the browser to copy buffers which can have a mass perf impact on mobile
UNCOV
1823
    for (const request of this._screenShotRequests) {
×
UNCOV
1824
      const finalWidth = request.preserveHiDPIResolution ? this.canvas.width : this.screen.resolution.width;
×
UNCOV
1825
      const finalHeight = request.preserveHiDPIResolution ? this.canvas.height : this.screen.resolution.height;
×
UNCOV
1826
      const screenshot = document.createElement('canvas');
×
UNCOV
1827
      screenshot.width = finalWidth;
×
UNCOV
1828
      screenshot.height = finalHeight;
×
UNCOV
1829
      const ctx = screenshot.getContext('2d');
×
UNCOV
1830
      ctx.imageSmoothingEnabled = this.screen.antialiasing;
×
UNCOV
1831
      ctx.drawImage(this.canvas, 0, 0, finalWidth, finalHeight);
×
1832

UNCOV
1833
      const result = new Image();
×
UNCOV
1834
      const raw = screenshot.toDataURL('image/png');
×
UNCOV
1835
      result.onload = () => {
×
UNCOV
1836
        request.resolve(result);
×
1837
      };
UNCOV
1838
      result.src = raw;
×
1839
    }
1840
    // Reset state
UNCOV
1841
    this._screenShotRequests.length = 0;
×
1842
  }
1843

1844
  /**
1845
   * Another option available to you to load resources into the game.
1846
   * Immediately after calling this the game will pause and the loading screen
1847
   * will appear.
1848
   * @param loader  Some {@apilink Loadable} such as a {@apilink Loader} collection, {@apilink Sound}, or {@apilink Texture}.
1849
   */
UNCOV
1850
  public async load(loader: DefaultLoader, hideLoader = false): Promise<void> {
×
UNCOV
1851
    await this.scope(async () => {
×
UNCOV
1852
      try {
×
1853
        // early exit if loaded
UNCOV
1854
        if (loader.isLoaded()) {
×
UNCOV
1855
          return;
×
1856
        }
UNCOV
1857
        this._loader = loader;
×
UNCOV
1858
        this._isLoading = true;
×
UNCOV
1859
        this._hideLoader = hideLoader;
×
1860

UNCOV
1861
        if (loader instanceof Loader) {
×
UNCOV
1862
          loader.suppressPlayButton = loader.suppressPlayButton || this._suppressPlayButton;
×
1863
        }
UNCOV
1864
        this._loader.onInitialize(this);
×
1865

UNCOV
1866
        await loader.load();
×
1867
      } catch (e) {
UNCOV
1868
        this._logger.error('Error loading resources, things may not behave properly', e);
×
UNCOV
1869
        await Promise.resolve();
×
1870
      } finally {
UNCOV
1871
        this._isLoading = false;
×
UNCOV
1872
        this._hideLoader = false;
×
UNCOV
1873
        this._loader = null;
×
1874
      }
1875
    });
1876
  }
1877
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc