diff --git a/.eslintrc.json b/.eslintrc.json
index 26ec91ce4..979f20807 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -59,6 +59,7 @@
"no-unused-labels": "error",
"no-var": "error",
"prefer-const": "error",
+ "require-await": "warn",
"radix": "error",
"max-len": ["error", { "code": 140 }],
"semi": ["error", "always"],
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7e84ebd6..c0d8de303 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,49 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added
--
+- Scene Transition & Loader API, this gives you the ability to have first class support for individual scene resource loading and scene transitions.
+ * Add or remove scenes by constructor
+ * Add loaders by constructor
+ * New `ex.DefaultLoader` type that allows for easier custom loader creation
+ * New `ex.Transition` type for building custom transitions
+ * New scene lifecycle to allow scene specific resource loading
+ * `onTransition(direction: "in" | "out") {...}`
+ * `onPreLoad(loader: DefaultLoader) {...}`
+ * New async goto API that allows overriding loaders/transitions between scenes
+ * Scenes now can have `async onInitialize` and `async onActivate`!
+ * New scenes director API that allows upfront definition of scenes/transitions/loaders
+
+ * Example:
+ Defining scenes upfront
+ ```typescript
+ const game = new ex.Engine({
+ scenes: {
+ scene1: {
+ scene: scene1,
+ transitions: {
+ out: new ex.FadeInOut({duration: 1000, direction: 'out', color: ex.Color.Black}),
+ in: new ex.FadeInOut({duration: 1000, direction: 'in'})
+ }
+ },
+ scene2: {
+ scene: scene2,
+ loader: ex.DefaultLoader, // Constructor only option!
+ transitions: {
+ out: new ex.FadeInOut({duration: 1000, direction: 'out'}),
+ in: new ex.FadeInOut({duration: 1000, direction: 'in', color: ex.Color.Black })
+ }
+ },
+ scene3: ex.Scene // Constructor only option!
+ }
+ })
+
+ // Specify the boot loader & first scene transition from loader
+ game.start('scene1',
+ {
+ inTransition: new ex.FadeInOut({duration: 500, direction: 'in', color: ex.Color.ExcaliburBlue})
+ loader: boot,
+ });
+ ```
### Fixed
diff --git a/package-lock.json b/package-lock.json
index fbfdf6358..8b40043e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -63,7 +63,6 @@
"typedoc": "0.25.3",
"typescript": "5.3.3",
"url-loader": "4.1.1",
- "wallaby-webpack": "3.9.16",
"webpack": "5.89.0",
"webpack-cli": "5.1.4"
}
@@ -27264,29 +27263,6 @@
"makeerror": "1.0.12"
}
},
- "node_modules/wallaby-webpack": {
- "version": "3.9.16",
- "resolved": "https://registry.npmjs.org/wallaby-webpack/-/wallaby-webpack-3.9.16.tgz",
- "integrity": "sha512-z252lpX5+SrE3DM/YM1XKqJNsYjO5Sd9ATPe9MZCvPHDJZk015OVkBbJqBlcTwOjSS3WCrMQzm6t9tgFnNYHIg==",
- "dev": true,
- "dependencies": {
- "graceful-fs": "^4.1.3",
- "lodash": "^4.17.10",
- "minimatch": "3.0.3"
- }
- },
- "node_modules/wallaby-webpack/node_modules/minimatch": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
- "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.0.0"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@@ -49124,28 +49100,6 @@
"makeerror": "1.0.12"
}
},
- "wallaby-webpack": {
- "version": "3.9.16",
- "resolved": "https://registry.npmjs.org/wallaby-webpack/-/wallaby-webpack-3.9.16.tgz",
- "integrity": "sha512-z252lpX5+SrE3DM/YM1XKqJNsYjO5Sd9ATPe9MZCvPHDJZk015OVkBbJqBlcTwOjSS3WCrMQzm6t9tgFnNYHIg==",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.3",
- "lodash": "^4.17.10",
- "minimatch": "3.0.3"
- },
- "dependencies": {
- "minimatch": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
- "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.0.0"
- }
- }
- }
- },
"watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
diff --git a/package.json b/package.json
index 5f65bc247..5b2b74e02 100644
--- a/package.json
+++ b/package.json
@@ -114,7 +114,6 @@
"typedoc": "0.25.3",
"typescript": "5.3.3",
"url-loader": "4.1.1",
- "wallaby-webpack": "3.9.16",
"webpack": "5.89.0",
"webpack-cli": "5.1.4"
},
diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts
index be70c5764..390313989 100644
--- a/sandbox/src/game.ts
+++ b/sandbox/src/game.ts
@@ -128,16 +128,16 @@ cards2.draw(game.graphicsContext, 0, 0);
jump.volume = 0.3;
-var loader = new ex.Loader();
-loader.addResource(heartImageSource);
-loader.addResource(heartTex);
-loader.addResource(imageRun);
-loader.addResource(imageJump);
-loader.addResource(imageBlocks);
-loader.addResource(spriteFontImage);
-loader.addResource(cards);
-loader.addResource(cloud);
-loader.addResource(jump);
+var boot = new ex.Loader();
+boot.addResource(heartImageSource);
+boot.addResource(heartTex);
+boot.addResource(imageRun);
+boot.addResource(imageJump);
+boot.addResource(imageBlocks);
+boot.addResource(spriteFontImage);
+boot.addResource(cards);
+boot.addResource(cloud);
+boot.addResource(jump);
// Set background color
game.backgroundColor = new ex.Color(114, 213, 224);
@@ -942,6 +942,6 @@ game.currentScene.camera.strategy.lockToActorAxis(player, ex.Axis.X);
game.currentScene.camera.y = 200;
// Run the mainloop
-game.start(loader).then(() => {
+game.start(boot).then(() => {
logger.info('All Resources have finished loading');
});
\ No newline at end of file
diff --git a/sandbox/tests/gotoscene/index.ts b/sandbox/tests/gotoscene/index.ts
index 3baf1bd30..47562a151 100644
--- a/sandbox/tests/gotoscene/index.ts
+++ b/sandbox/tests/gotoscene/index.ts
@@ -1,5 +1,8 @@
class Scene1 extends ex.Scene {
- onInitialize(_engine: ex.Engine): void {
+ async onInitialize(_engine: ex.Engine) {
+ console.log('before async');
+ await ex.Util.delay(1000);
+ console.log('after async');
const actor = new ex.Actor({
x: _engine.halfDrawWidth,
y: _engine.halfDrawHeight,
@@ -11,20 +14,21 @@ class Scene1 extends ex.Scene {
_engine.input.pointers.primary.on(
"down",
- (event: ex.PointerEvent): void => {
- _engine.goToScene("scene2");
+ async (event: ex.PointerEvent) => {
+ await _engine.goToScene("scene2");
}
);
}
- onActivate(): void {
+ async onActivate() {
console.log('Scene 1 Activate')
}
}
class Scene2 extends ex.Scene {
- onInitialize(_engine: ex.Engine): void {
+ async onInitialize(_engine: ex.Engine) {
+ await ex.Util.delay(1000);
// _engine.start();
const actor = new ex.Actor({
pos: ex.Vector.Zero,
@@ -32,9 +36,11 @@ class Scene2 extends ex.Scene {
height: 1000,
color: ex.Color.Cyan,
});
+ actor.angularVelocity = 1;
_engine.add(actor);
}
- onActivate(): void {
+ async onActivate() {
+ await ex.Util.delay(1000);
console.log('Scene 2 Activate')
}
}
diff --git a/sandbox/tests/router/index.html b/sandbox/tests/router/index.html
new file mode 100644
index 000000000..595383858
--- /dev/null
+++ b/sandbox/tests/router/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Router Test
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/tests/router/index.ts b/sandbox/tests/router/index.ts
new file mode 100644
index 000000000..9b9371858
--- /dev/null
+++ b/sandbox/tests/router/index.ts
@@ -0,0 +1,139 @@
+///
+var scene1 = new ex.Scene();
+scene1.add(new ex.Label({
+ pos: ex.vec(100, 100),
+ color: ex.Color.Green,
+ text: 'Scene 1',
+ z: 99
+}))
+var scene2 = new ex.Scene();
+scene2.add(new ex.Label({
+ pos: ex.vec(100, 100),
+ color: ex.Color.Violet,
+ text: 'Scene 2',
+ z: 99
+}))
+
+class MyCustomScene extends ex.Scene {
+ onTransition(direction: "in" | "out") {
+ return new ex.FadeInOut({
+ direction,
+ color: ex.Color.Violet,
+ duration: 2000
+ });
+ }
+ onPreLoad(loader: ex.DefaultLoader): void {
+ const image1 = new ex.ImageSource('./spritefont.png?=1');
+ const image2 = new ex.ImageSource('./spritefont.png?=2');
+ const image3 = new ex.ImageSource('./spritefont.png?=3');
+ const image4 = new ex.ImageSource('./spritefont.png?=4');
+ const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png');
+ loader.addResource(image1);
+ loader.addResource(image2);
+ loader.addResource(image3);
+ loader.addResource(image4);
+ loader.addResource(sword);
+ }
+ onActivate(context: ex.SceneActivationContext): void {
+ console.log(context.data);
+ }
+}
+
+let scenes = {
+ scene1: {
+ scene: scene1,
+ transitions: {
+ in: new ex.FadeInOut({duration: 500, direction: 'in'})
+ }
+ },
+ scene2: {
+ scene: scene2,
+ loader: ex.DefaultLoader,
+ transitions: {
+ out: new ex.FadeInOut({duration: 500, direction: 'out'}),
+ in: new ex.CrossFade({duration: 2500, direction: 'in', blockInput: true})
+ }
+ },
+ scene3: MyCustomScene
+} satisfies ex.SceneMap;
+
+var gameWithTransitions = new ex.Engine({
+ width: 800,
+ height: 600,
+ displayMode: ex.DisplayMode.FitScreenAndFill,
+ scenes
+});
+
+
+var actor = new ex.Actor({
+ width: 100,
+ height: 100,
+ pos: ex.vec(100, 100),
+ color: ex.Color.Red
+})
+actor.addChild(new ex.Actor({
+ width: 100,
+ height: 100,
+ pos: ex.vec(100, 100),
+ color: ex.Color.Black
+}));
+scene1.add(actor);
+
+
+scene2.onPreLoad = (loader) => {
+ const image1 = new ex.ImageSource('./spritefont.png?=1');
+ const image2 = new ex.ImageSource('./spritefont.png?=2');
+ const image3 = new ex.ImageSource('./spritefont.png?=3');
+ const image4 = new ex.ImageSource('./spritefont.png?=4');
+ const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png');
+ loader.addResource(image1);
+ loader.addResource(image2);
+ loader.addResource(image3);
+ loader.addResource(image4);
+ loader.addResource(sword);
+}
+scene1.onActivate = () => {
+ setTimeout(() => {
+ gameWithTransitions.goto('scene2');
+ // router.goto('scene2', {
+ // outTransition: new ex.FadeOut({duration: 1000, direction: 'in'}),
+ // inTransition: new ex.FadeOut({duration: 1000, direction: 'out'})
+ // });
+ }, 1000);
+}
+scene2.add(new ex.Actor({
+ width: 100,
+ height: 100,
+ pos: ex.vec(400, 400),
+ color: ex.Color.Blue
+}));
+
+var boot = new ex.Loader();
+const image1 = new ex.ImageSource('./spritefont.png?=1');
+const image2 = new ex.ImageSource('./spritefont.png?=2');
+const image3 = new ex.ImageSource('./spritefont.png?=3');
+const image4 = new ex.ImageSource('./spritefont.png?=4');
+const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png');
+boot.addResource(image1);
+boot.addResource(image2);
+boot.addResource(image3);
+boot.addResource(image4);
+boot.addResource(sword);
+gameWithTransitions.input.keyboard.on('press', evt => {
+ gameWithTransitions.goto('scene3', {
+ sceneActivationData: { data: 1 }
+ });
+});
+gameWithTransitions.input.pointers.primary.on('down', () => {
+ gameWithTransitions.goto('scene1');
+});
+var startTransition = new ex.FadeInOut({duration: 500, direction: 'in', color: ex.Color.ExcaliburBlue});
+// startTransition.events.on('kill', () => {
+// console.log(game.currentScene.entities);
+// console.log('killed!');
+// })
+gameWithTransitions.start('scene1',
+{
+ inTransition: startTransition,
+ loader: boot
+});
\ No newline at end of file
diff --git a/sandbox/tests/router/spritefont.png b/sandbox/tests/router/spritefont.png
new file mode 100644
index 000000000..a7fbf645b
Binary files /dev/null and b/sandbox/tests/router/spritefont.png differ
diff --git a/sandbox/tests/scene/lifecycle.ts b/sandbox/tests/scene/lifecycle.ts
index 29b97e01e..7554ed9fe 100644
--- a/sandbox/tests/scene/lifecycle.ts
+++ b/sandbox/tests/scene/lifecycle.ts
@@ -17,7 +17,7 @@ class MyGame2 extends ex.Engine {
console.log("scene deactivate");
}
}
- onInitialize() {
+ async onInitialize() {
console.log("engine init");
}
}
diff --git a/sandbox/tests/sound-loop/index.ts b/sandbox/tests/sound-loop/index.ts
index 7f5e5da02..1cf589a63 100644
--- a/sandbox/tests/sound-loop/index.ts
+++ b/sandbox/tests/sound-loop/index.ts
@@ -27,13 +27,13 @@ class BaseScene extends ex.Scene {
}
}
-var scene1 = new BaseScene("scene1");
+var scene11 = new BaseScene("scene1");
var scene22 = new BaseScene("scene2");
-var scene3 = new BaseScene("scene3");
+var scene33 = new BaseScene("scene3");
-game.add('scene1', scene1);
+game.add('scene1', scene11);
game.add('scene2', scene22);
-game.add('scene3', scene3);
+game.add('scene3', scene33);
game.start(loader).then(() => {
game.goToScene('scene1');
diff --git a/sandbox/tsconfig.json b/sandbox/tsconfig.json
index 4ee0140a4..6c1fe2dcd 100644
--- a/sandbox/tsconfig.json
+++ b/sandbox/tsconfig.json
@@ -2,8 +2,8 @@
"compilerOptions": {
"sourceMap": true,
"experimentalDecorators": true,
- "target": "es2015",
- "module": "es2015",
+ "target": "ES2022",
+ "module": "ESNext",
"removeComments": false,
"skipLibCheck": true,
"lib": ["dom", "es5", "es2015", "es2015.promise", "es2015.collection", "es2015.iterable"]
diff --git a/src/engine/Collision/CollisionSystem.ts b/src/engine/Collision/CollisionSystem.ts
index cf8b799ce..c530688a6 100644
--- a/src/engine/Collision/CollisionSystem.ts
+++ b/src/engine/Collision/CollisionSystem.ts
@@ -11,7 +11,10 @@ import { RealisticSolver } from './Solver/RealisticSolver';
import { CollisionSolver } from './Solver/Solver';
import { ColliderComponent } from './ColliderComponent';
import { CompositeCollider } from './Colliders/CompositeCollider';
-import { Engine, ExcaliburGraphicsContext, Scene, Side } from '..';
+import { Engine } from '../Engine';
+import { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext';
+import { Scene } from '../Scene';
+import { Side } from '../Collision/Side';
import { DynamicTreeCollisionProcessor } from './Detection/DynamicTreeCollisionProcessor';
import { PhysicsWorld } from './PhysicsWorld';
export class CollisionSystem extends System {
diff --git a/src/engine/Debug/Debug.ts b/src/engine/Debug/Debug.ts
index b1e5466da..eddf3742d 100644
--- a/src/engine/Debug/Debug.ts
+++ b/src/engine/Debug/Debug.ts
@@ -2,7 +2,7 @@ import { DebugFlags, ColorBlindFlags } from './DebugFlags';
import { Engine } from '../Engine';
import { Color } from '../Color';
import { CollisionContact } from '../Collision/Detection/CollisionContact';
-import { StandardClock, TestClock } from '..';
+import { StandardClock, TestClock } from '../Util/Clock';
/**
* Debug stats containing current and previous frame statistics
diff --git a/src/engine/Debug/DebugFlags.ts b/src/engine/Debug/DebugFlags.ts
index c2574da2e..c83031bf6 100644
--- a/src/engine/Debug/DebugFlags.ts
+++ b/src/engine/Debug/DebugFlags.ts
@@ -1,7 +1,7 @@
import { ColorBlindnessMode } from '../Graphics/PostProcessor/ColorBlindnessMode';
import { ColorBlindnessPostProcessor } from '../Graphics/PostProcessor/ColorBlindnessPostProcessor';
import { Engine } from '../Engine';
-import { ExcaliburGraphicsContextWebGL } from '..';
+import { ExcaliburGraphicsContextWebGL } from '../Graphics/Context/ExcaliburGraphicsContextWebGL';
export interface DebugFlags {
colorBlindMode: ColorBlindFlags;
diff --git a/src/engine/Debug/DebugSystem.ts b/src/engine/Debug/DebugSystem.ts
index fcc139321..68b8d2190 100644
--- a/src/engine/Debug/DebugSystem.ts
+++ b/src/engine/Debug/DebugSystem.ts
@@ -8,7 +8,11 @@ import { System, SystemType } from '../EntityComponentSystem/System';
import { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext';
import { vec, Vector } from '../Math/vector';
import { toDegrees } from '../Math/util';
-import { BodyComponent, CollisionSystem, CompositeCollider, GraphicsComponent, Particle } from '..';
+import { BodyComponent } from '../Collision/BodyComponent';
+import { CollisionSystem } from '../Collision/CollisionSystem';
+import { CompositeCollider } from '../Collision/Colliders/CompositeCollider';
+import { GraphicsComponent } from '../Graphics/GraphicsComponent';
+import { Particle } from '../Particles';
import { DebugGraphicsComponent } from '../Graphics/DebugGraphicsComponent';
import { CoordPlane } from '../Math/coord-plane';
diff --git a/src/engine/Director/CrossFade.ts b/src/engine/Director/CrossFade.ts
new file mode 100644
index 000000000..b9bc9240b
--- /dev/null
+++ b/src/engine/Director/CrossFade.ts
@@ -0,0 +1,54 @@
+import { ImageSource, Sprite } from '../Graphics';
+import { Engine } from '../Engine';
+import { Scene } from '../Scene';
+import { Transition, TransitionOptions } from './Transition';
+import { vec } from '../Math/vector';
+
+export interface CrossFadeOptions {
+ duration: number;
+}
+
+/**
+ * CrossFades between the previous scene and the destination scene
+ *
+ * Note: CrossFade only works as an "in" transition
+ */
+export class CrossFade extends Transition {
+ engine: Engine;
+ image: HTMLImageElement;
+ screenCover: Sprite;
+ constructor(options: TransitionOptions & CrossFadeOptions) {
+ super(options);
+ this.name = `CrossFade#${this.id}`;
+ }
+
+ override async onPreviousSceneDeactivate(scene: Scene) {
+ this.image = await scene.engine.screenshot(true);
+ }
+
+ override onInitialize(engine: Engine): void {
+ this.engine = engine;
+ const bounds = engine.screen.getWorldBounds();
+ this.transform.pos = vec(bounds.left, bounds.top);
+ this.screenCover = ImageSource.fromHtmlImageElement(this.image).toSprite();
+ this.graphics.add(this.screenCover);
+ this.transform.scale = vec(1 / engine.screen.pixelRatio, 1 / engine.screen.pixelRatio);
+ this.graphics.opacity = this.progress;
+ }
+
+ override onStart(_progress: number): void {
+ this.graphics.opacity = this.progress;
+ }
+
+ override onReset() {
+ this.graphics.opacity = this.progress;
+ }
+
+ override onEnd(progress: number): void {
+ this.graphics.opacity = progress;
+ }
+
+ override onUpdate(progress: number): void {
+ this.graphics.opacity = progress;
+ }
+}
\ No newline at end of file
diff --git a/src/engine/Director/DefaultLoader.ts b/src/engine/Director/DefaultLoader.ts
new file mode 100644
index 000000000..bafe3dcc8
--- /dev/null
+++ b/src/engine/Director/DefaultLoader.ts
@@ -0,0 +1,260 @@
+import { WebAudio } from '../Util/WebAudio';
+import { Engine } from '../Engine';
+import { Loadable } from '../Interfaces/Loadable';
+import { Canvas } from '../Graphics/Canvas';
+import { ImageFiltering } from '../Graphics/Filtering';
+import { clamp } from '../Math/util';
+import { Sound } from '../Resources/Sound/Sound';
+import { Future } from '../Util/Future';
+import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter';
+import { Color } from '../Color';
+import { delay } from '../Util/Util';
+
+export interface LoaderOptions {
+ loadables: Loadable[];
+}
+
+export type LoaderEvents = {
+ // Add event types here
+ beforeload: void,
+ afterload: void,
+ useraction: void,
+ loadresourcestart: Loadable,
+ loadresourceend: Loadable,
+}
+
+export const LoaderEvents = {
+ // Add event types here
+ BeforeLoad: 'beforeload',
+ AfterLoad: 'afterload',
+ UserAction: 'useraction',
+ LoadResourceStart: 'loadresourcestart',
+ LoadResourceEnd: 'loadresourceend'
+};
+
+export type LoaderConstructor = new (...args: any[]) => DefaultLoader;
+/**
+ * Returns true if the constructor is for an Excalibur Loader
+ */
+export function isLoaderConstructor(x: any): x is LoaderConstructor {
+ return !!x?.prototype && !!x?.prototype?.constructor?.name;
+}
+
+export class DefaultLoader implements Loadable[]> {
+ public data: Loadable[];
+ public events = new EventEmitter();
+ public canvas: Canvas = new Canvas({
+ filtering: ImageFiltering.Blended,
+ smoothing: true,
+ cache: false,
+ draw: this.onDraw.bind(this)
+ });
+ private _resources: Loadable[] = [];
+ public get resources(): readonly Loadable[] {
+ return this._resources;
+ }
+ private _numLoaded: number = 0;
+ public engine: Engine;
+
+
+ /**
+ * @param options Optionally provide the list of resources you want to load at constructor time
+ */
+ constructor(options?: LoaderOptions) {
+ if (options && options.loadables?.length) {
+ this.addResources(options.loadables);
+ }
+ }
+
+ /**
+ * Called by the engine before loading
+ * @param engine
+ */
+ public onInitialize(engine: Engine) {
+ this.engine = engine;
+ this.canvas.width = this.engine.screen.canvasWidth;
+ this.canvas.height = this.engine.screen.canvasHeight;
+ }
+
+ /**
+ * Return a promise that resolves when the user interacts with the loading screen in some way, usually a click.
+ *
+ * It's important to implement this in order to unlock the audio context in the browser. Browsers automatically prevent
+ * audio from playing until the user performs an action.
+ *
+ */
+ public async onUserAction(): Promise {
+
+ return await Promise.resolve();
+ }
+
+ /**
+ * Overridable lifecycle method, called directly before loading starts
+ */
+ public async onBeforeLoad() {
+ // override me
+ }
+
+ /**
+ * Overridable lifecycle method, called after loading has completed
+ */
+ public async onAfterLoad() {
+ // override me
+ await delay(500, this.engine.clock); // avoid a flicker
+ }
+
+ /**
+ * Add a resource to the loader to load
+ * @param loadable Resource to add
+ */
+ public addResource(loadable: Loadable) {
+ this._resources.push(loadable);
+ }
+
+ /**
+ * Add a list of resources to the loader to load
+ * @param loadables The list of resources to load
+ */
+ public addResources(loadables: Loadable[]) {
+ let i = 0;
+ const len = loadables.length;
+
+ for (i; i < len; i++) {
+ this.addResource(loadables[i]);
+ }
+ }
+
+ public markResourceComplete(): void {
+ this._numLoaded++;
+ }
+
+ /**
+ * Returns the progress of the loader as a number between [0, 1] inclusive.
+ */
+ public get progress(): number {
+ const total = this._resources.length;
+ return total > 0 ? clamp(this._numLoaded, 0, total) / total : 1;
+ }
+
+ /**
+ * Returns true if the loader has completely loaded all resources
+ */
+ public isLoaded() {
+ return this._numLoaded === this._resources.length;
+ }
+
+ private _totalTimeMs = 0;
+
+ /**
+ * Optionally override the onUpdate
+ * @param engine
+ * @param elapsedMilliseconds
+ */
+ onUpdate(engine: Engine, elapsedMilliseconds: number): void {
+ this._totalTimeMs += elapsedMilliseconds;
+ // override me
+ }
+
+ /**
+ * Optionally override the onDraw
+ */
+ onDraw(ctx: CanvasRenderingContext2D) {
+ const seconds = this._totalTimeMs / 1000;
+
+ ctx.fillStyle = Color.Black.toRGBA();
+ ctx.fillRect(0, 0, this.engine.screen.drawWidth, this.engine.screen.drawHeight);
+
+ ctx.save();
+ ctx.translate(this.engine.screen.center.x, this.engine.screen.center.y);
+ const speed = seconds * 10;
+ ctx.strokeStyle = 'white';
+ ctx.lineWidth = 10;
+ ctx.lineCap = 'round';
+ ctx.arc(0, 0, 40, speed, speed + (Math.PI * 3 / 2));
+ ctx.stroke();
+
+ ctx.fillStyle = 'white';
+ ctx.font = '16px sans-serif';
+ const text = (this.progress * 100).toFixed(0) + '%';
+ const textbox = ctx.measureText(text);
+ const width = Math.abs(textbox.actualBoundingBoxLeft) + Math.abs(textbox.actualBoundingBoxRight);
+ const height = Math.abs(textbox.actualBoundingBoxAscent) + Math.abs(textbox.actualBoundingBoxDescent);
+ ctx.fillText(text, -width / 2, height / 2); // center
+ ctx.restore();
+ }
+
+
+ private _loadingFuture = new Future();
+ public areResourcesLoaded() {
+ return this._loadingFuture.promise;
+ }
+
+ /**
+ * Not meant to be overridden
+ *
+ * Begin loading all of the supplied resources, returning a promise
+ * that resolves when loading of all is complete AND the user has interacted with the loading screen
+ */
+ public async load(): Promise[]> {
+ await this.onBeforeLoad();
+ this.events.emit('beforeload');
+ this.canvas.flagDirty();
+
+ await Promise.all(
+ this._resources.map(async (r) => {
+ this.events.emit('loadresourcestart', r);
+ await r.load().finally(() => {
+ // capture progress
+ this._numLoaded++;
+ this.canvas.flagDirty();
+ this.events.emit('loadresourceend', r);
+ });
+ })
+ );
+
+ // Wire all sound to the engine
+ for (const resource of this._resources) {
+ if (resource instanceof Sound) {
+ resource.wireEngine(this.engine);
+ }
+ }
+
+ this._loadingFuture.resolve();
+ this.canvas.flagDirty();
+ // Unlock browser AudioContext in after user gesture
+ // See: https://github.com/excaliburjs/Excalibur/issues/262
+ // See: https://github.com/excaliburjs/Excalibur/issues/1031
+ await this.onUserAction();
+ this.events.emit('useraction');
+ await WebAudio.unlock();
+
+ await this.onAfterLoad();
+ this.events.emit('afterload');
+ return (this.data = this._resources);
+ }
+
+ public emit>(eventName: TEventName, event: LoaderEvents[TEventName]): void;
+ public emit(eventName: string, event?: any): void;
+ public emit | string>(eventName: TEventName, event?: any): void {
+ this.events.emit(eventName, event);
+ }
+
+ public on>(eventName: TEventName, handler: Handler): Subscription;
+ public on(eventName: string, handler: Handler): Subscription;
+ public on | string>(eventName: TEventName, handler: Handler): Subscription {
+ return this.events.on(eventName, handler);
+ }
+
+ public once>(eventName: TEventName, handler: Handler): Subscription;
+ public once(eventName: string, handler: Handler): Subscription;
+ public once | string>(eventName: TEventName, handler: Handler): Subscription {
+ return this.events.once(eventName, handler);
+ }
+
+ public off>(eventName: TEventName, handler: Handler): void;
+ public off(eventName: string, handler: Handler): void;
+ public off(eventName: string): void;
+ public off | string>(eventName: TEventName, handler?: Handler): void {
+ this.events.off(eventName, handler);
+ }
+}
diff --git a/src/engine/Director/Director.ts b/src/engine/Director/Director.ts
new file mode 100644
index 000000000..1658d0ea0
--- /dev/null
+++ b/src/engine/Director/Director.ts
@@ -0,0 +1,553 @@
+import { Engine } from '../Engine';
+import { DefaultLoader, LoaderConstructor, isLoaderConstructor } from './DefaultLoader';
+import { Scene, SceneConstructor, isSceneConstructor } from '../Scene';
+import { Transition } from './Transition';
+import { Loader } from './Loader';
+import { Logger } from '../Util/Log';
+import { ActivateEvent, DeactivateEvent } from '../Events';
+import { EventEmitter } from '../EventEmitter';
+
+export interface DirectorNavigationEvent {
+ sourceName: string;
+ sourceScene: Scene;
+ destinationName: string;
+ destinationScene: Scene;
+}
+
+export type DirectorEvents = {
+ navigationstart: DirectorNavigationEvent,
+ navigation: DirectorNavigationEvent,
+ navigationend: DirectorNavigationEvent,
+}
+
+export const DirectorEvents = {
+ NavigationStart: 'navigationstart',
+ Navigation: 'navigation',
+ NavigationEnd: 'navigationend'
+};
+
+export interface SceneWithOptions {
+ /**
+ * Scene associated with this route
+ *
+ * If a constructor is provided it will not be constructed until navigation is requested
+ */
+ scene: Scene | SceneConstructor;
+ /**
+ * Specify scene transitions
+ */
+ transitions?: {
+ /**
+ * Optionally specify a transition when going "in" to this scene
+ */
+ in?: Transition;
+ /**
+ * Optionally specify a transition when going "out" of this scene
+ */
+ out?: Transition;
+ };
+ /**
+ * Optionally specify a loader for the scene
+ */
+ loader?: DefaultLoader | LoaderConstructor;
+}
+
+export type WithRoot = TScenes | 'root';
+
+export type SceneMap = Record;
+
+export interface StartOptions {
+ /**
+ * First transition from the game start screen
+ */
+ inTransition: Transition;
+ /**
+ * Optionally provide a main loader to run before the game starts
+ */
+ loader?: DefaultLoader | LoaderConstructor
+}
+
+
+/**
+ * Provide scene activation data and override any existing configured route transitions or loaders
+ */
+export interface GoToOptions {
+ /**
+ * Optionally supply scene activation data passed to Scene.onActivate
+ */
+ sceneActivationData?: any,
+ /**
+ * Optionally supply destination scene "in" transition, this will override any previously defined transition
+ */
+ destinationIn?: Transition,
+ /**
+ * Optionally supply source scene "out" transition, this will override any previously defined transition
+ */
+ sourceOut?: Transition,
+ /**
+ * Optionally supply a different loader for the destination scene, this will override any previously defined loader
+ */
+ loader?: DefaultLoader
+}
+
+/**
+ * The Director is responsible for managing scenes and changing scenes in Excalibur.
+ *
+ * It deals with transitions, scene loaders, switching scenes
+ *
+ * This is used internally by Excalibur, generally not mean to
+ * be instantiated end users directly.
+ */
+export class Director {
+ public events = new EventEmitter();
+ private _logger = Logger.getInstance();
+ private _deferredGoto: string;
+ private _initialized = false;
+
+ /**
+ * Current scene's name
+ */
+ currentSceneName: string;
+ /**
+ * Current scene playing in excalibur
+ */
+ currentScene: Scene;
+ /**
+ * Current transition if any
+ */
+ currentTransition: Transition | null;
+
+ /**
+ * All registered scenes in Excalibur
+ */
+ public readonly scenes: SceneMap> = {} as SceneMap>;
+
+ /**
+ * Holds all instantiated scenes
+ */
+ private _sceneToInstance = new Map();
+
+ startScene: string;
+ mainLoader: DefaultLoader;
+
+ /**
+ * The default [[Scene]] of the game, use [[Engine.goto]] to transition to different scenes.
+ */
+ public readonly rootScene: Scene;
+
+ private _sceneToLoader = new Map();
+ private _sceneToTransition = new Map();
+ /**
+ * Used to keep track of scenes that have already been loaded so we don't load multiple times
+ */
+ private _loadedScenes = new Set();
+
+ private _isTransitioning = false;
+
+ /**
+ * Gets whether the director currently transitioning between scenes
+ *
+ * Useful if you need to block behavior during transition
+ */
+ public get isTransitioning() {
+ return this._isTransitioning;
+ }
+
+ constructor(private _engine: Engine, scenes: SceneMap) {
+ this.rootScene = this.currentScene = new Scene();
+ this.add('root', this.rootScene);
+ this.currentScene = this.rootScene;
+ this.currentSceneName = 'root';
+ for (const sceneKey in scenes) {
+ const sceneOrOptions = scenes[sceneKey];
+ this.add(sceneKey, sceneOrOptions);
+ }
+ }
+
+ /**
+ * Initialize the director's internal state
+ */
+ async onInitialize() {
+ if (!this._initialized) {
+ this._initialized = true;
+ if (this._deferredGoto) {
+ const deferredScene = this._deferredGoto;
+ this._deferredGoto = null;
+ await this.swapScene(deferredScene);
+ } else {
+ await this.swapScene('root');
+ }
+ }
+ }
+
+ get isInitialized() {
+ return this._initialized;
+ }
+
+ /**
+ * Configures the start scene, and optionally the transition & loader for the director
+ *
+ * Typically this is called at the beginning of the game to the start scene and transition and never again.
+ * @param startScene
+ * @param options
+ */
+ configureStart(startScene: WithRoot, options?: StartOptions) {
+ const maybeLoaderOrCtor = options?.loader;
+ if (maybeLoaderOrCtor instanceof DefaultLoader) {
+ this.mainLoader = maybeLoaderOrCtor;
+ } else if (isLoaderConstructor(maybeLoaderOrCtor)) {
+ this.mainLoader = new maybeLoaderOrCtor();
+ } else {
+ this.mainLoader = new Loader();
+ }
+
+ let maybeStartTransition: Transition;
+
+ if (options) {
+ const { inTransition } = options;
+ maybeStartTransition = inTransition;
+ }
+
+ this.startScene = startScene;
+
+ // Fire and forget promise for the initial scene
+ if (maybeStartTransition) {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this.swapScene(this.startScene);
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this.playTransition(maybeStartTransition);
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this.swapScene(this.startScene);
+ }
+
+ this.currentSceneName = this.startScene;
+ }
+
+ private _getLoader(sceneName: string) {
+ return this._sceneToLoader.get(sceneName);
+ }
+
+ private _getInTransition(sceneName: string): Transition | undefined {
+ const sceneOrRoute = this.scenes[sceneName as TKnownScenes];
+ if (sceneOrRoute instanceof Scene || isSceneConstructor(sceneOrRoute)) {
+ return null;
+ }
+ return sceneOrRoute?.transitions?.in;
+ }
+
+ private _getOutTransition(sceneName: string): Transition | undefined {
+ const sceneOrRoute = this.scenes[sceneName as TKnownScenes];
+ if (sceneOrRoute instanceof Scene || isSceneConstructor(sceneOrRoute)) {
+ return null;
+ }
+ return sceneOrRoute?.transitions?.out;
+ }
+
+ getDeferredScene() {
+ const maybeDeferred = this.getSceneDefinition(this._deferredGoto);
+ if (this._deferredGoto && maybeDeferred) {
+ return maybeDeferred;
+ }
+ return null;
+ }
+
+ /**
+ * Returns a scene by name if it exists, might be the constructor and not the instance of a scene
+ * @param name
+ */
+ getSceneDefinition(name: string): Scene | SceneConstructor | undefined {
+ const maybeScene = this.scenes[name as TKnownScenes];
+ if (maybeScene instanceof Scene || isSceneConstructor(maybeScene)) {
+ return maybeScene;
+ } else if (maybeScene) {
+ return maybeScene.scene;
+ }
+ return undefined;
+ }
+
+ /**
+ * Returns the same Director, but asserts a scene DOES exist to the type system
+ * @param name
+ */
+ assertAdded(name: TScene): Director {
+ return this as Director;
+ }
+
+ /**
+ * Returns the same Director, but asserts a scene DOES NOT exist to the type system
+ * @param name
+ */
+ assertRemoved(name: TScene): Director> {
+ return this as Director>;
+ }
+
+ /**
+ * Adds additional Scenes to the game!
+ * @param name
+ * @param sceneOrRoute
+ */
+ add(name: TScene, sceneOrRoute: Scene | SceneConstructor | SceneWithOptions): Director {
+ if (!(sceneOrRoute instanceof Scene) && !(isSceneConstructor(sceneOrRoute))) {
+ const { loader, transitions } = sceneOrRoute;
+ const {in: inTransition, out: outTransition } = transitions ?? {};
+ this._sceneToTransition.set(name, {in: inTransition, out: outTransition});
+
+ if (isLoaderConstructor(loader)) {
+ this._sceneToLoader.set(name, new loader());
+ } else {
+ this._sceneToLoader.set(name, loader);
+ }
+ }
+
+ if (this.scenes[name as unknown as TKnownScenes]) {
+ this._logger.warn('Scene', name, 'already exists overwriting');
+ }
+ this.scenes[name as unknown as TKnownScenes] = sceneOrRoute;
+ return this.assertAdded(name);
+ }
+
+ remove(scene: Scene): void;
+ remove(sceneCtor: SceneConstructor): void;
+ remove(name: WithRoot): void;
+ remove(nameOrScene: TKnownScenes | Scene | SceneConstructor | string) {
+
+ if (nameOrScene instanceof Scene || isSceneConstructor(nameOrScene)) {
+ const sceneOrCtor = nameOrScene;
+ // remove scene
+ for (const key in this.scenes) {
+ if (this.scenes.hasOwnProperty(key)) {
+ const potentialSceneOrOptions = this.scenes[key as TKnownScenes];
+ let scene: Scene | SceneConstructor;
+ if (potentialSceneOrOptions instanceof Scene || isSceneConstructor(potentialSceneOrOptions)) {
+ scene = potentialSceneOrOptions;
+ } else {
+ scene = potentialSceneOrOptions.scene;
+ }
+
+ if (scene === sceneOrCtor) {
+ if (key === this.currentSceneName) {
+ throw new Error(`Cannot remove a currently active scene: ${key}`);
+ }
+
+ this._sceneToTransition.delete(key);
+ this._sceneToLoader.delete(key);
+ delete this.scenes[key as TKnownScenes];
+ }
+ }
+ }
+ }
+ if (typeof nameOrScene === 'string') {
+ if (nameOrScene === this.currentSceneName) {
+ throw new Error(`Cannot remove a currently active scene: ${nameOrScene}`);
+ }
+
+ // remove scene
+ this._sceneToTransition.delete(nameOrScene);
+ this._sceneToLoader.delete(nameOrScene);
+ delete this.scenes[nameOrScene as TKnownScenes];
+ }
+ }
+
+ /**
+ * Go to a specific scene, and optionally override loaders and transitions
+ * @param destinationScene
+ * @param options
+ */
+ async goto(destinationScene: TKnownScenes | string, options?: GoToOptions) {
+ if (destinationScene === this.currentSceneName) {
+ return;
+ }
+
+ const maybeDest = this.getSceneInstance(destinationScene);
+ if (!maybeDest) {
+ this._logger.warn(`Scene ${destinationScene} does not exist! Check the name, are you sure you added it?`);
+ return;
+ }
+
+ if (this._isTransitioning) {
+ // ? is this going to suck? I remember flux would block actions if one was already running and it made me sad
+ this._logger.warn('Cannot transition while a transition is in progress');
+ return;
+ }
+
+ const sourceScene = this.currentSceneName;
+ this._isTransitioning = true;
+
+ const maybeSourceOut = this.getSceneInstance(sourceScene)?.onTransition('out');
+ const maybeDestinationIn = maybeDest?.onTransition('in');
+
+ options = {
+ // Engine configuration then dynamic scene transitions
+ ...{ sourceOut: this._getOutTransition(this.currentSceneName) ?? maybeSourceOut},
+ ...{ destinationIn: this._getInTransition(destinationScene) ?? maybeDestinationIn},
+ // Goto options
+ ...options };
+
+ const { sourceOut, destinationIn, sceneActivationData } = options;
+
+ const outTransition = sourceOut ?? this._getOutTransition(this.currentSceneName);
+ const inTransition = destinationIn ?? this._getInTransition(destinationScene);
+
+ const hideLoader = outTransition?.hideLoader || inTransition?.hideLoader;
+ if (hideLoader) {
+ // Start hidden loader early and take advantage of the transition
+ // Don't await and block on a hidden loader
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this.maybeLoadScene(destinationScene, hideLoader);
+ }
+
+ this._emitEvent('navigationstart', sourceScene, destinationScene);
+
+ // Run the out transition on the current scene if present
+ await this.playTransition(outTransition);
+
+ // Run the loader if present
+ await this.maybeLoadScene(destinationScene, hideLoader);
+
+ // Give incoming transition a chance to grab info from previous
+ await inTransition?.onPreviousSceneDeactivate(this.currentScene);
+
+ // Swap to the new scene
+ await this.swapScene(destinationScene, sceneActivationData);
+ this._emitEvent('navigation', sourceScene, destinationScene);
+
+ // Run the in transition on the new scene if present
+ await this.playTransition(inTransition);
+ this._emitEvent('navigationend', sourceScene, destinationScene);
+
+ this._engine.toggleInputEnabled(true);
+ this._isTransitioning = false;
+ }
+
+ /**
+ * Retrieves a scene instance by key if it's registered.
+ *
+ * This will call any constructors that were given as a definition
+ * @param scene
+ */
+ getSceneInstance(scene: string): Scene | undefined {
+ const sceneDefinition = this.getSceneDefinition(scene);
+ if (!sceneDefinition) {
+ return undefined;
+ }
+ if (this._sceneToInstance.has(scene)) {
+ return this._sceneToInstance.get(scene) as Scene;
+ }
+ if (sceneDefinition instanceof Scene) {
+ this._sceneToInstance.set(scene, sceneDefinition);
+ return sceneDefinition;
+ }
+ const newScene = new sceneDefinition();
+ this._sceneToInstance.set(scene, newScene);
+ return newScene;
+ }
+
+ /**
+ * Triggers scene loading if has not already been loaded
+ * @param scene
+ * @param hideLoader
+ */
+ async maybeLoadScene(scene: string, hideLoader = false) {
+ const loader = this._getLoader(scene) ?? new DefaultLoader();
+ const sceneToLoad = this.getSceneDefinition(scene);
+ const sceneToLoadInstance = this.getSceneInstance(scene);
+ if (sceneToLoad && sceneToLoadInstance && !this._loadedScenes.has(sceneToLoadInstance)) {
+ sceneToLoadInstance.onPreLoad(loader);
+ sceneToLoadInstance.events.emit('preload', { loader });
+ if (hideLoader) {
+ // Don't await a hidden loader
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this._engine.load(loader, hideLoader);
+ } else {
+ await this._engine.load(loader);
+ }
+ this._loadedScenes.add(sceneToLoadInstance);
+ }
+ }
+
+ /**
+ * Plays a transition in the current scene
+ * @param transition
+ */
+ async playTransition(transition: Transition) {
+ if (transition) {
+ this.currentTransition = transition;
+ this._engine.toggleInputEnabled(!transition.blockInput);
+ this._engine.add(this.currentTransition);
+ await this.currentTransition.done;
+ }
+ this.currentTransition?.kill();
+ this.currentTransition?.reset();
+ this.currentTransition = null;
+ }
+
+ /**
+ * Swaps the current and destination scene after performing required lifecycle events
+ * @param destinationScene
+ * @param data
+ */
+ async swapScene(destinationScene: string, data?: TData): Promise {
+ const engine = this._engine;
+ // if not yet initialized defer goToScene
+ if (!this.isInitialized) {
+ this._deferredGoto = destinationScene;
+ return;
+ }
+
+ const maybeDest = this.getSceneInstance(destinationScene);
+
+ if (maybeDest) {
+ const previousScene = this.currentScene;
+ const nextScene = maybeDest;
+
+ this._logger.debug('Going to scene:', destinationScene);
+ // only deactivate when initialized
+ if (this.currentScene.isInitialized) {
+ const context = { engine, previousScene, nextScene };
+ await this.currentScene._deactivate(context);
+ this.currentScene.events.emit('deactivate', new DeactivateEvent(context, this.currentScene));
+ }
+
+ // wait for the scene to be loaded if needed
+ const destLoader = this._sceneToLoader.get(destinationScene);
+ await destLoader?.areResourcesLoaded();
+
+ // set current scene to new one
+ this.currentScene = nextScene;
+ this.currentSceneName = destinationScene;
+ engine.screen.setCurrentCamera(nextScene.camera);
+
+ // initialize the current scene if has not been already
+ await this.currentScene._initialize(engine);
+
+ const context = { engine, previousScene, nextScene, data };
+ await this.currentScene._activate(context);
+ this.currentScene.events.emit('activate', new ActivateEvent(context, this.currentScene));
+ } else {
+ this._logger.error('Scene', destinationScene, 'does not exist!');
+ }
+ }
+
+ private _emitEvent(eventName: keyof DirectorEvents, sourceScene: string, destinationScene: string) {
+ const source = this.getSceneDefinition(sourceScene)!;
+ const dest = this.getSceneDefinition(destinationScene)!;
+ this.events.emit(eventName, {
+ sourceScene: source,
+ sourceName: sourceScene,
+ destinationScene: dest,
+ destinationName: destinationScene
+ } as DirectorNavigationEvent);
+ }
+
+ /**
+ * Updates internal transitions
+ */
+ update() {
+ if (this.currentTransition) {
+ this.currentTransition.execute();
+ }
+ }
+}
+
+
diff --git a/src/engine/Director/FadeInOut.ts b/src/engine/Director/FadeInOut.ts
new file mode 100644
index 000000000..d2cb53106
--- /dev/null
+++ b/src/engine/Director/FadeInOut.ts
@@ -0,0 +1,51 @@
+import { Engine } from '../Engine';
+import { Color } from '../Color';
+import { vec } from '../Math/vector';
+import { Rectangle } from '../Graphics';
+import { Transition, TransitionOptions } from './Transition';
+
+export interface FadeOptions {
+ duration?: number;
+ color?: Color;
+}
+
+export class FadeInOut extends Transition {
+ screenCover: Rectangle;
+ color: Color;
+ constructor(options: FadeOptions & TransitionOptions) {
+ super({
+ ...options,
+ duration: options.duration ?? 2000
+ });
+ this.name = `FadeInOut#${this.id}`;
+ this.color = options.color ?? Color.Black;
+ }
+
+ public onInitialize(engine: Engine): void {
+ const bounds = engine.screen.getWorldBounds();
+ this.transform.pos = vec(bounds.left, bounds.top);
+ this.screenCover = new Rectangle({
+ width: bounds.width,
+ height: bounds.height,
+ color: this.color
+ });
+ this.graphics.add(this.screenCover);
+ this.graphics.opacity = this.progress;
+ }
+
+ override onReset() {
+ this.graphics.opacity = this.progress;
+ }
+
+ override onStart(progress: number): void {
+ this.graphics.opacity = progress;
+ }
+
+ override onEnd(progress: number): void {
+ this.graphics.opacity = progress;
+ }
+
+ override onUpdate(progress: number): void {
+ this.graphics.opacity = progress;
+ }
+}
\ No newline at end of file
diff --git a/src/engine/Loader.css b/src/engine/Director/Loader.css
similarity index 100%
rename from src/engine/Loader.css
rename to src/engine/Director/Loader.css
diff --git a/src/engine/Loader.logo.png b/src/engine/Director/Loader.logo.png
similarity index 100%
rename from src/engine/Loader.logo.png
rename to src/engine/Director/Loader.logo.png
diff --git a/src/engine/Loader.ts b/src/engine/Director/Loader.ts
similarity index 58%
rename from src/engine/Loader.ts
rename to src/engine/Director/Loader.ts
index 291e5ba46..4bbce7151 100644
--- a/src/engine/Loader.ts
+++ b/src/engine/Director/Loader.ts
@@ -1,28 +1,15 @@
-import { Color } from './Color';
-import { WebAudio } from './Util/WebAudio';
-import { Engine } from './Engine';
-import { Loadable } from './Interfaces/Loadable';
-import * as DrawUtil from './Util/DrawUtil';
+import { Color } from '../Color';
+import { Loadable } from '../Interfaces/Loadable';
+import * as DrawUtil from '../Util/DrawUtil';
import logoImg from './Loader.logo.png';
import loaderCss from './Loader.css';
-import { Canvas } from './Graphics/Canvas';
-import { Vector } from './Math/vector';
-import { delay } from './Util/Util';
-import { ImageFiltering } from './Graphics/Filtering';
-import { clamp } from './Math/util';
-import { Sound } from './Resources/Sound/Sound';
-import { Future } from './Util/Future';
-import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter';
-import { ScreenResizeEvent } from './Screen';
-
-export type LoaderEvents = {
- // Add event types here
-}
-
-export const LoaderEvents = {
- // Add event types here
-};
+import { Vector } from '../Math/vector';
+import { delay } from '../Util/Util';
+import { EventEmitter } from '../EventEmitter';
+import { DefaultLoader } from './DefaultLoader';
+import { Engine } from '../Engine';
+import { Screen } from '../Screen';
/**
* Pre-loading assets
@@ -89,23 +76,10 @@ export const LoaderEvents = {
* engine.start(loader).then(() => {});
* ```
*/
-export class Loader implements Loadable[]> {
+export class Loader extends DefaultLoader {
public events = new EventEmitter();
- public canvas: Canvas = new Canvas({
- filtering: ImageFiltering.Blended,
- smoothing: true,
- cache: true,
- draw: this.draw.bind(this)
- });
- private _resourceList: Loadable[] = [];
- private _index = 0;
-
+ public screen: Screen;
private _playButtonShown: boolean = false;
- private _resourceCount: number = 0;
- private _numLoaded: number = 0;
- private _progressCounts: { [key: string]: number } = {};
- private _totalCounts: { [key: string]: number } = {};
- private _engine: Engine;
// logo drawing stuff
@@ -149,13 +123,6 @@ export class Loader implements Loadable[]> {
return this._imageElement;
}
- private _getScreenParent() {
- if (this._engine) {
- return this._engine.screen.canvas.parentElement;
- }
- return document.body;
- }
-
public suppressPlayButton: boolean = false;
public get playButtonRootElement(): HTMLElement | null {
return this._playButtonRootElement;
@@ -177,10 +144,7 @@ export class Loader implements Loadable[]> {
this._playButtonRootElement = document.createElement('div');
this._playButtonRootElement.id = 'excalibur-play-root';
this._playButtonRootElement.style.position = 'absolute';
-
- // attach play button to canvas parent, this is important for fullscreen
- const parent = this._getScreenParent();
- parent.appendChild(this._playButtonRootElement);
+ document.body.appendChild(this._playButtonRootElement);
}
if (!this._styleBlock) {
this._styleBlock = document.createElement('style');
@@ -218,60 +182,14 @@ export class Loader implements Loadable[]> {
* @param loadables Optionally provide the list of resources you want to load at constructor time
*/
constructor(loadables?: Loadable[]) {
- if (loadables) {
- this.addResources(loadables);
- }
+ super({loadables});
}
- private _loaderResizeHandler = (evt: ScreenResizeEvent) => {
- // Configure resolution for loader, it expects resolution === viewport
- this._engine.screen.resolution = this._engine.screen.viewport;
- this._engine.screen.applyResolutionAndViewport();
-
- this.canvas.width = evt.viewport.width;
- this.canvas.height = evt.viewport.height;
- this.canvas.flagDirty();
- };
-
- public wireEngine(engine: Engine) {
- // wire once
- if (!this._engine) {
- this._engine = engine;
- this.canvas.width = this._engine.canvas.width;
- this.canvas.height = this._engine.canvas.height;
- }
- }
-
- /**
- * Add a resource to the loader to load
- * @param loadable Resource to add
- */
- public addResource(loadable: Loadable) {
- const key = this._index++;
- this._resourceList.push(loadable);
- this._progressCounts[key] = 0;
- this._totalCounts[key] = 1;
- this._resourceCount++;
- }
-
- /**
- * Add a list of resources to the loader to load
- * @param loadables The list of resources to load
- */
- public addResources(loadables: Loadable[]) {
- let i = 0;
- const len = loadables.length;
-
- for (i; i < len; i++) {
- this.addResource(loadables[i]);
- }
- }
-
- /**
- * Returns true if the loader has completely loaded all resources
- */
- public isLoaded() {
- return this._numLoaded === this._resourceCount;
+ public override onInitialize(engine: Engine): void {
+ this.engine = engine;
+ this.screen = engine.screen;
+ this.canvas.width = this.engine.canvas.width;
+ this.canvas.height = this.engine.canvas.height;
}
/**
@@ -281,13 +199,13 @@ export class Loader implements Loadable[]> {
if (this.suppressPlayButton) {
this.hidePlayButton();
// Delay is to give the logo a chance to show, otherwise don't delay
- await delay(500, this._engine?.clock);
+ await delay(500, this.engine?.clock);
} else {
const resizeHandler = () => {
this._positionPlayButton();
};
- if (this._engine?.browser) {
- this._engine.browser.window.on('resize', resizeHandler);
+ if (this.engine?.browser) {
+ this.engine.browser.window.on('resize', resizeHandler);
}
this._playButtonShown = true;
this._playButton.style.display = 'block';
@@ -303,8 +221,8 @@ export class Loader implements Loadable[]> {
e.stopPropagation();
// Hide Button after click
this.hidePlayButton();
- if (this._engine?.browser) {
- this._engine.browser.window.off('resize', resizeHandler);
+ if (this.engine?.browser) {
+ this.engine.browser.window.off('resize', resizeHandler);
}
resolve();
};
@@ -336,79 +254,43 @@ export class Loader implements Loadable[]> {
}
}
- update(engine: Engine, delta: number): void {
- // override me
- }
-
data: Loadable[];
- private _loadingFuture = new Future();
- public areResourcesLoaded() {
- return this._loadingFuture.promise;
- }
-
- /**
- * Begin loading all of the supplied resources, returning a promise
- * that resolves when loading of all is complete AND the user has clicked the "Play button"
- */
- public async load(): Promise[]> {
- this._engine.screen.events.on('resize', this._loaderResizeHandler);
-
- await this._image?.decode(); // decode logo if it exists
- this.canvas.flagDirty();
-
- await Promise.all(
- this._resourceList.map(async (r) => {
- await r.load().finally(() => {
- // capture progress
- this._numLoaded++;
- this.canvas.flagDirty();
- });
- })
- );
- // Wire all sound to the engine
- for (const resource of this._resourceList) {
- if (resource instanceof Sound) {
- resource.wireEngine(this._engine);
- }
- }
-
- this._loadingFuture.resolve();
-
+ public override async onUserAction(): Promise {
// short delay in showing the button for aesthetics
- await delay(200, this._engine?.clock);
+ await delay(200, this.engine?.clock);
this.canvas.flagDirty();
-
+ // show play button
await this.showPlayButton();
- // Unlock browser AudioContext in after user gesture
- // See: https://github.com/excaliburjs/Excalibur/issues/262
- // See: https://github.com/excaliburjs/Excalibur/issues/1031
- await WebAudio.unlock();
+ }
- // unload loader resize watcher
- this._engine.screen.events.off('resize', this._loaderResizeHandler);
+ public override async onBeforeLoad(): Promise {
+ // Push the current user entered resolution/viewport
+ this.screen.pushResolutionAndViewport();
+ // Configure resolution for loader, it expects resolution === viewport
+ this.screen.resolution = this.screen.viewport;
+ this.screen.applyResolutionAndViewport();
- return (this.data = this._resourceList);
- }
+ this.canvas.width = this.engine.canvas.width;
+ this.canvas.height = this.engine.canvas.height;
- public markResourceComplete(): void {
- this._numLoaded++;
+ await this._image?.decode(); // decode logo if it exists
}
- /**
- * Returns the progress of the loader as a number between [0, 1] inclusive.
- */
- public get progress(): number {
- return this._resourceCount > 0 ? clamp(this._numLoaded, 0, this._resourceCount) / this._resourceCount : 1;
+ // eslint-disable-next-line require-await
+ public override async onAfterLoad(): Promise {
+ this.screen.popResolutionAndViewport();
+ this.screen.applyResolutionAndViewport();
+ this.dispose();
}
private _positionPlayButton() {
- if (this._engine) {
- const screenHeight = this._engine.screen.viewport.height;
- const screenWidth = this._engine.screen.viewport.width;
+ if (this.engine) {
+ const screenHeight = this.engine.screen.viewport.height;
+ const screenWidth = this.engine.screen.viewport.width;
if (this._playButtonRootElement) {
- const left = this._engine.canvas.offsetLeft;
- const top = this._engine.canvas.offsetTop;
+ const left = this.engine.canvas.offsetLeft;
+ const top = this.engine.canvas.offsetTop;
const buttonWidth = this._playButton.clientWidth;
const buttonHeight = this._playButton.clientHeight;
if (this.playButtonPosition) {
@@ -427,9 +309,9 @@ export class Loader implements Loadable[]> {
* Override `logo`, `logoWidth`, `logoHeight` and `backgroundColor` properties
* to customize the drawing, or just override entire method.
*/
- public draw(ctx: CanvasRenderingContext2D) {
- const canvasHeight = this._engine.canvasHeight / this._engine.pixelRatio;
- const canvasWidth = this._engine.canvasWidth / this._engine.pixelRatio;
+ public override onDraw(ctx: CanvasRenderingContext2D) {
+ const canvasHeight = this.engine.canvasHeight / this.engine.pixelRatio;
+ const canvasWidth = this.engine.canvasWidth / this.engine.pixelRatio;
this._positionPlayButton();
@@ -446,8 +328,8 @@ export class Loader implements Loadable[]> {
}
const imageHeight = Math.floor(width * (this.logoHeight / this.logoWidth)); // OG height/width factor
- const oldAntialias = this._engine.getAntialiasing();
- this._engine.setAntialiasing(true);
+ const oldAntialias = this.engine.getAntialiasing();
+ this.engine.setAntialiasing(true);
if (!this.logoPosition) {
ctx.drawImage(this._image, 0, 0, this.logoWidth, this.logoHeight, logoX, logoY - imageHeight - 20, width, imageHeight);
} else {
@@ -456,7 +338,7 @@ export class Loader implements Loadable[]> {
// loading box
if (!this.suppressPlayButton && this._playButtonShown) {
- this._engine.setAntialiasing(oldAntialias);
+ this.engine.setAntialiasing(oldAntialias);
return;
}
@@ -483,31 +365,6 @@ export class Loader implements Loadable[]> {
null,
this.loadingBarColor
);
- this._engine.setAntialiasing(oldAntialias);
- }
-
- public emit>(eventName: TEventName, event: LoaderEvents[TEventName]): void;
- public emit(eventName: string, event?: any): void;
- public emit | string>(eventName: TEventName, event?: any): void {
- this.events.emit(eventName, event);
- }
-
- public on>(eventName: TEventName, handler: Handler): Subscription;
- public on(eventName: string, handler: Handler): Subscription;
- public on | string>(eventName: TEventName, handler: Handler): Subscription {
- return this.events.on(eventName, handler);
- }
-
- public once>(eventName: TEventName, handler: Handler): Subscription;
- public once(eventName: string, handler: Handler): Subscription;
- public once | string>(eventName: TEventName, handler: Handler): Subscription {
- return this.events.once(eventName, handler);
- }
-
- public off>(eventName: TEventName, handler: Handler): void;
- public off(eventName: string, handler: Handler): void;
- public off(eventName: string): void;
- public off | string>(eventName: TEventName, handler?: Handler): void {
- this.events.off(eventName, handler);
+ this.engine.setAntialiasing(oldAntialias);
}
}
diff --git a/src/engine/Director/Transition.ts b/src/engine/Director/Transition.ts
new file mode 100644
index 000000000..103295c03
--- /dev/null
+++ b/src/engine/Director/Transition.ts
@@ -0,0 +1,215 @@
+import { Engine } from '../Engine';
+import { Scene } from '../Scene';
+import { Future } from '../Util/Future';
+import { Entity, TransformComponent } from '../EntityComponentSystem';
+import { GraphicsComponent } from '../Graphics';
+import { CoordPlane } from '../Math/coord-plane';
+import { Vector } from '../Math/vector';
+import { clamp } from '../Math/util';
+import { EasingFunction, EasingFunctions } from '../Util/EasingFunctions';
+
+export interface TransitionOptions {
+ /**
+ * Transition duration in milliseconds
+ */
+ duration: number;
+
+ /**
+ * Optionally hides the loader during the transition
+ *
+ * If either the out or in transition have this set to true, then the loader will be hidden.
+ *
+ * Default false
+ */
+ hideLoader?: boolean;
+
+ /**
+ * Optionally blocks user input during a transition
+ *
+ * Default false
+ */
+ blockInput?: boolean;
+
+ /**
+ * Optionally specify a easing function, by default linear
+ */
+ easing?: EasingFunction;
+ /**
+ * Optionally specify a transition direction, by default 'out'
+ *
+ * * For 'in' direction transitions start at 1 and complete is at 0
+ * * For 'out' direction transitions start at 0 and complete is at 1
+ */
+ direction?: 'out' | 'in';
+}
+
+/**
+ * Base Transition that can be extended to provide custom scene transitions in Excalibur.
+ */
+export class Transition extends Entity {
+ transform = new TransformComponent();
+ graphics = new GraphicsComponent();
+ readonly hideLoader: boolean;
+ readonly blockInput: boolean;
+ readonly duration: number;
+ readonly easing: EasingFunction;
+ readonly direction: 'out' | 'in';
+ private _completeFuture = new Future();
+
+ // State needs to be reset between uses
+ public started = false;
+ private _currentDistance: number = 0;
+ private _currentProgress: number = 0;
+
+ public done = this._completeFuture.promise;
+
+ /**
+ * Returns a number between [0, 1] indicating what state the transition is in.
+ *
+ * * For 'out' direction transitions start at 0 and end at 1
+ * * For 'in' direction transitions start at 1 and end at 0
+ */
+ get progress(): number {
+ return this._currentProgress;
+ }
+
+ get complete(): boolean {
+ if (this.direction === 'out') {
+ return this.progress >= 1;
+ } else {
+ return this.progress <= 0;
+ }
+ }
+
+ constructor(options: TransitionOptions) {
+ super();
+ this.name = `Transition#${this.id}`;
+ this.duration = options.duration;
+ this.easing = options.easing ?? EasingFunctions.Linear;
+ this.direction = options.direction ?? 'out';
+ this.hideLoader = options.hideLoader ?? false;
+ this.blockInput = options.blockInput ?? false;
+ this.transform.coordPlane = CoordPlane.Screen;
+ this.transform.pos = Vector.Zero;
+ this.transform.z = Infinity; // Transitions sit on top of everything
+ this.graphics.anchor = Vector.Zero;
+ this.addComponent(this.transform);
+ this.addComponent(this.graphics);
+
+ if (this.direction === 'out') {
+ this._currentProgress = 0;
+ } else {
+ this._currentProgress = 1;
+ }
+ }
+
+ /**
+ * Overridable lifecycle method, called before each update.
+ *
+ * **WARNING BE SURE** to call `super.onPreUpdate()` if overriding in your own custom implementation
+ * @param engine
+ * @param delta
+ */
+ public override onPreUpdate(engine: Engine, delta: number): void {
+ if (this.complete) {
+ return;
+ }
+
+ this._currentDistance += clamp(delta / this.duration, 0, 1);
+ if (this._currentDistance >= 1) {
+ this._currentDistance = 1;
+ }
+
+ if (this.direction === 'out') {
+ this._currentProgress = clamp(this.easing(this._currentDistance, 0, 1, 1), 0, 1);
+ } else {
+ this._currentProgress = clamp(this.easing(this._currentDistance, 1, 0, 1), 0, 1);
+ }
+ }
+
+ /**
+ * Overridable lifecycle method, called right before the previous scene has deactivated.
+ *
+ * This gives incoming transition a chance to grab info from previous scene if desired
+ * @param scene
+ */
+ async onPreviousSceneDeactivate(scene: Scene) {
+ // override me
+ }
+
+ /**
+ * Overridable lifecycle method, called once at the beginning of the transition
+ *
+ * `progress` is given between 0 and 1
+ * @param progress
+ */
+ onStart(progress: number) {
+ // override me
+ }
+
+ /**
+ * Overridable lifecycle method, called every frame of the transition
+ *
+ * `progress` is given between 0 and 1
+ * @param progress
+ */
+ onUpdate(progress: number) {
+ // override me
+ }
+
+ /**
+ * Overridable lifecycle method, called at the end of the transition,
+ *
+ * `progress` is given between 0 and 1
+ * @param progress
+ */
+ onEnd(progress: number) {
+ // override me
+ }
+
+ /**
+ * Overridable lifecycle method, called when the transition is reset
+ *
+ * Use this to override and provide your own reset logic for internal state in custom transition implementations
+ */
+ onReset() {
+ // override me
+ }
+
+ /**
+ * reset() is called by the engine to reset transitions
+ */
+ reset() {
+ this.started = false;
+ this._completeFuture = new Future();
+ this.done = this._completeFuture.promise;
+ this._currentDistance = 0;
+ if (this.direction === 'out') {
+ this._currentProgress = 0;
+ } else {
+ this._currentProgress = 1;
+ }
+ this.onReset();
+ }
+
+ /**
+ * execute() is called by the engine every frame to update the Transition lifecycle onStart/onUpdate/onEnd
+ */
+ execute() {
+ if (!this.isInitialized) {
+ return;
+ }
+
+ if (!this.started) {
+ this.started = true;
+ this.onStart(this.progress);
+ }
+
+ this.onUpdate(this.progress);
+
+ if (this.complete && !this._completeFuture.isCompleted) {
+ this.onEnd(this.progress);
+ this._completeFuture.resolve();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/engine/Director/index.ts b/src/engine/Director/index.ts
new file mode 100644
index 000000000..938301f24
--- /dev/null
+++ b/src/engine/Director/index.ts
@@ -0,0 +1,7 @@
+
+export * from './Transition';
+export * from './FadeInOut';
+export * from './CrossFade';
+export * from './Director';
+export * from './Loader';
+export * from './DefaultLoader';
\ No newline at end of file
diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts
index 1ef850d6a..347032929 100644
--- a/src/engine/Engine.ts
+++ b/src/engine/Engine.ts
@@ -1,21 +1,20 @@
import { EX_VERSION } from './';
+import { obsolete } from './Util/Decorators';
+import { Future } from './Util/Future';
import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter';
-import { Gamepads } from './Input/Gamepad';
-import { Keyboard } from './Input/Keyboard';
import { PointerScope } from './Input/PointerScope';
-import { EngineInput } from './Input/EngineInput';
import { Flags } from './Flags';
import { polyfill } from './Polyfill';
polyfill();
import { CanUpdate, CanDraw, CanInitialize } from './Interfaces/LifecycleEvents';
-import { Loadable } from './Interfaces/Loadable';
import { Vector } from './Math/vector';
import { Screen, DisplayMode, ScreenDimension, Resolution } from './Screen';
import { ScreenElement } from './ScreenElement';
import { Actor } from './Actor';
import { Timer } from './Timer';
import { TileMap } from './TileMap';
-import { Loader } from './Loader';
+import { DefaultLoader } from './Director/DefaultLoader';
+import { Loader } from './Director/Loader';
import { Detector } from './Util/Detector';
import {
VisibleEvent,
@@ -26,25 +25,24 @@ import {
PostUpdateEvent,
PreFrameEvent,
PostFrameEvent,
- DeactivateEvent,
- ActivateEvent,
PreDrawEvent,
PostDrawEvent,
InitializeEvent
} from './Events';
import { Logger, LogLevel } from './Util/Log';
import { Color } from './Color';
-import { Scene } from './Scene';
+import { Scene, SceneConstructor, isSceneConstructor } from './Scene';
import { Entity } from './EntityComponentSystem/Entity';
import { Debug, DebugStats } from './Debug/Debug';
import { BrowserEvents } from './Util/Browser';
import { ExcaliburGraphicsContext, ExcaliburGraphicsContext2DCanvas, ExcaliburGraphicsContextWebGL, TextureLoader } from './Graphics';
-import { PointerEventReceiver } from './Input/PointerEventReceiver';
import { Clock, StandardClock } from './Util/Clock';
import { ImageFiltering } from './Graphics/Filtering';
import { GraphicsDiagnostics } from './Graphics/GraphicsDiagnostics';
import { Toaster } from './Util/Toaster';
import { InputMapper } from './Input/InputMapper';
+import { GoToOptions, SceneMap, Director, StartOptions, SceneWithOptions, WithRoot } from './Director/Director';
+import { InputHost } from './Input/InputHost';
export type EngineEvents = {
fallbackgraphicscontext: ExcaliburGraphicsContext2DCanvas,
@@ -97,7 +95,7 @@ export enum ScrollPreventionMode {
/**
* Defines the available options to configure the Excalibur engine at constructor time.
*/
-export interface EngineOptions {
+export interface EngineOptions {
/**
* Optionally configure the width of the viewport in css pixels
*/
@@ -268,7 +266,14 @@ export interface EngineOptions {
* Canvas renderer.
*/
threshold?: { numberOfFrames: number, fps: number };
- }
+ },
+
+ /**
+ * Optionally specify scenes with their transitions and loaders to excalibur's scene [[Director]]
+ *
+ * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goto`
+ */
+ scenes?: SceneMap
}
/**
@@ -278,7 +283,7 @@ export interface EngineOptions {
* starting/stopping the game, maintaining state, transmitting events,
* loading resources, and managing the scene.
*/
-export class Engine implements CanInitialize, CanUpdate, CanDraw {
+export class Engine implements CanInitialize, CanUpdate, CanDraw {
/**
* Current Excalibur version string
*
@@ -301,6 +306,11 @@ export class Engine implements CanInitialize, CanUpdate, CanDraw {
*/
public screen: Screen;
+ /**
+ * Scene director, manages all scenes, scene transitions, and loaders in excalibur
+ */
+ public director: Director;
+
/**
* Direct access to the engine's canvas element
*/
@@ -410,13 +420,15 @@ export class Engine implements CanInitialize, CanUpdate, CanDraw {
/**
* Access engine input like pointer, keyboard, or gamepad
*/
- public input: EngineInput;
+ public input: InputHost;
/**
* Map multiple input sources to specific game actions actions
*/
public inputMapper: InputMapper;
+ private _inputEnabled: boolean = true;
+
/**
* Access Excalibur debugging functionality.
*
@@ -437,17 +449,23 @@ export class Engine implements CanInitialize, CanUpdate, CanDraw {
/**
* The current [[Scene]] being drawn and updated on screen
*/
- public currentScene: Scene;
+ public get currentScene(): Scene {
+ return this.director.currentScene;
+ }
/**
- * The default [[Scene]] of the game, use [[Engine.goToScene]] to transition to different scenes.
+ * The default [[Scene]] of the game, use [[Engine.goto]] to transition to different scenes.
*/
- public readonly rootScene: Scene;
+ public get rootScene(): Scene {
+ return this.director.rootScene;
+ }
/**
* Contains all the scenes currently registered with Excalibur
*/
- public readonly scenes: { [key: string]: Scene } = {};
+ public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } {
+ return this.director.scenes;
+ };
/**
* Indicates whether the engine is set to fullscreen or not
@@ -509,7 +527,7 @@ export class Engine implements CanInitialize, CanUpdate, CanDraw {
* The action to take when a fatal exception is thrown
*/
public onFatalException = (e: any) => {
- Logger.getInstance().fatal(e);
+ Logger.getInstance().fatal(e, e.stack);
};
/**
@@ -527,12 +545,10 @@ export class Engine implements CanInitialize, CanUpdate, CanDraw {
private _timescale: number = 1.0;
// loading
- private _loader: Loader;
+ private _loader: DefaultLoader;
private _isInitialized: boolean = false;
- private _deferredGoTo: string = null;
-
public emit>(eventName: TEventName, event: EngineEvents[TEventName]): void;
public emit(eventName: string, event?: any): void;
public emit | string>(eventName: TEventName, event?: any): void {
@@ -611,7 +627,7 @@ export class Engine implements CanInitialize, CanUpdate, CanDraw {
* });
* ```
*/
- constructor(options?: EngineOptions) {
+ constructor(options?: EngineOptions) {
options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options };
this._originalOptions = options;
@@ -748,6 +764,7 @@ O|===|* >________________>\n\
pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? null)
});
+ // TODO REMOVE STATIC!!!
// Set default filtering based on antialiasing
TextureLoader.filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel;
@@ -766,15 +783,12 @@ O|===|* >________________>\n\
this.enableCanvasTransparency = options.enableCanvasTransparency;
- this._loader = new Loader();
- this._loader.wireEngine(this);
this.debug = new Debug(this);
- this._initialize(options);
+ this.director = new Director(this, options.scenes);
- this.rootScene = this.currentScene = new Scene();
+ this._initialize(options);
- this.addScene('root', this.rootScene);
(window as any).___EXCALIBUR_DEVTOOL = this;
}
@@ -929,18 +943,16 @@ O|===|* >________________>\n\
* @param key The name of the scene, must be unique
* @param scene The scene to add to the engine
*/
- public addScene(key: string, scene: Scene) {
- if (this.scenes[key]) {
- this._logger.warn('Scene', key, 'already exists overwriting');
- }
- this.scenes[key] = scene;
+ public addScene(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine {
+ this.director.add(key, scene);
+ return this as Engine;
}
/**
* Removes a [[Scene]] instance from the engine
* @param scene The scene to remove
*/
- public removeScene(scene: Scene): void;
+ public removeScene(scene: Scene | SceneConstructor): void;
/**
* Removes a scene from the engine by key
* @param key The scene key to remove
@@ -950,21 +962,7 @@ O|===|* >________________>\n\
* @internal
*/
public removeScene(entity: any): void {
- if (entity instanceof Scene) {
- // remove scene
- for (const key in this.scenes) {
- if (this.scenes.hasOwnProperty(key)) {
- if (this.scenes[key] === entity) {
- delete this.scenes[key];
- }
- }
- }
- }
-
- if (typeof entity === 'string') {
- // remove scene
- delete this.scenes[entity];
- }
+ this.director.remove(entity);
}
/**
@@ -973,7 +971,7 @@ O|===|* >________________>\n\
* @param sceneKey The key of the scene, must be unique
* @param scene The scene to add to the engine
*/
- public add(sceneKey: string, scene: Scene): void;
+ public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void;
/**
* Adds a [[Timer]] to the [[currentScene]].
* @param timer The timer to add to the [[currentScene]].
@@ -1005,11 +1003,12 @@ O|===|* >________________>\n\
public add(screenElement: ScreenElement): void;
public add(entity: any): void {
if (arguments.length === 2) {
- this.addScene(arguments[0], arguments[1]);
+ this.director.add(arguments[0], arguments[1]);
return;
}
- if (this._deferredGoTo && this.scenes[this._deferredGoTo]) {
- this.scenes[this._deferredGoTo].add(entity);
+ const maybeDeferred = this.director.getDeferredScene();
+ if (maybeDeferred instanceof Scene) {
+ maybeDeferred.add(entity);
} else {
this.currentScene.add(entity);
}
@@ -1019,7 +1018,7 @@ O|===|* >________________>\n\
* Removes a scene instance from the engine
* @param scene The scene to remove
*/
- public remove(scene: Scene): void;
+ public remove(scene: Scene | SceneConstructor): void;
/**
* Removes a scene from the engine by key
* @param sceneKey The scene to remove
@@ -1051,7 +1050,7 @@ O|===|* >________________>\n\
this.currentScene.remove(entity);
}
- if (entity instanceof Scene) {
+ if (entity instanceof Scene || isSceneConstructor(entity)) {
this.removeScene(entity);
}
@@ -1060,45 +1059,50 @@ O|===|* >________________>\n\
}
}
+ /**
+ * Changes the current scene with optionally supplied:
+ * * Activation data
+ * * Transitions
+ * * Loaders
+ *
+ * Example:
+ * ```typescript
+ * game.goto('myScene', {
+ * sceneActivationData: {any: 'thing at all'},
+ * destinationIn: new FadeInOut({duration: 1000, direction: 'in'}),
+ * sourceOut: new FadeInOut({duration: 1000, direction: 'out'}),
+ * loader: MyLoader
+ * });
+ * ```
+ *
+ * Scenes are defined in the Engine constructor
+ * ```typescript
+ * const engine = new ex.Engine({
+ scenes: {...}
+ });
+ * ```
+ * Or by adding dynamically
+ *
+ * ```typescript
+ * engine.addScene('myScene', new ex.Scene());
+ * ```
+ * @param destinationScene
+ * @param options
+ */
+ public async goto(destinationScene: WithRoot, options?: GoToOptions) {
+ await this.director.goto(destinationScene, options);
+ }
+
/**
* Changes the currently updating and drawing scene to a different,
* named scene. Calls the [[Scene]] lifecycle events.
* @param key The key of the scene to transition to.
* @param data Optional data to send to the scene's onActivate method
+ * @deprecated Use [[Engine.goto]] will be removed in v1!
*/
- public goToScene(key: string, data?: TData): void {
- // if not yet initialized defer goToScene
- if (!this.isInitialized) {
- this._deferredGoTo = key;
- return;
- }
-
- if (this.scenes[key]) {
- const previousScene = this.currentScene;
- const nextScene = this.scenes[key];
-
- this._logger.debug('Going to scene:', key);
-
- // only deactivate when initialized
- if (this.currentScene.isInitialized) {
- const context = { engine: this, previousScene, nextScene };
- this.currentScene._deactivate.apply(this.currentScene, [context, nextScene]);
- this.currentScene.events.emit('deactivate', new DeactivateEvent(context, this.currentScene));
- }
-
- // set current scene to new one
- this.currentScene = nextScene;
- this.screen.setCurrentCamera(nextScene.camera);
-
- // initialize the current scene if has not been already
- this.currentScene._initialize(this);
-
- const context = { engine: this, previousScene, nextScene, data };
- this.currentScene._activate.apply(this.currentScene, [context, nextScene]);
- this.currentScene.events.emit('activate', new ActivateEvent(context, this.currentScene));
- } else {
- this._logger.error('Scene', key, 'does not exist!');
- }
+ @obsolete({message: 'Engine.goToScene is deprecated, will be removed in v1', alternateMethod: 'Engine.goto'})
+ public async goToScene(key: string, data?: TData): Promise {
+ await this.director.swapScene(key, data);
}
/**
@@ -1125,19 +1129,13 @@ O|===|* >________________>\n\
// initialize inputs
const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
- this.input = {
- keyboard: new Keyboard(),
- pointers: new PointerEventReceiver(pointerTarget, this),
- gamepads: new Gamepads()
- };
- this.input.keyboard.init({
- grabWindowFocus: this._originalOptions?.grabWindowFocus ?? true
- });
- this.input.pointers.init({
- grabWindowFocus: this._originalOptions?.grabWindowFocus ?? true
+ const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true;
+ this.input = new InputHost({
+ pointerTarget,
+ grabWindowFocus,
+ engine: this
});
- this.input.gamepads.init();
- this.inputMapper = new InputMapper(this.input);
+ this.inputMapper = this.input.inputMapper;
// Issue #385 make use of the visibility api
// https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API
@@ -1157,6 +1155,11 @@ O|===|* >________________>\n\
}
}
+ public toggleInputEnabled(enabled: boolean) {
+ this._inputEnabled = enabled;
+ this.input.toggleEnabled(this._inputEnabled);
+ }
+
public onInitialize(engine: Engine) {
// Override me
}
@@ -1185,18 +1188,12 @@ O|===|* >________________>\n\
return this._isInitialized;
}
- private _overrideInitialize(engine: Engine) {
+ private async _overrideInitialize(engine: Engine) {
if (!this.isInitialized) {
- this.onInitialize(engine);
+ await this.director.onInitialize();
+ await this.onInitialize(engine);
this.events.emit('initialize', new InitializeEvent(engine, this));
this._isInitialized = true;
- if (this._deferredGoTo) {
- const deferredScene = this._deferredGoTo;
- this._deferredGoTo = null;
- this.goToScene(deferredScene);
- } else {
- this.goToScene('root');
- }
}
}
@@ -1205,17 +1202,15 @@ O|===|* >________________>\n\
* @param delta Number of milliseconds elapsed since the last update.
*/
private _update(delta: number) {
- if (!this.ready) {
+ this.director.update();
+ if (this._isLoading) {
// suspend updates until loading is finished
- this._loader.update(this, delta);
+ this._loader?.onUpdate(this, delta);
// Update input listeners
- this.inputMapper.execute();
- this.input.keyboard.update();
- this.input.gamepads.update();
+ this.input.update();
return;
}
-
// Publish preupdate events
this._preupdate(delta);
@@ -1229,9 +1224,7 @@ O|===|* >________________>\n\
this._postupdate(delta);
// Update input listeners
- this.inputMapper.execute();
- this.input.keyboard.update();
- this.input.gamepads.update();
+ this.input.update();
}
/**
@@ -1268,9 +1261,12 @@ O|===|* >________________>\n\
this._predraw(this.graphicsContext, delta);
// Drawing nothing else while loading
- if (!this._isReady) {
- this._loader.canvas.draw(this.graphicsContext, 0, 0);
- this.graphicsContext.flush();
+ if (this._isLoading) {
+ if (!this._hideLoader) {
+ this._loader?.canvas.draw(this.graphicsContext, 0, 0);
+ this.graphicsContext.flush();
+ this.graphicsContext.endDrawLifecycle();
+ }
return;
}
@@ -1328,80 +1324,73 @@ O|===|* >________________>\n\
return this._isDebug;
}
- private _loadingComplete: boolean = false;
-
/**
* Returns true when loading is totally complete and the player has clicked start
*/
public get loadingComplete() {
- return this._loadingComplete;
+ return !this._isLoading;
}
- private _isReady = false;
+ private _isLoading = false;
+ private _hideLoader = false;
+ private _isReadyFuture = new Future();
public get ready() {
- return this._isReady;
+ return this._isReadyFuture.isCompleted;
}
- private _isReadyResolve: () => any;
- private _isReadyPromise = new Promise(resolve => {
- this._isReadyResolve = resolve;
- });
public isReady(): Promise {
- return this._isReadyPromise;
+ return this._isReadyFuture.promise;
}
+
/**
* Starts the internal game loop for Excalibur after loading
* any provided assets.
- * @param loader Optional [[Loader]] to use to load resources. The default loader is [[Loader]], override to provide your own
- * custom loader.
+ * @param loader Optional [[Loader]] to use to load resources. The default loader is [[Loader]],
+ * override to provide your own custom loader.
*
* Note: start() only resolves AFTER the user has clicked the play button
*/
- public async start(loader?: Loader): Promise {
+ public async start(loader?: DefaultLoader): Promise;
+ /**
+ * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions
+ * @param startOptions Optional [[StartOptions]] to configure the routes for scenes in Excalibur
+ *
+ * Note: start() only resolves AFTER the user has clicked the play button
+ */
+ public async start(sceneName: WithRoot, options?: StartOptions): Promise;
+ /**
+ * Starts the internal game loop after any loader is finished
+ * @param loader
+ */
+ public async start(loader?: DefaultLoader): Promise;
+ public async start(sceneNameOrLoader?: WithRoot | DefaultLoader, options?: StartOptions): Promise {
if (!this._compatible) {
throw new Error('Excalibur is incompatible with your browser');
}
-
- // Wire loader if we have it
- if (loader) {
- // Push the current user entered resolution/viewport
- this.screen.pushResolutionAndViewport();
-
- // Configure resolution for loader, it expects resolution === viewport
- this.screen.resolution = this.screen.viewport;
- this.screen.applyResolutionAndViewport();
- this._loader = loader;
- this._loader.suppressPlayButton = this._suppressPlayButton || this._loader.suppressPlayButton;
- this._loader.wireEngine(this);
+ this._isLoading = true;
+ let loader: DefaultLoader;
+ if (sceneNameOrLoader instanceof DefaultLoader) {
+ loader = sceneNameOrLoader;
+ } else if (typeof sceneNameOrLoader === 'string') {
+ this.director.configureStart(sceneNameOrLoader, options);
+ loader = this.director.mainLoader;
}
// Start the excalibur clock which drives the mainloop
- // has started is a slight misnomer, it's really mainloop started
this._logger.debug('Starting game clock...');
this.browser.resume();
this.clock.start();
this._logger.debug('Game clock started');
- if (loader) {
- await this.load(this._loader);
- this._loadingComplete = true;
-
- // reset back to previous user resolution/viewport
- this.screen.popResolutionAndViewport();
- this.screen.applyResolutionAndViewport();
- }
-
- this._loadingComplete = true;
+ await this.load(loader ?? new Loader());
// Initialize before ready
- this._overrideInitialize(this);
+ await this._overrideInitialize(this);
- this._isReady = true;
-
- this._isReadyResolve();
+ this._isReadyFuture.resolve();
this.emit('start', new GameStartEvent(this));
- return this._isReadyPromise;
+ return this._isReadyFuture.promise;
}
/**
@@ -1518,12 +1507,29 @@ O|===|* >________________>\n\
* will appear.
* @param loader Some [[Loadable]] such as a [[Loader]] collection, [[Sound]], or [[Texture]].
*/
- public async load(loader: Loadable): Promise {
+ public async load(loader: DefaultLoader, hideLoader = false): Promise {
try {
+ // early exit if loaded
+ if (loader.isLoaded()) {
+ return;
+ }
+ this._loader = loader;
+ this._isLoading = true;
+ this._hideLoader = hideLoader;
+
+ if (loader instanceof Loader) {
+ loader.suppressPlayButton = this._suppressPlayButton;
+ }
+ this._loader.onInitialize(this);
+
await loader.load();
} catch (e) {
this._logger.error('Error loading resources, things may not behave properly', e);
await Promise.resolve();
+ } finally {
+ this._isLoading = false;
+ this._hideLoader = false;
+ this._loader = null;
}
}
}
\ No newline at end of file
diff --git a/src/engine/EntityComponentSystem/Entity.ts b/src/engine/EntityComponentSystem/Entity.ts
index 79d5a47e2..470ae9772 100644
--- a/src/engine/EntityComponentSystem/Entity.ts
+++ b/src/engine/EntityComponentSystem/Entity.ts
@@ -4,7 +4,10 @@ import { Observable, Message } from '../Util/Observable';
import { OnInitialize, OnPreUpdate, OnPostUpdate } from '../Interfaces/LifecycleEvents';
import { Engine } from '../Engine';
import { InitializeEvent, PreUpdateEvent, PostUpdateEvent } from '../Events';
-import { EventEmitter, EventKey, Handler, Scene, Subscription, Util } from '..';
+import { KillEvent } from '../Events';
+import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter';
+import { Scene } from '../Scene';
+import { removeItemFromArray } from '../Util/Util';
/**
* Interface holding an entity component pair
@@ -51,12 +54,14 @@ export type EntityEvents = {
'initialize': InitializeEvent;
'preupdate': PreUpdateEvent;
'postupdate': PostUpdateEvent;
+ 'kill': KillEvent
};
export const EntityEvents = {
Initialize: 'initialize',
PreUpdate: 'preupdate',
- PostUpdate: 'postupdate'
+ PostUpdate: 'postupdate',
+ Kill: 'kill'
} as const;
/**
@@ -127,6 +132,7 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate {
this.active = false;
this.unparent();
}
+ this.emit('kill', new KillEvent(this));
}
public isKilled() {
@@ -268,7 +274,7 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate {
*/
public removeChild(entity: Entity): Entity {
if (entity.parent === this) {
- Util.removeItemFromArray(entity, this._children);
+ removeItemFromArray(entity, this._children);
entity._parent = null;
this.childrenRemoved$.notifyAll(entity);
}
diff --git a/src/engine/EntityComponentSystem/EntityManager.ts b/src/engine/EntityComponentSystem/EntityManager.ts
index 6a5803ef6..ee99d0478 100644
--- a/src/engine/EntityComponentSystem/EntityManager.ts
+++ b/src/engine/EntityComponentSystem/EntityManager.ts
@@ -1,7 +1,7 @@
import { Entity, RemovedComponent, AddedComponent, isAddedComponent, isRemovedComponent } from './Entity';
import { Observer } from '../Util/Observable';
import { World } from './World';
-import { Util } from '..';
+import { removeItemFromArray } from '../Util/Util';
// Add/Remove entities and components
@@ -102,7 +102,7 @@ export class EntityManager implements Observer extends Observable extends Observable {
new (...args: any[]): T;
@@ -53,7 +54,7 @@ export class SystemManager {
* @param system
*/
public removeSystem(system: System) {
- Util.removeItemFromArray(system, this.systems);
+ removeItemFromArray(system, this.systems);
const query = this._world.queryManager.getQuery(system.types);
if (query) {
query.unregister(system);
diff --git a/src/engine/Events.ts b/src/engine/Events.ts
index f25615789..d6fa1cf57 100644
--- a/src/engine/Events.ts
+++ b/src/engine/Events.ts
@@ -199,8 +199,8 @@ export class GameEvent {
/**
* The 'kill' event is emitted on actors when it is killed. The target is the actor that was killed.
*/
-export class KillEvent extends GameEvent {
- constructor(public target: Actor) {
+export class KillEvent extends GameEvent {
+ constructor(public target: Entity) {
super();
}
}
diff --git a/src/engine/Events/PointerEvents.ts b/src/engine/Events/PointerEvents.ts
index 810ba31c9..3b61eb7c4 100644
--- a/src/engine/Events/PointerEvents.ts
+++ b/src/engine/Events/PointerEvents.ts
@@ -1,4 +1,4 @@
-import { GlobalCoordinates } from '..';
+import { GlobalCoordinates } from '../Math/global-coordinates';
import { WheelDeltaMode } from '../Input/WheelDeltaMode';
import { ExEvent } from './ExEvent';
diff --git a/src/engine/Graphics/Circle.ts b/src/engine/Graphics/Circle.ts
index 3fe632f35..d920d81aa 100644
--- a/src/engine/Graphics/Circle.ts
+++ b/src/engine/Graphics/Circle.ts
@@ -1,4 +1,4 @@
-import { ImageFiltering } from '.';
+import { ImageFiltering } from './Filtering';
import { Raster, RasterOptions } from './Raster';
export interface CircleOptions {
diff --git a/src/engine/Graphics/Context/debug-text.ts b/src/engine/Graphics/Context/debug-text.ts
index 32b1bfb90..a11fa6ac6 100644
--- a/src/engine/Graphics/Context/debug-text.ts
+++ b/src/engine/Graphics/Context/debug-text.ts
@@ -1,5 +1,8 @@
-import { ExcaliburGraphicsContext, ImageSource, SpriteFont, SpriteSheet } from '..';
-import { Vector } from '../..';
+import { ExcaliburGraphicsContext } from '../Context/ExcaliburGraphicsContext';
+import { ImageSource } from '../ImageSource';
+import { SpriteFont } from '../SpriteFont';
+import { SpriteSheet } from '../SpriteSheet';
+import { Vector } from '../../Math/vector';
import debugFont from './debug-font.png';
/**
@@ -7,6 +10,8 @@ import debugFont from './debug-font.png';
*/
export class DebugText {
constructor() {
+ // We fire and forget, we don't care if it's loaded or not
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
}
diff --git a/src/engine/Graphics/DebugGraphicsComponent.ts b/src/engine/Graphics/DebugGraphicsComponent.ts
index 5201e8ce9..2d54f1a65 100644
--- a/src/engine/Graphics/DebugGraphicsComponent.ts
+++ b/src/engine/Graphics/DebugGraphicsComponent.ts
@@ -1,4 +1,4 @@
-import { ExcaliburGraphicsContext } from '.';
+import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
import { Debug } from '../Debug';
import { Component } from '../EntityComponentSystem/Component';
diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts
index a6c48eb00..bb9e597c8 100644
--- a/src/engine/Graphics/GraphicsSystem.ts
+++ b/src/engine/Graphics/GraphicsSystem.ts
@@ -7,8 +7,8 @@ import { Entity } from '../EntityComponentSystem/Entity';
import { Camera } from '../Camera';
import { AddedEntity, isAddedSystemEntity, RemovedEntity, System, SystemType } from '../EntityComponentSystem';
import { Engine } from '../Engine';
-import { GraphicsGroup } from '.';
-import { Particle } from '../Particles';
+import { GraphicsGroup } from './GraphicsGroup';
+import { Particle } from '../Particles'; // this import seems to bomb wallaby
import { ParallaxComponent } from './ParallaxComponent';
import { CoordPlane } from '../Math/coord-plane';
import { BodyComponent } from '../Collision/BodyComponent';
@@ -138,6 +138,7 @@ export class GraphicsSystem extends System {
private _logger = Logger.getInstance();
private _resource: Resource;
@@ -66,6 +71,27 @@ export class ImageSource implements Loadable {
}
}
+ /**
+ * Create an ImageSource from and HTML tag element
+ * @param image
+ */
+ static fromHtmlImageElement(image: HTMLImageElement, options?: ImageSourceOptions) {
+ const imageSource = new ImageSource('');
+ imageSource._src = 'image-element';
+ imageSource.data = image;
+ imageSource.data.setAttribute('data-original-src', 'image-element');
+
+ if (options?.filtering) {
+ imageSource.data.setAttribute('filtering', options?.filtering);
+ } else {
+ imageSource.data.setAttribute('filtering', ImageFiltering.Blended);
+ }
+
+ TextureLoader.checkImageSizeSupportedAndLog(image);
+ imageSource._readyFuture.resolve(image);
+ return imageSource;
+ }
+
/**
* Should excalibur add a cache busting querystring? By default false.
* Must be set before loading
diff --git a/src/engine/Graphics/Polygon.ts b/src/engine/Graphics/Polygon.ts
index e9c065fdd..248936eb3 100644
--- a/src/engine/Graphics/Polygon.ts
+++ b/src/engine/Graphics/Polygon.ts
@@ -1,4 +1,4 @@
-import { ImageFiltering } from '.';
+import { ImageFiltering } from './Filtering';
import { Vector, vec } from '../Math/vector';
import { Raster, RasterOptions } from './Raster';
diff --git a/src/engine/Graphics/PostProcessor/PostProcessor.ts b/src/engine/Graphics/PostProcessor/PostProcessor.ts
index 626503217..85a0359cd 100644
--- a/src/engine/Graphics/PostProcessor/PostProcessor.ts
+++ b/src/engine/Graphics/PostProcessor/PostProcessor.ts
@@ -1,5 +1,5 @@
-import { VertexLayout } from '..';
+import { VertexLayout } from '../Context/vertex-layout';
import { Shader } from '../Context/shader';
diff --git a/src/engine/Graphics/Sprite.ts b/src/engine/Graphics/Sprite.ts
index 3f4210cb2..26c5d5703 100644
--- a/src/engine/Graphics/Sprite.ts
+++ b/src/engine/Graphics/Sprite.ts
@@ -41,6 +41,8 @@ export class Sprite extends Graphic {
this.sourceView = options.sourceView ?? { x: 0, y: 0, width: width ?? 0, height: height ?? 0 };
this.destSize = options.destSize ?? { width: width ?? 0, height: height ?? 0 };
this._updateSpriteDimensions();
+ // Fire when loaded
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.image.ready.then(() => {
this._updateSpriteDimensions();
});
diff --git a/src/engine/Graphics/SpriteFont.ts b/src/engine/Graphics/SpriteFont.ts
index c8ff093d8..605e887d5 100644
--- a/src/engine/Graphics/SpriteFont.ts
+++ b/src/engine/Graphics/SpriteFont.ts
@@ -5,7 +5,8 @@ import { FontRenderer } from './FontCommon';
import { Graphic, GraphicOptions } from './Graphic';
import { Sprite } from './Sprite';
import { SpriteSheet } from './SpriteSheet';
-import { BoundingBox, Color } from '..';
+import { BoundingBox } from '../Collision/BoundingBox';
+import { Color } from '../Color';
export interface SpriteFontOptions {
/**
diff --git a/src/engine/Input/EngineInput.ts b/src/engine/Input/EngineInput.ts
deleted file mode 100644
index 3972d1684..000000000
--- a/src/engine/Input/EngineInput.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Keyboard } from './Keyboard';
-import { Gamepads } from './Gamepad';
-import { PointerEventReceiver } from './PointerEventReceiver';
-
-export interface EngineInput {
- keyboard: Keyboard;
- pointers: PointerEventReceiver;
- gamepads: Gamepads;
-}
diff --git a/src/engine/Input/Gamepad.ts b/src/engine/Input/Gamepad.ts
index bbe1b5b14..585fbe63f 100644
--- a/src/engine/Input/Gamepad.ts
+++ b/src/engine/Input/Gamepad.ts
@@ -42,6 +42,7 @@ export class Gamepads {
private _initSuccess: boolean = false;
private _navigator: NavigatorGamepads = navigator;
private _minimumConfiguration: GamepadConfiguration = null;
+ private _enabled = true;
public init() {
if (!this.supported) {
@@ -59,6 +60,10 @@ export class Gamepads {
}
}
+ public toggleEnabled(enabled: boolean) {
+ this._enabled = enabled;
+ }
+
/**
* Sets the minimum gamepad configuration, for example {axis: 4, buttons: 4} means
* this game requires at minimum 4 axis inputs and 4 buttons, this is not restrictive
@@ -134,6 +139,9 @@ export class Gamepads {
if (!this.enabled || !this.supported) {
return;
}
+ if (!this._enabled) {
+ return;
+ }
this.init();
const gamepads = this._navigator.getGamepads();
diff --git a/src/engine/Input/Index.ts b/src/engine/Input/Index.ts
index f1df9e26e..65a1af251 100644
--- a/src/engine/Input/Index.ts
+++ b/src/engine/Input/Index.ts
@@ -48,12 +48,6 @@ export {
CapturePointerConfig
} from './CapturePointerConfig';
-export {
- /**
- * @deprecated ex.Input.EngineInput import site will be removed in v0.29.0, use ex.EngineInput
- */
- EngineInput
-} from './EngineInput';
export {
/**
diff --git a/src/engine/Input/InputHost.ts b/src/engine/Input/InputHost.ts
new file mode 100644
index 000000000..39f66ecf8
--- /dev/null
+++ b/src/engine/Input/InputHost.ts
@@ -0,0 +1,51 @@
+import { Engine } from '../Engine';
+import { Gamepads } from './Gamepad';
+import { InputMapper } from './InputMapper';
+import { Keyboard } from './Keyboard';
+import { PointerEventReceiver } from './PointerEventReceiver';
+
+export interface InputHostOptions {
+ pointerTarget: Document | HTMLCanvasElement
+ grabWindowFocus: boolean,
+ engine: Engine
+}
+
+export class InputHost {
+ private _enabled = true;
+
+ keyboard: Keyboard;
+ pointers: PointerEventReceiver;
+ gamepads: Gamepads;
+ inputMapper: InputMapper;
+
+ constructor(options: InputHostOptions) {
+ const { pointerTarget, grabWindowFocus, engine } = options;
+ this.keyboard = new Keyboard();
+ this.pointers = new PointerEventReceiver(pointerTarget, engine);
+ this.gamepads = new Gamepads();
+
+ this.keyboard.init({grabWindowFocus});
+ this.pointers.init({grabWindowFocus});
+ this.gamepads.init();
+ this.inputMapper = new InputMapper({
+ keyboard: this.keyboard,
+ pointers: this.pointers,
+ gamepads: this.gamepads
+ });
+ }
+
+ toggleEnabled(enabled: boolean) {
+ this._enabled = enabled;
+ this.keyboard.toggleEnabled(this._enabled);
+ this.pointers.toggleEnabled(this._enabled);
+ this.gamepads.toggleEnabled(this._enabled);
+ }
+
+ update() {
+ if (this._enabled) {
+ this.inputMapper.execute();
+ this.keyboard.update();
+ this.gamepads.update();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/engine/Input/Keyboard.ts b/src/engine/Input/Keyboard.ts
index f6e856f14..8064a7866 100644
--- a/src/engine/Input/Keyboard.ts
+++ b/src/engine/Input/Keyboard.ts
@@ -212,6 +212,7 @@ export const KeyEvents = {
*/
export class Keyboard {
public events = new EventEmitter();
+ private _enabled = true;
/**
* Keys that are currently held down
*/
@@ -283,6 +284,10 @@ export class Keyboard {
global.addEventListener('keydown', this._handleKeyDown);
}
+ toggleEnabled(enabled: boolean) {
+ this._enabled = enabled;
+ }
+
private _releaseAllKeys = (ev: KeyboardEvent) => {
for (const code of this._keys) {
const keyEvent = new KeyEvent(code, ev.key, ev);
@@ -294,6 +299,10 @@ export class Keyboard {
};
private _handleKeyDown = (ev: KeyboardEvent) => {
+ if (!this._enabled) {
+ return;
+ }
+
// handle macos meta key issue
// https://github.com/excaliburjs/Excalibur/issues/2608
if (!ev.metaKey && (this._keys.includes(Keys.MetaLeft) || this._keys.includes(Keys.MetaRight))) {
@@ -311,6 +320,9 @@ export class Keyboard {
};
private _handleKeyUp = (ev: KeyboardEvent) => {
+ if (!this._enabled) {
+ return;
+ }
const code = ev.code as Keys;
const key = this._keys.indexOf(code);
this._keys.splice(key, 1);
diff --git a/src/engine/Input/PointerEventReceiver.ts b/src/engine/Input/PointerEventReceiver.ts
index fe747b319..bf130998e 100644
--- a/src/engine/Input/PointerEventReceiver.ts
+++ b/src/engine/Input/PointerEventReceiver.ts
@@ -74,8 +74,14 @@ export class PointerEventReceiver {
public currentFrameCancel: PointerEvent[] = [];
public currentFrameWheel: WheelEvent[] = [];
+ private _enabled = true;
+
constructor(public readonly target: GlobalEventHandlers & EventTarget, public engine: Engine) {}
+ public toggleEnabled(enabled: boolean) {
+ this._enabled = enabled;
+ }
+
/**
* Creates a new PointerEventReceiver with a new target and engine while preserving existing pointer event
* handlers.
@@ -364,6 +370,9 @@ export class PointerEventReceiver {
* Responsible for handling and parsing pointer events
*/
private _handle(ev: NativeTouchEvent | NativePointerEvent | NativeMouseEvent) {
+ if (!this._enabled) {
+ return;
+ }
ev.preventDefault();
const eventCoords = new Map();
let button: PointerButton;
@@ -422,6 +431,9 @@ export class PointerEventReceiver {
}
private _handleWheel(ev: NativeWheelEvent) {
+ if (!this._enabled) {
+ return;
+ }
// Should we prevent page scroll because of this event
if (
this.engine.pageScrollPreventionMode === ScrollPreventionMode.All ||
diff --git a/src/engine/Label.ts b/src/engine/Label.ts
index 1acedd0c1..4d6cc256d 100644
--- a/src/engine/Label.ts
+++ b/src/engine/Label.ts
@@ -5,7 +5,7 @@ import { Text } from './Graphics/Text';
import { GraphicsComponent, SpriteFont } from './Graphics';
import { Font } from './Graphics/Font';
import { Actor } from './Actor';
-import { ActorArgs } from '.';
+import { ActorArgs } from './Actor';
/**
* Option for creating a label
diff --git a/src/engine/Resources/Sound/Sound.ts b/src/engine/Resources/Sound/Sound.ts
index dd90ced1d..8e23fdbe0 100644
--- a/src/engine/Resources/Sound/Sound.ts
+++ b/src/engine/Resources/Sound/Sound.ts
@@ -194,6 +194,7 @@ export class Sound implements Audio, Loadable {
this._engine.on('visible', () => {
if (engine.pauseAudioWhenHidden && this._wasPlayingOnHidden) {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.play();
this._wasPlayingOnHidden = false;
}
diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts
index 78b493ec9..bde393746 100644
--- a/src/engine/Scene.ts
+++ b/src/engine/Scene.ts
@@ -34,6 +34,12 @@ import { ExcaliburGraphicsContext } from './Graphics';
import { PhysicsWorld } from './Collision/PhysicsWorld';
import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter';
import { Color } from './Color';
+import { DefaultLoader } from './Director/DefaultLoader';
+import { Transition } from './Director';
+
+export class PreLoadEvent {
+ loader: DefaultLoader;
+}
export type SceneEvents = {
initialize: InitializeEvent,
@@ -45,6 +51,7 @@ export type SceneEvents = {
postdraw: PostDrawEvent,
predebugdraw: PreDebugDrawEvent,
postdebugdraw: PostDebugDrawEvent
+ preload: PreLoadEvent
}
export const SceneEvents = {
@@ -56,9 +63,18 @@ export const SceneEvents = {
PreDraw: 'predraw',
PostDraw: 'postdraw',
PreDebugDraw: 'predebugdraw',
- PostDebugDraw: 'postdebugdraw'
+ PostDebugDraw: 'postdebugdraw',
+ PreLoad: 'preload'
};
+export type SceneConstructor = new (...args: any[]) => Scene;
+/**
+ *
+ */
+export function isSceneConstructor(x: any): x is SceneConstructor {
+ return !!x?.prototype && !!x?.prototype?.constructor?.name;
+}
+
/**
* [[Actor|Actors]] are composed together into groupings called Scenes in
* Excalibur. The metaphor models the same idea behind real world
@@ -180,6 +196,33 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
this.events.off(eventName, handler);
}
+ /**
+ * Event hook to provide Scenes a way of loading scene specific resources.
+ *
+ * This is called before the Scene.onInitialize during scene transition. It will only ever fire once for a scene.
+ * @param loader
+ */
+ public onPreLoad(loader: DefaultLoader) {
+ // will be overridden
+ }
+
+ /**
+ * Event hook fired directly before transition, either "in" or "out" of the scene
+ *
+ * This overrides the Engine scene definition. However transitions specified in goto take hightest precedence
+ *
+ * ```typescript
+ * // Overrides all
+ * Engine.goto('scene', { destinationIn: ..., sourceOut: ... });
+ * ```
+ *
+ * This can be used to configure custom transitions for a scene dynamically
+ */
+ public onTransition(direction: 'in' | 'out'): Transition | undefined {
+ // will be overridden
+ return undefined;
+ }
+
/**
* This is called before the first update of the [[Scene]]. Initializes scene members like the camera. This method is meant to be
* overridden. This is where initialization of child actors should take place.
@@ -245,7 +288,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
/**
* Initializes actors in the scene
*/
- private _initializeChildren(): void {
+ private _initializeChildren() {
for (const child of this.entities) {
child._initialize(this.engine);
}
@@ -258,6 +301,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
return this._isInitialized;
}
+
/**
* It is not recommended that internal excalibur methods be overridden, do so at your own risk.
*
@@ -265,7 +309,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
* Excalibur
* @internal
*/
- public _initialize(engine: Engine) {
+ public async _initialize(engine: Engine) {
if (!this.isInitialized) {
this.engine = engine;
// Initialize camera first
@@ -275,7 +319,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
// This order is important! we want to be sure any custom init that add actors
// fire before the actor init
- this.onInitialize.call(this, engine);
+ await this.onInitialize(engine);
this._initializeChildren();
this._logger.debug('Scene.onInitialize', this, engine);
@@ -290,9 +334,9 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
* Activates the scene with the base behavior, then calls the overridable `onActivate` implementation.
* @internal
*/
- public _activate(context: SceneActivationContext): void {
+ public async _activate(context: SceneActivationContext) {
this._logger.debug('Scene.onActivate', this);
- this.onActivate(context);
+ await this.onActivate(context);
}
/**
@@ -301,9 +345,9 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
* Deactivates the scene with the base behavior, then calls the overridable `onDeactivate` implementation.
* @internal
*/
- public _deactivate(context: SceneActivationContext): void {
+ public async _deactivate(context: SceneActivationContext) {
this._logger.debug('Scene.onDeactivate', this);
- this.onDeactivate(context);
+ await this.onDeactivate(context);
}
/**
@@ -356,6 +400,9 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate
* @param delta The number of milliseconds since the last update
*/
public update(engine: Engine, delta: number) {
+ if (!this.isInitialized) {
+ throw new Error('Scene update called before it was initialized!');
+ }
this._preupdate(engine, delta);
// TODO differed entity removal for timers
diff --git a/src/engine/Screen.ts b/src/engine/Screen.ts
index 371347da3..c1c8bcc18 100644
--- a/src/engine/Screen.ts
+++ b/src/engine/Screen.ts
@@ -448,8 +448,10 @@ export class Screen {
}
public popResolutionAndViewport() {
- this.resolution = this._resolutionStack.pop();
- this.viewport = this._viewportStack.pop();
+ if (this._resolutionStack.length && this._viewportStack.length) {
+ this.resolution = this._resolutionStack.pop();
+ this.viewport = this._viewportStack.pop();
+ }
}
private _alreadyWarned = false;
diff --git a/src/engine/TileMap/IsometricMap.ts b/src/engine/TileMap/IsometricMap.ts
index d7b9b29ac..1a1982346 100644
--- a/src/engine/TileMap/IsometricMap.ts
+++ b/src/engine/TileMap/IsometricMap.ts
@@ -1,4 +1,10 @@
-import { BodyComponent, BoundingBox, Collider, ColliderComponent, CollisionType, CompositeCollider, vec, Vector } from '..';
+import { BodyComponent } from '../Collision/BodyComponent';
+import { BoundingBox} from '../Collision/BoundingBox';
+import { ColliderComponent } from '../Collision/ColliderComponent';
+import { Collider } from '../Collision/Colliders/Collider';
+import { CollisionType } from '../Collision/CollisionType';
+import { CompositeCollider } from '../Collision/Colliders/CompositeCollider';
+import { vec, Vector } from '../Math/vector';
import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent';
import { Entity } from '../EntityComponentSystem/Entity';
import { DebugGraphicsComponent, ExcaliburGraphicsContext, Graphic, GraphicsComponent } from '../Graphics';
diff --git a/src/engine/Util/Clock.ts b/src/engine/Util/Clock.ts
index 865c44181..9413f75ff 100644
--- a/src/engine/Util/Clock.ts
+++ b/src/engine/Util/Clock.ts
@@ -1,4 +1,4 @@
-import { Logger } from '..';
+import { Logger } from '../Util/Log';
import { FpsSampler } from './Fps';
export interface ClockOptions {
diff --git a/src/engine/Util/Pool.ts b/src/engine/Util/Pool.ts
index dee3b5e9d..c7b272226 100644
--- a/src/engine/Util/Pool.ts
+++ b/src/engine/Util/Pool.ts
@@ -1,4 +1,4 @@
-import { Logger } from '..';
+import { Logger } from '../Util/Log';
export class Pool {
public totalAllocations = 0;
public index = 0;
diff --git a/src/engine/Util/Semaphore.ts b/src/engine/Util/Semaphore.ts
index 0c73e9f98..2b7b9ffc6 100644
--- a/src/engine/Util/Semaphore.ts
+++ b/src/engine/Util/Semaphore.ts
@@ -38,6 +38,7 @@ export class Semaphore {
return this._waitQueue.length;
}
+ // eslint-disable-next-line require-await
public async enter() {
if (this._count !== 0) {
this._count--;
diff --git a/src/engine/index.ts b/src/engine/index.ts
index 07b386b32..d475c66cb 100644
--- a/src/engine/index.ts
+++ b/src/engine/index.ts
@@ -23,7 +23,6 @@ export * from './Events/MediaEvents';
export * from './Events';
export * from './Label';
export { FontStyle, FontUnit, TextAlign, BaseAlign } from './Graphics/FontCommon';
-export * from './Loader';
export { Particle, ParticleTransform, ParticleEmitter, ParticleArgs, ParticleEmitterArgs, EmitterType } from './Particles';
export * from './Collision/Physics';
export * from './Scene';
@@ -42,6 +41,8 @@ export * from './Resources/Index';
export * from './EntityComponentSystem/index';
+export * from './Director/index';
+
export * from './Color';
export * from './Graphics/index';
@@ -79,9 +80,6 @@ export {
CapturePointerConfig
} from './Input/CapturePointerConfig';
-export {
- EngineInput
-} from './Input/EngineInput';
export {
NativePointerEvent,
diff --git a/src/spec/ActionSpec.ts b/src/spec/ActionSpec.ts
index 1a2936c8f..29cb3ecf1 100644
--- a/src/spec/ActionSpec.ts
+++ b/src/spec/ActionSpec.ts
@@ -8,7 +8,7 @@ describe('Action', () => {
let engine: ex.Engine & any;
let scene: ex.Scene;
- beforeEach(() => {
+ beforeEach(async () => {
jasmine.addMatchers(ExcaliburMatchers);
engine = TestUtils.engine({ width: 100, height: 100 });
@@ -16,8 +16,8 @@ describe('Action', () => {
scene = new ex.Scene();
scene.add(actor);
engine.addScene('test', scene);
- engine.goToScene('test');
- engine.start();
+ await engine.goToScene('test');
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(100);
diff --git a/src/spec/ActorSpec.ts b/src/spec/ActorSpec.ts
index a31dee9f8..7adffe5b8 100644
--- a/src/spec/ActorSpec.ts
+++ b/src/spec/ActorSpec.ts
@@ -17,7 +17,7 @@ describe('A game actor', () => {
jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
});
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine({ width: 100, height: 100 });
actor = new ex.Actor({name: 'Default'});
actor.body.collisionType = ex.CollisionType.Active;
@@ -27,13 +27,13 @@ describe('A game actor', () => {
scene = new ex.Scene();
scene.add(actor);
engine.addScene('test', scene);
- engine.goToScene('test');
+ await engine.goToScene('test');
spyOn(scene, 'draw').and.callThrough();
spyOn(scene, 'debugDraw').and.callThrough();
- engine.start();
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(1);
collisionSystem.initialize(scene);
@@ -628,10 +628,11 @@ describe('A game actor', () => {
expect(scene.actors.length).toBe(0);
});
- it('once killed is not drawn', () => {
+ it('once killed is not drawn', async () => {
engine.stop();
engine = null;
engine = TestUtils.engine({ width: 100, height: 100 });
+ await TestUtils.runToReady(engine);
actor = new ex.Actor();
actor.body.collisionType = ex.CollisionType.Active;
motionSystem = new ex.MotionSystem();
@@ -639,8 +640,7 @@ describe('A game actor', () => {
scene = new ex.Scene();
scene.add(actor);
engine.addScene('test', scene);
- engine.goToScene('test');
- scene._initialize(engine);
+ await engine.goToScene('test');
engine.screen.setCurrentCamera(engine.currentScene.camera);
spyOn(scene, 'draw').and.callThrough();
@@ -694,8 +694,8 @@ describe('A game actor', () => {
scene = new ex.Scene();
engine.addScene('test', scene);
- engine.goToScene('test');
- engine.start();
+ await engine.goToScene('test');
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(1);
@@ -730,8 +730,8 @@ describe('A game actor', () => {
});
scene = new ex.Scene();
engine.addScene('test', scene);
- engine.goToScene('test');
- engine.start();
+ await engine.goToScene('test');
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(1);
@@ -900,12 +900,11 @@ describe('A game actor', () => {
expect(parent.contains(250, 250, true)).toBeTruthy();
});
- it('with an active collision type can be placed on a fixed type', () => {
+ it('with an active collision type can be placed on a fixed type', async () => {
ex.Physics.useArcadePhysics();
const scene = new ex.Scene();
engine.add('somescene', scene);
- engine.goToScene('somescene');
- scene._initialize(engine);
+ await engine.goToScene('somescene');
const active = new ex.Actor({ x: 0, y: -50, width: 100, height: 100 });
active.body.collisionType = ex.CollisionType.Active;
@@ -936,9 +935,9 @@ describe('A game actor', () => {
expect(fixed.pos.y).toBe(50);
});
- it('with an active collision type can jump on a fixed type', () => {
+ it('with an active collision type can jump on a fixed type', async () => {
const scene = new ex.Scene();
- scene._initialize(engine);
+ await scene._initialize(engine);
const active = new ex.Actor({ x: 0, y: -50, width: 100, height: 100 });
active.body.collisionType = ex.CollisionType.Active;
active.vel.y = -100;
@@ -1060,21 +1059,20 @@ describe('A game actor', () => {
scene.update(engine, 100);
});
- it('can only be initialized once', () => {
+ it('can only be initialized once', async () => {
const actor = new ex.Actor();
- let initializeCount = 0;
- actor.on('initialize', () => {
- initializeCount++;
- });
- actor._initialize(engine);
- actor._initialize(engine);
- actor._initialize(engine);
+ const initializeSpy = jasmine.createSpy('initialize');
+
+ actor.on('initialize', initializeSpy);
+ await actor._initialize(engine);
+ await actor._initialize(engine);
+ await actor._initialize(engine);
- expect(initializeCount).toBe(1, 'Actors can only be initialized once');
+ expect(initializeSpy).toHaveBeenCalledTimes(1);
});
- it('can initialize child actors', () => {
+ it('can initialize child actors', async () => {
const actor = new ex.Actor();
const child = new ex.Actor();
const grandchild = new ex.Actor();
@@ -1091,10 +1089,10 @@ describe('A game actor', () => {
initializeCount++;
});
- actor._initialize(engine);
- actor._initialize(engine);
+ await actor._initialize(engine);
+ await actor._initialize(engine);
- expect(initializeCount).toBe(3, 'All child actors should be initialized');
+ expect(initializeCount).withContext('All child actors should be initialized').toBe(3);
});
describe('should detect assigned events and', () => {
diff --git a/src/spec/AlgebraSpec.ts b/src/spec/AlgebraSpec.ts
index cb5b8d8dc..816fce5a4 100644
--- a/src/spec/AlgebraSpec.ts
+++ b/src/spec/AlgebraSpec.ts
@@ -1,7 +1,5 @@
-import { ExcaliburMatchers, ensureImagesLoaded } from 'excalibur-jasmine';
+import { ExcaliburMatchers } from 'excalibur-jasmine';
import * as ex from '@excalibur';
-import { TestUtils } from './util/TestUtils';
-import { Mocks } from './util/Mocks';
describe('Vectors', () => {
beforeEach(() => {
diff --git a/src/spec/BodyComponentSpec.ts b/src/spec/BodyComponentSpec.ts
index 940bdf15a..783a43e22 100644
--- a/src/spec/BodyComponentSpec.ts
+++ b/src/spec/BodyComponentSpec.ts
@@ -1,5 +1,5 @@
import * as ex from '@excalibur';
-import { BodyComponent, CollisionGroup, CollisionType } from '@excalibur';
+import { BodyComponent } from '@excalibur';
import { ExcaliburMatchers } from 'excalibur-jasmine';
describe('A body component', () => {
diff --git a/src/spec/ColliderComponentSpec.ts b/src/spec/ColliderComponentSpec.ts
index b5b387ea1..b18496b2f 100644
--- a/src/spec/ColliderComponentSpec.ts
+++ b/src/spec/ColliderComponentSpec.ts
@@ -46,6 +46,7 @@ describe('A ColliderComponent', () => {
new ex.CollisionStartEvent(
ex.Shape.Circle(50),
ex.Shape.Circle(50),
+ null,
null));
expect(originalCollisionHandler).toHaveBeenCalledTimes(1);
diff --git a/src/spec/CollisionShapeSpec.ts b/src/spec/CollisionShapeSpec.ts
index afe785726..2bc790120 100644
--- a/src/spec/CollisionShapeSpec.ts
+++ b/src/spec/CollisionShapeSpec.ts
@@ -16,13 +16,13 @@ describe('Collision Shape', () => {
let circle: ex.CircleCollider;
let actor: ex.Actor;
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine();
engine.backgroundColor = ex.Color.Transparent;
scene = new ex.Scene();
engine.add('test', scene);
- engine.goToScene('test');
- engine.start();
+ await engine.goToScene('test');
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(1);
@@ -423,13 +423,13 @@ describe('Collision Shape', () => {
describe('a ConvexPolygon', () => {
let engine: ex.Engine;
let scene: ex.Scene;
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine();
engine.backgroundColor = ex.Color.Transparent;
scene = new ex.Scene();
engine.addScene('test', scene);
- engine.goToScene('test');
- engine.start();
+ await engine.goToScene('test');
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(1);
});
@@ -821,13 +821,13 @@ describe('Collision Shape', () => {
engine = null;
});
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine();
engine.backgroundColor = ex.Color.Transparent;
scene = new ex.Scene();
engine.addScene('test', scene);
- engine.goToScene('test');
- engine.start();
+ await engine.goToScene('test');
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(1);
diff --git a/src/spec/CollisionSpec.ts b/src/spec/CollisionSpec.ts
index b285b91f4..e275fb8c9 100644
--- a/src/spec/CollisionSpec.ts
+++ b/src/spec/CollisionSpec.ts
@@ -7,7 +7,7 @@ describe('A Collision', () => {
let engine: ex.Engine = null;
let clock: ex.TestClock = null;
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine({ width: 600, height: 400 });
clock = engine.clock = engine.clock.toTestClock();
@@ -16,7 +16,7 @@ describe('A Collision', () => {
actor1.body.collisionType = ex.CollisionType.Active;
actor2.body.collisionType = ex.CollisionType.Active;
- engine.start();
+ await engine.start();
engine.add(actor1);
engine.add(actor2);
});
@@ -29,7 +29,7 @@ describe('A Collision', () => {
actor2 = null;
});
- it('should throw one event for each actor participating', async () => {
+ it('should throw one event for each actor participating', () => {
let actor1Collision = 0;
let actor2Collision = 0;
actor1.on('precollision', (e: ex.PreCollisionEvent) => {
diff --git a/src/spec/CrossFadeSpec.ts b/src/spec/CrossFadeSpec.ts
new file mode 100644
index 000000000..b73d44dff
--- /dev/null
+++ b/src/spec/CrossFadeSpec.ts
@@ -0,0 +1,63 @@
+import * as ex from '@excalibur';
+import { TestUtils } from './util/TestUtils';
+import { ExcaliburAsyncMatchers } from 'excalibur-jasmine';
+
+describe('A CrossFade transition', () => {
+ beforeAll(() => {
+ jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
+ });
+ it('exists', () => {
+ expect(ex.CrossFade).toBeDefined();
+ });
+
+ it('can be constructed', () => {
+ const sut = new ex.CrossFade({duration: 1000});
+ expect(sut.duration).toBe(1000);
+ expect(sut.name).toContain('CrossFade#');
+ });
+
+ it('can cross fade', (done) => {
+ const engine = TestUtils.engine({backgroundColor: ex.Color.ExcaliburBlue});
+ const clock = engine.clock as ex.TestClock;
+ TestUtils.runToReady(engine).then(() => {
+ engine.rootScene.add(new ex.Actor({
+ pos: ex.vec(20, 20),
+ width: 100,
+ height: 100,
+ color: ex.Color.Red
+ }));
+
+ const onDeactivateSpy = jasmine.createSpy('onDeactivate').and.callFake(async () => {
+ await Promise.resolve();
+ });
+
+ engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy;
+
+ const sut = new ex.CrossFade({duration: 1000});
+ const scene = new ex.Scene();
+ scene.add(new ex.Actor({
+ pos: ex.vec(200, 200),
+ width: 40,
+ height: 40,
+ color: ex.Color.Violet
+ }));
+ engine.addScene('newScene', { scene, transitions: {in: sut }});
+
+ const goto = engine.goto('newScene', { destinationIn: sut});
+ setTimeout(() => {
+ clock.step(1);
+ });
+ setTimeout(() => {
+ clock.step(400);
+ clock.step(400);
+ });
+ setTimeout(() => {
+ clock.step(1);
+ expect(onDeactivateSpy).toHaveBeenCalledTimes(1);
+ expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('/src/spec/images/CrossFadeSpec/crossfade.png').then(() => {
+ done();
+ });
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/spec/DefaultLoaderSpec.ts b/src/spec/DefaultLoaderSpec.ts
new file mode 100644
index 000000000..0613dd0ad
--- /dev/null
+++ b/src/spec/DefaultLoaderSpec.ts
@@ -0,0 +1,72 @@
+import * as ex from '@excalibur';
+import { TestUtils } from './util/TestUtils';
+import { ExcaliburAsyncMatchers, ExcaliburMatchers } from 'excalibur-jasmine';
+
+describe('A DefaultLoader', () => {
+
+ let engine: ex.Engine;
+ beforeEach(() => {
+ jasmine.addMatchers(ExcaliburMatchers);
+ jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
+ engine = TestUtils.engine();
+ });
+
+ it('exists', () => {
+ expect(ex.DefaultLoader).toBeDefined();
+ });
+
+ it('can be constructed', () => {
+ const sut = new ex.DefaultLoader();
+ expect(sut).toBeDefined();
+ });
+
+ it('can be constructed with non-defaults', () => {
+ const sut = new ex.DefaultLoader({
+ loadables: [new ex.ImageSource('./some/image.png')]
+ });
+ expect(sut.resources.length).toBe(1);
+ expect(sut.isLoaded()).toBe(false);
+ expect(sut.progress).toBe(0);
+ });
+
+ it('can draw', async () => {
+ const sut = new ex.DefaultLoader({
+ loadables: [ , , , ]
+ });
+ sut.onInitialize(engine);
+ sut.markResourceComplete();
+ sut.onUpdate(engine, 100);
+ sut.onDraw(sut.canvas.ctx);
+ expect(sut.resources.length).toBe(3);
+ await expectAsync(sut.canvas.ctx).toEqualImage('src/spec/images/DefaultLoaderSpec/loading.png');
+ });
+
+ it('can load stuff', async () => {
+ const img1 = new ex.ImageSource('src/spec/images/DefaultLoaderSpec/loading.png');
+ const img2 = new ex.ImageSource('src/spec/images/DefaultLoaderSpec/loading.png');
+ const sut = new ex.DefaultLoader({
+ loadables: [ img1, img2 ]
+ });
+ sut.onInitialize(engine);
+
+ const onBeforeLoadSpy = jasmine.createSpy('onBeforeLoad');
+ const onAfterLoadSpy = jasmine.createSpy('onBeforeLoad');
+ const onUserAction = jasmine.createSpy('onUserAction');
+ const resourceStartLoad = jasmine.createSpy('resourceStartLoad');
+ const resourceEndLoad = jasmine.createSpy('resourceStartLoad');
+ sut.on('loadresourcestart', resourceStartLoad);
+ sut.on('loadresourceend', resourceEndLoad);
+ sut.onBeforeLoad = onBeforeLoadSpy;
+ sut.onAfterLoad = onAfterLoadSpy;
+ sut.onUserAction = onUserAction;
+
+ await sut.load();
+
+ expect(onBeforeLoadSpy).toHaveBeenCalledTimes(1);
+ expect(onAfterLoadSpy).toHaveBeenCalledTimes(1);
+ expect(onUserAction).toHaveBeenCalledTimes(1);
+
+ expect(resourceStartLoad).toHaveBeenCalledTimes(2);
+ expect(resourceEndLoad).toHaveBeenCalledTimes(2);
+ });
+});
\ No newline at end of file
diff --git a/src/spec/DirectorSpec.ts b/src/spec/DirectorSpec.ts
new file mode 100644
index 000000000..29abb9f87
--- /dev/null
+++ b/src/spec/DirectorSpec.ts
@@ -0,0 +1,221 @@
+import * as ex from '@excalibur';
+import { TestUtils } from './util/TestUtils';
+import { ExcaliburAsyncMatchers } from 'excalibur-jasmine';
+
+describe('A Director', () => {
+ beforeAll(() => {
+ jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
+ });
+ it('exists', () => {
+ expect(ex.Director).toBeDefined();
+ });
+
+ it('can be constructed with a varied scene map', () => {
+ const engine = TestUtils.engine();
+ const sut = new ex.Director(engine, {
+ scene1: new ex.Scene(),
+ scene2: { scene: new ex.Scene() },
+ scene3: { scene: ex.Scene },
+ scene4: ex.Scene
+ });
+
+ expect(sut).toBeDefined();
+ expect(sut.rootScene).not.toBe(null);
+ expect(sut.getSceneInstance('scene1')).not.toBe(null);
+ expect(sut.getSceneInstance('scene2')).not.toBe(null);
+ expect(sut.getSceneInstance('scene3')).not.toBe(null);
+ expect(sut.getSceneInstance('scene4')).not.toBe(null);
+ });
+
+ it('can be constructed with a varied loaders', () => {
+ const engine = TestUtils.engine();
+ const sut = new ex.Director(engine, {
+ scene1: new ex.Scene(),
+ scene2: { scene: new ex.Scene(), loader: new ex.DefaultLoader() },
+ scene3: { scene: ex.Scene, loader: ex.DefaultLoader },
+ scene4: ex.Scene
+ });
+
+ expect(sut).toBeDefined();
+ expect(sut.rootScene).not.toBe(null);
+ expect(sut.getSceneInstance('scene1')).not.toBe(null);
+ expect(sut.getSceneInstance('scene2')).not.toBe(null);
+ expect(sut.getSceneInstance('scene3')).not.toBe(null);
+ expect(sut.getSceneInstance('scene4')).not.toBe(null);
+ });
+
+ it('can configure start, non deferred', async () => {
+ const engine = TestUtils.engine();
+ const scene1 = new ex.Scene();
+ const scene2 = new ex.Scene();
+ const sut = new ex.Director(engine, {
+ scene1,
+ scene2
+ });
+ sut.onInitialize();
+
+
+ const fadeIn = new ex.FadeInOut({ direction: 'in', duration: 1000});
+ const loader = new ex.DefaultLoader();
+ sut.configureStart('scene1', {
+ inTransition: fadeIn,
+ loader
+ });
+ await engine.load(loader);
+ sut.update();
+
+ expect(sut.currentTransition).toBe(fadeIn);
+ expect(sut.currentSceneName).toBe('scene1');
+ expect(sut.currentScene).toBe(scene1);
+ });
+
+ it('can configure start deferred', async () => {
+ const engine = TestUtils.engine();
+ const scene1 = new ex.Scene();
+ const scene2 = new ex.Scene();
+ const sut = new ex.Director(engine, {
+ scene1,
+ scene2
+ });
+ const fadeIn = new ex.FadeInOut({ direction: 'in', duration: 1000});
+ const loader = new ex.DefaultLoader();
+ sut.configureStart('scene1', {
+ inTransition: fadeIn,
+ loader
+ });
+
+ sut.onInitialize();
+ await engine.load(loader);
+ sut.update();
+
+ expect(sut.currentTransition).toBe(fadeIn);
+ expect(sut.currentSceneName).toBe('scene1');
+ expect(sut.currentScene).toBe(scene1);
+ });
+
+ it('will draw a start scene transition', async () => {
+ const engine = TestUtils.engine();
+ const clock = engine.clock as ex.TestClock;
+ clock.start();
+ const scene1 = new ex.Scene();
+ const scene2 = new ex.Scene();
+ const sut = new ex.Director(engine, {
+ scene1,
+ scene2
+ });
+ const fadeIn = new ex.FadeInOut({ direction: 'in', duration: 1000});
+ const loader = new ex.DefaultLoader();
+ sut.configureStart('scene1', {
+ inTransition: fadeIn,
+ loader
+ });
+
+ sut.onInitialize();
+ await engine.load(loader);
+ await (engine as any)._overrideInitialize(engine);
+
+ clock.step(100);
+ sut.update();
+ clock.step(100);
+ sut.update();
+ clock.step(100);
+ sut.update();
+ clock.step(100);
+ sut.update();
+
+ expect(sut.currentTransition).toBe(fadeIn);
+ expect(sut.currentSceneName).toBe('scene1');
+ expect(sut.currentScene).toBe(scene1);
+
+ await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('/src/spec/images/DirectorSpec/fadein.png');
+ });
+
+ it('will run the loader cycle on a scene only once', async () => {
+ const engine = TestUtils.engine();
+ const clock = engine.clock as ex.TestClock;
+ clock.start();
+ const scene1 = new ex.Scene();
+ const loaderSpy = jasmine.createSpy('loader');
+ scene1.onPreLoad = loaderSpy;
+ const scene2 = new ex.Scene();
+ const sut = new ex.Director(engine, {
+ scene1,
+ scene2
+ });
+
+ await sut.maybeLoadScene('scene1');
+ await sut.maybeLoadScene('scene1');
+ await sut.maybeLoadScene('scene1');
+ await sut.maybeLoadScene('scene1');
+
+ expect(loaderSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('can remove a scene', () => {
+ const engine = TestUtils.engine();
+ const clock = engine.clock as ex.TestClock;
+ clock.start();
+ const sut = new ex.Director(engine, {
+ scene1: new ex.Scene(),
+ scene2: { scene: new ex.Scene() },
+ scene3: { scene: ex.Scene },
+ scene4: ex.Scene
+ });
+
+ sut.remove('scene1');
+ expect(sut.getSceneDefinition('scene1')).toBe(undefined);
+ expect(sut.getSceneInstance('scene1')).toBe(undefined);
+ sut.remove('scene2');
+ expect(sut.getSceneDefinition('scene2')).toBe(undefined);
+ expect(sut.getSceneInstance('scene2')).toBe(undefined);
+ sut.remove('scene3');
+ expect(sut.getSceneDefinition('scene3')).toBe(undefined);
+ expect(sut.getSceneInstance('scene3')).toBe(undefined);
+ sut.remove('scene4');
+ expect(sut.getSceneDefinition('scene4')).toBe(undefined);
+ expect(sut.getSceneInstance('scene4')).toBe(undefined);
+ });
+
+ it('cant remove an active scene', () => {
+ const engine = TestUtils.engine();
+ const clock = engine.clock as ex.TestClock;
+ clock.start();
+ const sut = new ex.Director(engine, {
+ scene1: new ex.Scene(),
+ scene2: { scene: new ex.Scene() },
+ scene3: { scene: ex.Scene },
+ scene4: ex.Scene
+ });
+
+ expect(() => sut.remove('root')).toThrowError('Cannot remove a currently active scene: root');
+ expect(() => sut.remove(sut.rootScene)).toThrowError('Cannot remove a currently active scene: root');
+ });
+
+ it('can goto a scene', async () => {
+ const engine = TestUtils.engine();
+ const clock = engine.clock as ex.TestClock;
+ clock.start();
+ const scene2 = new ex.Scene();
+ class MyScene extends ex.Scene {}
+ const sut = new ex.Director(engine, {
+ scene1: new ex.Scene(),
+ scene2: { scene: scene2 },
+ scene3: { scene: ex.Scene },
+ scene4: MyScene
+ });
+ sut.configureStart('root');
+
+ sut.onInitialize();
+ await engine.load(sut.mainLoader);
+ await (engine as any)._overrideInitialize(engine);
+
+ await sut.goto('scene2');
+
+ expect(sut.currentScene).toBe(scene2);
+ expect(sut.currentSceneName).toBe('scene2');
+
+ await sut.goto('scene4');
+ expect(sut.currentSceneName).toBe('scene4');
+ expect(sut.currentScene).toBeInstanceOf(MyScene);
+ });
+});
\ No newline at end of file
diff --git a/src/spec/EngineSpec.ts b/src/spec/EngineSpec.ts
index 4ed47a7d1..cc715aa75 100644
--- a/src/spec/EngineSpec.ts
+++ b/src/spec/EngineSpec.ts
@@ -14,7 +14,7 @@ function flushWebGLCanvasTo2D(source: HTMLCanvasElement): HTMLCanvasElement {
return canvas;
}
-describe('The engine', () => {
+describe('The engine', () => { // TODO timeout
let engine: ex.Engine;
let scene: ex.Scene;
@@ -29,15 +29,15 @@ describe('The engine', () => {
}
};
- beforeEach(() => {
+ beforeEach(async () => {
jasmine.addMatchers(ExcaliburMatchers);
jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
engine = TestUtils.engine();
scene = new ex.Scene();
engine.add('default', scene);
- engine.goToScene('default');
- engine.start();
+ await engine.goToScene('default');
+ await engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(1);
});
@@ -46,21 +46,22 @@ describe('The engine', () => {
reset();
});
- it('should not throw if no loader provided', () => {
+ it('should not throw if no loader provided', async () => {
const exceptionSpy = jasmine.createSpy('exception');
- const boot = () => {
+ const boot = async () => {
const engine = TestUtils.engine();
+ await TestUtils.runToReady(engine);
const clock = engine.clock as ex.TestClock;
clock.setFatalExceptionHandler(exceptionSpy);
clock.start();
clock.step(100);
};
- boot();
+ await boot();
expect(exceptionSpy).not.toHaveBeenCalled();
});
- it('should show the play button by default', (done) => {
+ xit('should show the play button by default', (done) => {
reset();
engine = TestUtils.engine({
suppressPlayButton: false
@@ -69,27 +70,28 @@ describe('The engine', () => {
const imageSource = new ex.ImageSource('src/spec/images/SpriteSpec/icon.png');
const loader = new ex.Loader([imageSource]);
- engine.start(loader);
const testClock = engine.clock as ex.TestClock;
-
- loader.areResourcesLoaded().then(() => {
- testClock.run(2, 100); // 200 ms delay in loader
- expect(document.getElementById('excalibur-play')).withContext('Play button should exist in the document').toBeDefined();
- setTimeout(() => { // needed for the delay to work
- testClock.run(1, 100);
- engine.graphicsContext.flush();
- expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas))
- .toEqualImage('src/spec/images/EngineSpec/engine-load-complete.png').then(() => {
- done();
- });
+ engine.start(loader).then(() => {
+ loader.areResourcesLoaded().then(() => {
+ testClock.run(2, 100); // 200 ms delay in loader
+ expect(document.getElementById('excalibur-play')).withContext('Play button should exist in the document').toBeDefined();
+ setTimeout(() => { // needed for the delay to work
+ testClock.run(1, 100);
+ engine.graphicsContext.flush();
+ expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas))
+ .toEqualImage('src/spec/images/EngineSpec/engine-load-complete.png').then(() => {
+ done();
+ });
+ });
});
});
});
it('should log if loading fails', async () => {
- class FailedLoader implements ex.Loadable {
+ class FailedLoader extends ex.DefaultLoader {
data = undefined;
- load(): Promise {
+ // eslint-disable-next-line require-await
+ async load(): Promise[]> {
throw new Error('I failed');
}
isLoaded(): boolean {
@@ -194,8 +196,9 @@ describe('The engine', () => {
expect(engine.graphicsContext).toBeInstanceOf(ex.ExcaliburGraphicsContext2DCanvas);
});
- it('should update the frame stats every tick', () => {
+ it('should update the frame stats every tick', async () => {
engine = TestUtils.engine();
+ await TestUtils.runToReady(engine);
const testClock = engine.clock as ex.TestClock;
testClock.start();
expect(engine.stats.currFrame.id).toBe(0);
@@ -639,7 +642,7 @@ describe('The engine', () => {
expect(ex.Logger.getInstance().error).toHaveBeenCalledWith('Scene', 'madeUp', 'does not exist!');
});
- it('will add actors to the correct scene when initialized after a deferred goTo', () => {
+ it('will add actors to the correct scene when initialized after a deferred goTo', async () => {
const engine = TestUtils.engine();
const scene1 = new ex.Scene();
const scene2 = new ex.Scene();
@@ -657,9 +660,9 @@ describe('The engine', () => {
spyOn(scene2, 'onInitialize').and.callThrough();
- engine.goToScene('scene1');
+ await engine.goToScene('scene1');
- TestUtils.runToReady(engine);
+ await TestUtils.runToReady(engine);
expect(engine.currentScene).toBe(scene2);
expect(scene1.actors.length).toBe(0);
@@ -678,17 +681,19 @@ describe('The engine', () => {
height: 100,
color: ex.Color.Red
}));
- TestUtils.runToReady(engine);
-
- clock.step(1);
+ TestUtils.runToReady(engine).then(() => {
+ clock.step(1);
- engine.screenshot().then((image) => {
- return expectAsync(image).toEqualImage(flushWebGLCanvasTo2D(engine.canvas)).then(() => {
- done();
+ engine.screenshot().then((image) => {
+ expectAsync(image).toEqualImage(flushWebGLCanvasTo2D(engine.canvas)).then(() => {
+ done();
+ });
});
+ clock.step(1);
+ clock.step(1);
+ clock.step(1);
});
- clock.step(1);
});
it('can screen shot the game HiDPI (in WebGL)', async () => {
@@ -705,7 +710,7 @@ describe('The engine', () => {
height: 100,
color: ex.Color.Red
}));
- TestUtils.runToReady(engine);
+ await TestUtils.runToReady(engine);
clock.step(1);
const screenShotPromise = engine.screenshot();
@@ -743,7 +748,7 @@ describe('The engine', () => {
});
actor.graphics.use(img.toSprite());
engine.add(actor);
- TestUtils.runToReady(engine);
+ await TestUtils.runToReady(engine);
clock.step(1);
const screenShotPromise = engine.screenshot();
@@ -786,8 +791,8 @@ describe('The engine', () => {
expect(engine.onInitialize).toHaveBeenCalledTimes(1);
});
- it('can have onPostUpdate overridden safely', () => {
- engine.start();
+ it('can have onPostUpdate overridden safely', async () => {
+ await engine.start();
const clock = engine.clock as ex.TestClock;
expect(engine.clock.isRunning()).toBe(true);
@@ -806,8 +811,8 @@ describe('The engine', () => {
expect(engine.onPostUpdate).toHaveBeenCalledTimes(2);
});
- it('can have onPreUpdate overridden safely', () => {
- engine.start();
+ it('can have onPreUpdate overridden safely', async () => {
+ await engine.start();
const clock = engine.clock as ex.TestClock;
expect(engine.clock.isRunning()).toBe(true);
@@ -826,8 +831,8 @@ describe('The engine', () => {
expect(engine.onPreUpdate).toHaveBeenCalledTimes(2);
});
- it('can have onPreDraw overridden safely', () => {
- engine.start();
+ it('can have onPreDraw overridden safely', async () => {
+ await engine.start();
const clock = engine.clock as ex.TestClock;
expect(engine.clock.isRunning()).toBe(true);
@@ -846,8 +851,8 @@ describe('The engine', () => {
expect(engine.onPreDraw).toHaveBeenCalledTimes(2);
});
- it('can have onPostDraw overridden safely', () => {
- engine.start();
+ it('can have onPostDraw overridden safely', async () => {
+ await engine.start();
const clock = engine.clock as ex.TestClock;
expect(engine.clock.isRunning()).toBe(true);
diff --git a/src/spec/FadeInOutSpec.ts b/src/spec/FadeInOutSpec.ts
new file mode 100644
index 000000000..5f1c74026
--- /dev/null
+++ b/src/spec/FadeInOutSpec.ts
@@ -0,0 +1,101 @@
+import * as ex from '@excalibur';
+import { TestUtils } from './util/TestUtils';
+import { ExcaliburAsyncMatchers } from 'excalibur-jasmine';
+
+describe('A FadeInOut transition', () => {
+ beforeAll(() => {
+ jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
+ });
+ it('exists', () => {
+ expect(ex.CrossFade).toBeDefined();
+ });
+
+ it('can be constructed', () => {
+ const sut = new ex.FadeInOut({duration: 1000, color: ex.Color.Red});
+ expect(sut.duration).toBe(1000);
+ expect(sut.name).toContain('FadeInOut#');
+ expect(sut.color).toEqual(ex.Color.Red);
+ });
+
+ it('can fade in', (done) => {
+ const engine = TestUtils.engine({backgroundColor: ex.Color.ExcaliburBlue});
+ const clock = engine.clock as ex.TestClock;
+ TestUtils.runToReady(engine).then(() => {
+ engine.add(new ex.Actor({
+ pos: ex.vec(20, 20),
+ width: 100,
+ height: 100,
+ color: ex.Color.Red
+ }));
+
+ const onDeactivateSpy = jasmine.createSpy('onDeactivate').and.callFake(async () => {
+ await Promise.resolve();
+ });
+
+ engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy;
+
+ const sut = new ex.FadeInOut({duration: 1000, direction: 'in'});
+ const scene = new ex.Scene();
+ scene.add(new ex.Actor({
+ pos: ex.vec(200, 200),
+ width: 40,
+ height: 40,
+ color: ex.Color.Violet
+ }));
+ engine.addScene('newScene', { scene, transitions: {in: sut}});
+
+ const goto = engine.goto('newScene');
+ setTimeout(() => {
+ clock.step(1);
+ });
+ setTimeout(() => {
+ clock.step(500);
+ });
+ setTimeout(() => {
+ clock.step(1);
+ expect(onDeactivateSpy).toHaveBeenCalledTimes(1);
+ expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('/src/spec/images/FadeInOutSpec/fadein.png').then(() => {
+ done();
+ });
+ });
+ });
+ });
+
+ it('can fade out', (done) => {
+ const engine = TestUtils.engine({backgroundColor: ex.Color.ExcaliburBlue});
+ const clock = engine.clock as ex.TestClock;
+ TestUtils.runToReady(engine).then(() => {
+ engine.add(new ex.Actor({
+ pos: ex.vec(20, 20),
+ width: 100,
+ height: 100,
+ color: ex.Color.Red
+ }));
+
+
+ const sut = new ex.FadeInOut({duration: 1000, direction: 'out', color: ex.Color.Violet});
+ const scene = new ex.Scene();
+ scene.add(new ex.Actor({
+ pos: ex.vec(200, 200),
+ width: 40,
+ height: 40,
+ color: ex.Color.Violet
+ }));
+ engine.addScene('newScene', scene);
+
+ const goto = engine.goto('newScene', {sourceOut: sut});
+ setTimeout(() => {
+ clock.step(1);
+ });
+ setTimeout(() => {
+ clock.step(900);
+ });
+ setTimeout(() => {
+ clock.step(1);
+ expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('/src/spec/images/FadeInOutSpec/fadeout.png').then(() => {
+ done();
+ });
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/spec/FrameStatsSpec.ts b/src/spec/FrameStatsSpec.ts
index 55b7c5a05..dc9b63c0e 100644
--- a/src/spec/FrameStatsSpec.ts
+++ b/src/spec/FrameStatsSpec.ts
@@ -10,19 +10,18 @@ describe('The engine', () => {
let actor: ex.Actor;
let stats: ex.FrameStats;
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine({
width: 400,
height: 400
});
scene = new ex.Scene();
- engine.removeScene('root');
- engine.addScene('root', scene);
- engine.goToScene('root');
+ engine.addScene('newScene', scene);
+ await engine.goToScene('newScene'); // TODO use new method
actor = new ex.Actor({ x: 0, y: 0, width: 10, height: 10, color: ex.Color.Red });
scene.add(actor);
- TestUtils.runToReady(engine);
+ await TestUtils.runToReady(engine);
const clock = engine.clock as TestClock;
clock.step(16.6);
stats = engine.stats.currFrame;
diff --git a/src/spec/InputMapperSpec.ts b/src/spec/InputMapperSpec.ts
index b9822ac11..0938195e3 100644
--- a/src/spec/InputMapperSpec.ts
+++ b/src/spec/InputMapperSpec.ts
@@ -50,10 +50,10 @@ describe('An InputMapper', () => {
expect(command).toHaveBeenCalledTimes(0);
});
- it('can fire wasPressed events when used in a engine', () => {
+ it('can fire wasPressed events when used in a engine', async () => {
const engine = TestUtils.engine({ width: 100, height: 100 });
-
+ await TestUtils.runToReady(engine);
const clock = engine.clock as ex.TestClock;
clock.start();
engine.input.keyboard.triggerEvent('down', ex.Keys.Space);
diff --git a/src/spec/LabelSpec.ts b/src/spec/LabelSpec.ts
index 1f613a631..491f90cff 100644
--- a/src/spec/LabelSpec.ts
+++ b/src/spec/LabelSpec.ts
@@ -1,7 +1,6 @@
import { ExcaliburMatchers, ensureImagesLoaded } from 'excalibur-jasmine';
import * as ex from '@excalibur';
import { TestUtils } from './util/TestUtils';
-import { Mocks } from './util/Mocks';
describe('A label', () => {
let label: ex.Label;
diff --git a/src/spec/LineSpec.ts b/src/spec/LineSpec.ts
index c13dee25b..524b19051 100644
--- a/src/spec/LineSpec.ts
+++ b/src/spec/LineSpec.ts
@@ -100,7 +100,7 @@ describe('A Line', () => {
const testClock = game.clock as ex.TestClock;
- TestUtils.runToReady(game);
+ await TestUtils.runToReady(game);
const sut = new ex.Line({
start: ex.vec(0, 0),
diff --git a/src/spec/LoaderSpec.ts b/src/spec/LoaderSpec.ts
index 5fc78fce3..36d75c4bf 100644
--- a/src/spec/LoaderSpec.ts
+++ b/src/spec/LoaderSpec.ts
@@ -1,5 +1,4 @@
import * as ex from '@excalibur';
-import { Loader } from '@excalibur';
import { ExcaliburMatchers, ensureImagesLoaded } from 'excalibur-jasmine';
import { TestUtils } from './util/TestUtils';
@@ -45,8 +44,8 @@ describe('A loader', () => {
it('can be drawn at 0', (done) => {
const loader = new ex.Loader([, , , ,]);
(loader as any)._image.onload = () => {
- loader.wireEngine(engine);
- loader.draw(loader.canvas.ctx);
+ loader.onInitialize(engine);
+ loader.onDraw(loader.canvas.ctx);
ensureImagesLoaded(loader.canvas.ctx.canvas, 'src/spec/images/LoaderSpec/zero.png').then(([canvas, image]) => {
expect(canvas).toEqualImage(image);
done();
@@ -60,8 +59,8 @@ describe('A loader', () => {
loader.markResourceComplete();
loader.markResourceComplete();
- loader.wireEngine(engine);
- loader.draw(loader.canvas.ctx);
+ loader.onInitialize(engine);
+ loader.onDraw(loader.canvas.ctx);
ensureImagesLoaded(loader.canvas.ctx.canvas, 'src/spec/images/LoaderSpec/fifty.png').then(([canvas, image]) => {
expect(canvas).toEqualImage(image);
done();
@@ -77,8 +76,8 @@ describe('A loader', () => {
loader.markResourceComplete();
loader.markResourceComplete();
- loader.wireEngine(engine);
- loader.draw(loader.canvas.ctx);
+ loader.onInitialize(engine);
+ loader.onDraw(loader.canvas.ctx);
ensureImagesLoaded(loader.canvas.ctx.canvas, 'src/spec/images/LoaderSpec/100.png').then(([canvas, image]) => {
expect(canvas).toEqualImage(image);
done();
@@ -93,10 +92,10 @@ describe('A loader', () => {
loader.markResourceComplete();
loader.markResourceComplete();
loader.markResourceComplete();
- loader.wireEngine(engine);
+ loader.onInitialize(engine);
loader.showPlayButton();
- loader.draw(loader.canvas.ctx);
+ loader.onDraw(loader.canvas.ctx);
ensureImagesLoaded(loader.canvas.ctx.canvas, 'src/spec/images/LoaderSpec/playbuttonshown-noprogressbar.png')
.then(([canvas, image]) => {
expect(canvas).toEqualImage(image);
@@ -107,10 +106,10 @@ describe('A loader', () => {
it('can have the play button position customized', () => {
const loader = new ex.Loader([, , , ,]);
- loader.wireEngine(engine);
+ loader.onInitialize(engine);
loader.playButtonPosition = ex.vec(42, 77);
loader.showPlayButton();
- loader.draw(loader.canvas.ctx);
+ loader.onDraw(loader.canvas.ctx);
// there is some dom pollution want to be sure we get the RIGHT root element
const playbutton = (loader as any)._playButtonRootElement as HTMLDivElement;
expect(playbutton.style.left).toBe('42px');
@@ -120,10 +119,10 @@ describe('A loader', () => {
it('can have the logo position customized', (done) => {
const loader = new ex.Loader([, , , ,]);
(loader as any)._image.onload = () => {
- loader.wireEngine(engine);
+ loader.onInitialize(engine);
loader.logoPosition = ex.vec(0, 0);
loader.showPlayButton();
- loader.draw(loader.canvas.ctx);
+ loader.onDraw(loader.canvas.ctx);
ensureImagesLoaded(loader.canvas.ctx.canvas, 'src/spec/images/LoaderSpec/logo-position.png').then(([canvas, image]) => {
expect(canvas).toEqualImage(image);
done();
@@ -140,8 +139,8 @@ describe('A loader', () => {
loader.markResourceComplete();
loader.markResourceComplete();
loader.markResourceComplete();
- loader.wireEngine(engine);
- loader.draw(loader.canvas.ctx);
+ loader.onInitialize(engine);
+ loader.onDraw(loader.canvas.ctx);
ensureImagesLoaded(loader.canvas.ctx.canvas, 'src/spec/images/LoaderSpec/loader-position-color.png').then(([canvas, image]) => {
expect(canvas).toEqualImage(image);
done();
@@ -177,7 +176,7 @@ describe('A loader', () => {
it('can have the enter key pressed to start', (done) => {
const loader = new ex.Loader([, , , ,]);
- loader.wireEngine(engine);
+ loader.onInitialize(engine);
loader.loadingBarPosition = ex.vec(0, 0);
loader.loadingBarColor = ex.Color.Red;
loader.markResourceComplete();
@@ -222,8 +221,8 @@ describe('A loader', () => {
const clock = engine.clock = engine.clock.toTestClock();
const pointerHandler = jasmine.createSpy('pointerHandler');
engine.input.pointers.primary.on('up', pointerHandler);
- const loader = new Loader([new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png')]);
- engine.start(loader);
+ const loader = new ex.Loader([new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png')]);
+ const start = engine.start(loader);
await loader.areResourcesLoaded();
clock.step(200);
@@ -241,7 +240,7 @@ describe('A loader', () => {
it('updates the play button postion on resize', () => {
const engine = new ex.Engine({width: 1000, height: 1000});
const loader = new ex.Loader([, , , ,]);
- loader.wireEngine(engine);
+ loader.onInitialize(engine);
loader.markResourceComplete();
loader.markResourceComplete();
loader.markResourceComplete();
diff --git a/src/spec/ParticleSpec.ts b/src/spec/ParticleSpec.ts
index 8de11fab9..ae0636cc9 100644
--- a/src/spec/ParticleSpec.ts
+++ b/src/spec/ParticleSpec.ts
@@ -17,7 +17,7 @@ function flushWebGLCanvasTo2D(source: HTMLCanvasElement): HTMLCanvasElement {
describe('A particle', () => {
let engine: ex.Engine;
let scene: ex.Scene;
- beforeEach(() => {
+ beforeEach(async () => {
jasmine.addMatchers(ExcaliburMatchers);
jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
engine = TestUtils.engine(
@@ -30,8 +30,7 @@ describe('A particle', () => {
);
scene = new ex.Scene();
engine.addScene('root', scene);
- engine.start();
-
+ await TestUtils.runToReady(engine);
const clock = engine.clock as ex.TestClock;
clock.step(1);
diff --git a/src/spec/PointerInputSpec.ts b/src/spec/PointerInputSpec.ts
index ab9108572..e28ae38cc 100644
--- a/src/spec/PointerInputSpec.ts
+++ b/src/spec/PointerInputSpec.ts
@@ -22,11 +22,11 @@ describe('A pointer', () => {
target.dispatchEvent(evt);
}
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine({
pointerScope: ex.PointerScope.Document
});
- engine.start();
+ await TestUtils.runToReady(engine);
const clock = engine.clock as ex.TestClock;
clock.step(1);
diff --git a/src/spec/ResourceSpec.ts b/src/spec/ResourceSpec.ts
index 60e6c7323..2f0527ae2 100644
--- a/src/spec/ResourceSpec.ts
+++ b/src/spec/ResourceSpec.ts
@@ -31,14 +31,11 @@ describe('A generic Resource', () => {
});
describe('without data', () => {
- it('should not fail on load', (done) => {
+ it('should not fail on load', async () => {
const emptyLoader = new ex.Loader();
const game = TestUtils.engine();
- TestUtils.runToReady(game, emptyLoader).then(() => {
- expect(emptyLoader.isLoaded()).toBe(true);
- game.stop();
- done();
- });
+ await game.start();
+ expect(emptyLoader.isLoaded()).toBe(true);
});
});
diff --git a/src/spec/SceneSpec.ts b/src/spec/SceneSpec.ts
index 8a8e12096..7bedc3197 100644
--- a/src/spec/SceneSpec.ts
+++ b/src/spec/SceneSpec.ts
@@ -7,16 +7,15 @@ describe('A scene', () => {
let scene: ex.Scene;
let clock: ex.TestClock;
- beforeEach(() => {
+ beforeEach(async () => {
actor = new ex.Actor();
engine = TestUtils.engine({ width: 100, height: 100 });
scene = new ex.Scene();
spyOn(scene, 'draw').and.callThrough();
- engine.removeScene('root');
- engine.addScene('root', scene);
- engine.goToScene('root');
- engine.start();
+ engine.addScene('newScene', scene);
+ engine.goToScene('newScene');
+ await TestUtils.runToReady(engine);
clock = engine.clock as ex.TestClock;
clock.step(100);
@@ -31,14 +30,14 @@ describe('A scene', () => {
expect(ex.Scene).toBeTruthy();
});
- it('can have a background color set', () => {
+ it('can have a background color set', async () => {
engine.backgroundColor = ex.Color.Black;
const newScene = new ex.Scene();
newScene.backgroundColor = ex.Color.Yellow;
engine.addScene('background', newScene);
- engine.goToScene('background');
+ await engine.goToScene('background');
(engine as any)._draw(100);
@@ -315,39 +314,30 @@ describe('A scene', () => {
expect(actor.graphics.onPostDraw).not.toHaveBeenCalled();
});
- it('initializes after start or play in first update', () => {
+ it('initializes after start or play in first update', async () => {
const scene = new ex.Scene();
spyOn(scene, 'onInitialize');
- engine.removeScene('root');
- engine.addScene('root', scene);
+ engine.addScene('otherScene', scene);
expect(scene.onInitialize).toHaveBeenCalledTimes(0);
- engine.goToScene('root');
- engine.start();
- clock.step(100);
+ await engine.goToScene('otherScene');
expect(scene.onInitialize).toHaveBeenCalledTimes(1);
});
- it('calls onActivate and onDeactivate with the correct args', () => {
+ it('calls onActivate and onDeactivate with the correct args', async () => {
const sceneA = new ex.Scene();
sceneA.onDeactivate = jasmine.createSpy('onDeactivate()');
const sceneB = new ex.Scene();
sceneB.onActivate = jasmine.createSpy('onActivate()');
- engine.removeScene('root');
- engine.addScene('root', sceneA);
+ engine.addScene('sceneA', sceneA);
engine.addScene('sceneB', sceneB);
- engine.goToScene('root');
- engine.start();
- clock.step(100);
- clock.step(100);
+ await engine.goToScene('sceneA');
- engine.goToScene('sceneB', { foo: 'bar' });
- clock.step(100);
- clock.step(100);
+ await engine.goToScene('sceneB', { foo: 'bar' });
expect(sceneA.onDeactivate).toHaveBeenCalledWith({
engine,
@@ -366,8 +356,8 @@ describe('A scene', () => {
engine = TestUtils.engine({ width: 100, height: 100 });
scene = new ex.Scene();
- engine.removeScene('root');
- engine.addScene('root', scene);
+ engine.removeScene('otherScene');
+ engine.addScene('otherScene', scene);
let initialized = false;
scene.on('initialize', (evt: ex.InitializeEvent) => {
@@ -378,7 +368,7 @@ describe('A scene', () => {
done();
});
- engine.goToScene('root');
+ engine.goToScene('otherScene');
engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(100);
@@ -388,8 +378,8 @@ describe('A scene', () => {
engine = TestUtils.engine({ width: 100, height: 100 });
scene = new ex.Scene();
- engine.removeScene('root');
- engine.addScene('root', scene);
+ engine.removeScene('otherScene');
+ engine.addScene('otherScene', scene);
let sceneInitialized = false;
const sceneActivated = false;
@@ -409,44 +399,39 @@ describe('A scene', () => {
});
scene.add(actor);
- engine.goToScene('root');
+ engine.goToScene('otherScene');
engine.start();
const clock = engine.clock as ex.TestClock;
clock.step(100);
});
- it('can only be initialized once', () => {
+ it('can only be initialized once', async () => {
engine = TestUtils.engine({ width: 100, height: 100 });
- const clock = engine.clock as ex.TestClock;
+ await TestUtils.runToReady(engine);
scene = new ex.Scene();
- engine.removeScene('root');
- engine.addScene('root', scene);
+ engine.addScene('newScene', scene);
- let initializeCount = 0;
- scene.on('initialize', (evt) => {
- initializeCount++;
- });
+ const initSpy = jasmine.createSpy('init');
+ scene.on('initialize', initSpy);
- engine.goToScene('root');
- engine.start();
- clock.step(1);
+ await engine.goToScene('newScene');
scene.update(engine, 100);
scene.update(engine, 100);
- scene._initialize(engine);
- scene._initialize(engine);
- scene._initialize(engine);
+ await scene._initialize(engine);
+ await scene._initialize(engine);
+ await scene._initialize(engine);
- expect(initializeCount).toBe(1, 'Scenes can only be initialized once');
+ expect(initSpy).toHaveBeenCalledTimes(1);
});
- it('should initialize before actors in the scene', () => {
+ it('should initialize before actors in the scene', async () => {
engine = TestUtils.engine({ width: 100, height: 100 });
+ await TestUtils.runToReady(engine);
const clock = engine.clock as ex.TestClock;
+ clock.step(1);
scene = new ex.Scene();
-
- engine.removeScene('root');
- engine.addScene('root', scene);
+ engine.addScene('newScene', scene);
const actor = new ex.Actor();
scene.add(actor);
@@ -458,8 +443,8 @@ describe('A scene', () => {
expect(sceneInit).toBe(true, 'Scene should be initialized first before any actors');
};
- engine.goToScene('root');
- engine.start();
+ await engine.goToScene('newScene');
+
clock.step(1);
scene.update(engine, 100);
});
@@ -752,11 +737,11 @@ describe('A scene', () => {
expect(otherScene.isCurrentScene()).toBe(false);
});
- it('will not be the current scene if the scene was switched', () => {
+ it('will not be the current scene if the scene was switched', async () => {
const otherScene = new ex.Scene();
- engine.goToScene('root');
+ await engine.goToScene('root');
engine.addScene('secondaryScene', otherScene);
- engine.goToScene('secondaryScene');
+ await engine.goToScene('secondaryScene');
expect(scene.isCurrentScene()).toBe(false);
expect(otherScene.isCurrentScene()).toBe(true);
@@ -768,8 +753,8 @@ describe('A scene', () => {
beforeEach(() => {
engine = TestUtils.engine({ width: 100, height: 100 });
scene = new ex.Scene();
- engine.removeScene('root');
- engine.addScene('root', scene);
+ engine.removeScene('newScene');
+ engine.addScene('newScene', scene);
});
afterEach(() => {
@@ -778,7 +763,7 @@ describe('A scene', () => {
scene = null;
});
- it('can have onInitialize overridden safely', () => {
+ it('can have onInitialize overridden safely', async () => {
const clock = engine.clock as ex.TestClock;
let initCalled = false;
scene.onInitialize = (engine) => {
@@ -791,16 +776,16 @@ describe('A scene', () => {
spyOn(scene, 'onInitialize').and.callThrough();
- TestUtils.runToReady(engine);
- engine.goToScene('root');
+ await TestUtils.runToReady(engine);
+ await engine.goToScene('newScene');
clock.step(100);
expect(initCalled).toBe(true);
expect(scene.onInitialize).toHaveBeenCalledTimes(1);
});
- it('can have onPostUpdate overridden safely', () => {
- scene._initialize(engine);
+ it('can have onPostUpdate overridden safely', async () => {
+ await scene._initialize(engine);
scene.onPostUpdate = (engine, delta) => {
expect(engine).not.toBe(null);
expect(delta).toBe(100);
@@ -816,8 +801,8 @@ describe('A scene', () => {
expect(scene.onPostUpdate).toHaveBeenCalledTimes(2);
});
- it('can have onPreUpdate overridden safely', () => {
- scene._initialize(engine);
+ it('can have onPreUpdate overridden safely', async () => {
+ await scene._initialize(engine);
scene.onPreUpdate = (engine, delta) => {
expect(engine).not.toBe(null);
expect(delta).toBe(100);
diff --git a/src/spec/ScreenElementSpec.ts b/src/spec/ScreenElementSpec.ts
index 61e1201f7..b82aaaed8 100644
--- a/src/spec/ScreenElementSpec.ts
+++ b/src/spec/ScreenElementSpec.ts
@@ -15,7 +15,7 @@ describe('A ScreenElement', () => {
jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
});
- beforeEach(() => {
+ beforeEach(async () => {
screenElement = new ex.ScreenElement({
pos: new ex.Vector(50, 50),
width: 100,
@@ -28,7 +28,8 @@ describe('A ScreenElement', () => {
scene = new ex.Scene();
engine.addScene('test', scene);
engine.goToScene('test');
- engine.start();
+
+ await TestUtils.runToReady(engine);
clock = engine.clock as ex.TestClock;
diff --git a/src/spec/TextSpec.ts b/src/spec/TextSpec.ts
index ec73f5ddd..a57e595aa 100644
--- a/src/spec/TextSpec.ts
+++ b/src/spec/TextSpec.ts
@@ -61,7 +61,7 @@ declare global {
/**
*
*/
-export async function waitForFontLoad(font: string, timeout = 2000, interval = 100) {
+export function waitForFontLoad(font: string, timeout = 2000, interval = 100): Promise {
return new Promise((resolve, reject) => {
// repeatedly poll check
const poller = setInterval(async () => {
diff --git a/src/spec/TileMapSpec.ts b/src/spec/TileMapSpec.ts
index 2d1862f89..cb0567a92 100644
--- a/src/spec/TileMapSpec.ts
+++ b/src/spec/TileMapSpec.ts
@@ -26,7 +26,7 @@ describe('A TileMap', () => {
});
scene = new ex.Scene();
engine.addScene('root', scene);
- engine.start();
+ await TestUtils.runToReady(engine);
const clock = engine.clock as ex.TestClock;
texture = new ex.ImageSource('src/spec/images/TileMapSpec/Blocks.png');
await texture.load();
@@ -188,6 +188,7 @@ describe('A TileMap', () => {
columns: 5
});
tm._initialize(engine);
+ tm.update(engine, 99);
const cell = tm.getTile(0, 0);
const rectangle = new ex.Rectangle({
diff --git a/src/spec/TimerSpec.ts b/src/spec/TimerSpec.ts
index 67626a6be..b7458d754 100644
--- a/src/spec/TimerSpec.ts
+++ b/src/spec/TimerSpec.ts
@@ -6,7 +6,7 @@ describe('A Timer', () => {
let scene: ex.Scene;
let engine: ex.Engine;
- beforeEach(() => {
+ beforeEach(async () => {
engine = TestUtils.engine({
width: 600,
height: 400
@@ -19,7 +19,8 @@ describe('A Timer', () => {
});
scene = new ex.Scene();
engine.addScene('root', scene);
- scene._initialize(engine);
+
+ await TestUtils.runToReady(engine);
});
it('has a unique id', () => {
diff --git a/src/spec/TransistionSpec.ts b/src/spec/TransistionSpec.ts
new file mode 100644
index 000000000..731893032
--- /dev/null
+++ b/src/spec/TransistionSpec.ts
@@ -0,0 +1,130 @@
+import * as ex from '@excalibur';
+import { TestUtils } from './util/TestUtils';
+
+describe('A Transition', () => {
+
+ it('exists', () => {
+ expect(ex.Transition).toBeDefined();
+ });
+
+ it('can be constructed', () => {
+ const sut = new ex.Transition({
+ duration: 1000,
+ direction: 'in',
+ easing: ex.EasingFunctions.EaseInOutCubic,
+ hideLoader: false,
+ blockInput: false
+ });
+
+ expect(sut).toBeTruthy();
+ expect(sut.name).toContain('Transition#');
+ expect(sut.duration).toBe(1000);
+ expect(sut.direction).toBe('in');
+ expect(sut.blockInput).toBe(false);
+ expect(sut.hideLoader).toBe(false);
+ expect(sut.easing).toBe(ex.EasingFunctions.EaseInOutCubic);
+
+ expect(sut.progress).toBe(1);
+ expect(sut.complete).toBe(false);
+ expect(sut.started).toBe(false);
+ });
+
+ it('can be constructed with defaults', () => {
+ const sut = new ex.Transition({duration: 2000});
+ expect(sut).toBeTruthy();
+ expect(sut.name).toContain('Transition#');
+ expect(sut.duration).toBe(2000);
+
+ expect(sut.direction).toBe('out');
+ expect(sut.blockInput).toBe(false);
+ expect(sut.hideLoader).toBe(false);
+ expect(sut.easing).toBe(ex.EasingFunctions.Linear);
+
+ expect(sut.progress).toBe(0);
+ expect(sut.complete).toBe(false);
+ expect(sut.started).toBe(false);
+ });
+
+ it('can be started with execute()', () => {
+ const engine = TestUtils.engine();
+ const sut = new ex.Transition({duration: 3000});
+ const onUpdateSpy = jasmine.createSpy('onUpdate');
+ const onStartSpy = jasmine.createSpy('onStart');
+ const onEndSpy = jasmine.createSpy('onEnd');
+ sut.onUpdate = onUpdateSpy;
+ sut.onStart = onStartSpy;
+ sut.onEnd = onEndSpy;
+ sut._initialize(engine);
+
+
+ sut.execute();
+ expect(sut.started).toBe(true);
+ expect(sut.progress).toBe(0);
+ expect(sut.onStart).toHaveBeenCalledWith(0);
+ expect(sut.onUpdate).toHaveBeenCalledWith(0);
+
+ sut.onPreUpdate(engine, 16);
+ sut.execute();
+ expect(onUpdateSpy.calls.argsFor(1)).toEqual([16/3000]);
+
+ sut.onPreUpdate(engine, 16);
+ sut.execute();
+ expect(onUpdateSpy.calls.argsFor(2)).toEqual([32/3000]);
+
+ sut.onPreUpdate(engine, 3200 -32);
+ sut.execute();
+ expect(onEndSpy).toHaveBeenCalledWith(1);
+ expect(sut.complete).toBe(true);
+
+ sut.onPreUpdate(engine, 4000);
+ sut.execute();
+
+ // Start and end should only be called once
+ expect(onStartSpy).toHaveBeenCalledTimes(1);
+ expect(onEndSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('can be reset()', () => {
+ const engine = TestUtils.engine();
+ const sut = new ex.Transition({duration: 3000});
+ const onUpdateSpy = jasmine.createSpy('onUpdate');
+ const onStartSpy = jasmine.createSpy('onStart');
+ const onEndSpy = jasmine.createSpy('onEnd');
+ sut.onUpdate = onUpdateSpy;
+ sut.onStart = onStartSpy;
+ sut.onEnd = onEndSpy;
+ sut._initialize(engine);
+
+
+ sut.execute();
+ expect(sut.started).toBe(true);
+ expect(sut.progress).toBe(0);
+ expect(sut.onStart).toHaveBeenCalledWith(0);
+ expect(sut.onUpdate).toHaveBeenCalledWith(0);
+
+ sut.onPreUpdate(engine, 16);
+ sut.execute();
+ expect(onUpdateSpy.calls.argsFor(1)).toEqual([16/3000]);
+
+ sut.onPreUpdate(engine, 16);
+ sut.execute();
+ expect(onUpdateSpy.calls.argsFor(2)).toEqual([32/3000]);
+
+ sut.onPreUpdate(engine, 3200 -32);
+ sut.execute();
+ expect(onEndSpy).toHaveBeenCalledWith(1);
+ expect(sut.complete).toBe(true);
+
+ sut.onPreUpdate(engine, 4000);
+ sut.execute();
+
+ expect(sut.complete).toBe(true);
+
+ sut.onReset = jasmine.createSpy('onReset');
+ sut.reset();
+
+ expect(sut.complete).toBe(false);
+ expect(sut.started).toBe(false);
+ expect(sut.onReset).toHaveBeenCalledTimes(1);
+ });
+});
\ No newline at end of file
diff --git a/src/spec/images/CrossFadeSpec/crossfade.png b/src/spec/images/CrossFadeSpec/crossfade.png
new file mode 100644
index 000000000..5f06e3722
Binary files /dev/null and b/src/spec/images/CrossFadeSpec/crossfade.png differ
diff --git a/src/spec/images/DefaultLoaderSpec/loading.png b/src/spec/images/DefaultLoaderSpec/loading.png
new file mode 100644
index 000000000..a014936e8
Binary files /dev/null and b/src/spec/images/DefaultLoaderSpec/loading.png differ
diff --git a/src/spec/images/DirectorSpec/fadein.png b/src/spec/images/DirectorSpec/fadein.png
new file mode 100644
index 000000000..5085f5f0b
Binary files /dev/null and b/src/spec/images/DirectorSpec/fadein.png differ
diff --git a/src/spec/images/FadeInOutSpec/fadein.png b/src/spec/images/FadeInOutSpec/fadein.png
new file mode 100644
index 000000000..b16b0f1f0
Binary files /dev/null and b/src/spec/images/FadeInOutSpec/fadein.png differ
diff --git a/src/spec/images/FadeInOutSpec/fadeout.png b/src/spec/images/FadeInOutSpec/fadeout.png
new file mode 100644
index 000000000..db5f5e5d1
Binary files /dev/null and b/src/spec/images/FadeInOutSpec/fadeout.png differ
diff --git a/src/spec/util/TestUtils.ts b/src/spec/util/TestUtils.ts
index 0eb0fdc63..48e123401 100644
--- a/src/spec/util/TestUtils.ts
+++ b/src/spec/util/TestUtils.ts
@@ -39,9 +39,12 @@ export namespace TestUtils {
}
/**
- *
+ * Waits for the internal loader state to be ready by ticking the test clock
*/
- export async function runToReady(engine: ex.Engine, loader?: ex.Loader) {
+ export async function runToReady(engine: ex.Engine, loader?: ex.DefaultLoader) {
+ if (!(engine.clock instanceof ex.TestClock)) {
+ throw Error('Engine does not have TestClock enabled');
+ }
const clock = engine.clock as ex.TestClock;
const start = engine.start(loader);
// If loader
@@ -53,6 +56,7 @@ export namespace TestUtils {
});
await engine.isReady();
}
+ await start;
}
/**
diff --git a/wallaby.js b/wallaby.js
index 2bc66a9c8..db34e2702 100644
--- a/wallaby.js
+++ b/wallaby.js
@@ -3,6 +3,7 @@ const webpack = require('webpack');
module.exports = function (wallaby) {
return {
+ runMode: 'onsave',
files: [
{ pattern: 'src/spec/util/*.ts', load: false },
{ pattern: 'src/engine/**/*.ts', load: false },