diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts index 6a5f213e6..9e397e612 100644 --- a/src/engine/Scene.ts +++ b/src/engine/Scene.ts @@ -494,18 +494,17 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact this.actors.push(entity); } // TODO remove after collision ecs - entity.children.forEach(c => this.add(c)); + entity.children.forEach((c) => this.add(c)); entity.childrenAdded$.register({ - notify: (e => { + notify: (e) => { this.add(e); - }) + } }); entity.childrenRemoved$.register({ - notify: (e => { + notify: (e) => { this.remove(e); - }) + } }); - } return; } @@ -577,7 +576,7 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact * @todo Should this be `ScreenElement` only? * @deprecated Use [[Scene.add]] */ - @obsolete({message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.add'}) + @obsolete({ message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.add' }) public addScreenElement(actor: Actor) { this.add(actor); } @@ -586,7 +585,7 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact * Removes an actor as a piece of UI * @deprecated Use [[Scene.remove]] */ - @obsolete({message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.remove'}) + @obsolete({ message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.remove' }) public removeScreenElement(actor: Actor) { this.remove(actor); } @@ -595,7 +594,7 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact * Adds a [[TileMap]] to the scene, once this is done the TileMap will be drawn and updated. * @deprecated Use [[Scene.add]] */ - @obsolete({message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.add'}) + @obsolete({ message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.add' }) public addTileMap(tileMap: TileMap) { this.tileMaps.push(tileMap); this.world.add(tileMap); @@ -605,7 +604,7 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact * Removes a [[TileMap]] from the scene, it will no longer be drawn or updated. * @deprecated Use [[Scene.remove]] */ - @obsolete({message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.remove'}) + @obsolete({ message: 'Will be removed in excalibur v0.26.0', alternateMethod: 'Use Scene.remove' }) public removeTileMap(tileMap: TileMap) { const index = this.tileMaps.indexOf(tileMap); if (index > -1) { @@ -668,7 +667,8 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact for (const actor of this.actors) { engine.stats.currFrame.actors.alive++; for (const child of actor.children) { - if (ActorUtils.isScreenElement(child as Actor)) { // TODO not true + if (ActorUtils.isScreenElement(child as Actor)) { + // TODO not true engine.stats.currFrame.actors.ui++; } else { engine.stats.currFrame.actors.alive++; diff --git a/src/engine/Timer.ts b/src/engine/Timer.ts index 71b84e245..e531080a2 100644 --- a/src/engine/Timer.ts +++ b/src/engine/Timer.ts @@ -1,4 +1,6 @@ import { Scene } from './Scene'; +import { obsolete } from './Util/Decorators'; +import { Logger } from './Util/Log'; export interface TimerOptions { repeats?: boolean; @@ -12,17 +14,26 @@ export interface TimerOptions { * after a certain interval, optionally repeating. */ export class Timer { - public static id: number = 0; + private _logger = Logger.getInstance(); + private static _MAX_ID: number = 0; public id: number = 0; - public interval: number = 10; - public repeats: boolean = false; - public maxNumberOfRepeats: number = -1; + private _elapsedTime: number = 0; private _totalTimeAlive: number = 0; - private _paused: boolean = false; + + private _running = false; + private _numberOfTicks: number = 0; private _callbacks: Array<() => void>; - public complete: boolean = false; + + public interval: number = 10; + public repeats: boolean = false; + public maxNumberOfRepeats: number = -1; + + private _complete = false; + public get complete() { + return this._complete; + } public scene: Scene = null; /** @@ -48,7 +59,7 @@ export class Timer { } } - this.id = Timer.id++; + this.id = Timer._MAX_ID++; this.interval = interval || this.interval; this.repeats = repeats || this.repeats; @@ -81,12 +92,14 @@ export class Timer { * @param delta Number of elapsed milliseconds since the last update. */ public update(delta: number) { - if (!this._paused) { + if (this._running) { this._totalTimeAlive += delta; this._elapsedTime += delta; if (this.maxNumberOfRepeats > -1 && this._numberOfTicks >= this.maxNumberOfRepeats) { - this.complete = true; + this._complete = true; + this._running = false; + this._elapsedTime = 0; } if (!this.complete && this._elapsedTime >= this.interval) { @@ -98,7 +111,9 @@ export class Timer { if (this.repeats) { this._elapsedTime = 0; } else { - this.complete = true; + this._complete = true; + this._running = false; + this._elapsedTime = 0; } } } @@ -106,6 +121,8 @@ export class Timer { /** * Resets the timer so that it can be reused, and optionally reconfigure the timers interval. + * + * Warning** you may need to call `timer.start()` again if the timer had completed * @param newInterval If specified, sets a new non-negative interval in milliseconds to refire the callback * @param newNumberOfRepeats If specified, sets a new non-negative upper limit to the number of time this timer executes */ @@ -121,7 +138,7 @@ export class Timer { } } - this.complete = false; + this._complete = false; this._elapsedTime = 0; this._numberOfTicks = 0; } @@ -135,23 +152,84 @@ export class Timer { } /** - * Pauses the timer so that no more time will be incremented towards the next call + * @returns milliseconds until the next action callback, if complete will return 0 + */ + public get timeToNextAction() { + if (this.complete) { + return 0; + } + return this.interval - this._elapsedTime; + } + + /** + * @returns milliseconds elapsed toward the next action + */ + public get timeElapsedTowardNextAction() { + return this._elapsedTime; + } + + public get isRunning() { + return this._running; + } + + /** + * Pauses the timer, time will no longer increment towards the next call */ - public pause() { - this._paused = true; + public pause(): Timer { + this._running = false; + return this; } /** * Unpauses the timer. Time will now increment towards the next call + * @deprecated Will be removed in v0.26.0 */ + @obsolete({ message: 'Will be removed in v0.26.0', alternateMethod: 'Use Timer.resume()' }) public unpause() { - this._paused = false; + this._running = true; + } + + /** + * Resumes the timer, time will now increment towards the next call. + */ + public resume(): Timer { + this._running = true; + return this; + } + + /** + * Starts the timer, if the timer was complete it will restart the timer and reset the elapsed time counter + */ + public start(): Timer { + if (!this.scene) { + this._logger.warn('Cannot start a timer not part of a scene, timer wont start until added'); + } + + this._running = true; + if (this.complete) { + this._complete = false; + this._elapsedTime = 0; + this._numberOfTicks = 0; + } + + return this; + } + + /** + * Stops the timer and resets the elapsed time counter towards the next action invocation + */ + public stop(): Timer { + this._running = false; + this._elapsedTime = 0; + this._numberOfTicks = 0; + return this; } /** * Cancels the timer, preventing any further executions. */ public cancel() { + this.pause(); if (this.scene) { this.scene.cancelTimer(this); } diff --git a/src/spec/CollisionSpec.ts b/src/spec/CollisionSpec.ts index f4dd2ef4e..0ac2f5795 100644 --- a/src/spec/CollisionSpec.ts +++ b/src/spec/CollisionSpec.ts @@ -182,7 +182,7 @@ describe('A Collision', () => { it('should recognize when actor bodies are touching', () => { let touching = false; - actor1.on('postupdate', function() { + actor1.on('postupdate', function () { if (actor1.body.collider.touching(actor2.body.collider)) { touching = true; } @@ -209,17 +209,17 @@ describe('A Collision', () => { engine.add(passiveBlock); const collisionHandler = (ev: ex.PreCollisionEvent) => { - engine.add( - new ex.Timer({ - interval: 30, - fcn: () => { - expect(activeBlock.vel.x).toBeGreaterThan(0); - expect(passiveBlock.vel.x).toBeLessThan(0); - done(); - }, - repeats: false - }) - ); + const timer = new ex.Timer({ + interval: 30, + fcn: () => { + expect(activeBlock.vel.x).toBeGreaterThan(0); + expect(passiveBlock.vel.x).toBeLessThan(0); + done(); + }, + repeats: false + }); + timer.start(); + engine.add(timer); }; activeBlock.once('precollision', collisionHandler); @@ -337,7 +337,7 @@ describe('A Collision', () => { passiveBlock.vel = ex.vec(-100, activeBlock.vel.y); engine.add(passiveBlock); - const collisionEnd = function() { + const collisionEnd = function () { expect(this).toBe(activeBlock); done(); }; @@ -362,7 +362,7 @@ describe('A Collision', () => { passiveBlock.vel = ex.vec(-100, activeBlock.vel.y); engine.add(passiveBlock); - const collisionEnd = function() { + const collisionEnd = function () { expect(this).toBe(activeBlock); done(); }; diff --git a/src/spec/SceneSpec.ts b/src/spec/SceneSpec.ts index f753da07c..b0c58862b 100644 --- a/src/spec/SceneSpec.ts +++ b/src/spec/SceneSpec.ts @@ -444,6 +444,7 @@ describe('A scene', () => { }); scene.add(timer); + timer.start(); scene.update(engine, 11); scene.draw(engine.ctx, 11); @@ -479,6 +480,7 @@ describe('A scene', () => { }); scene.add(timer); + timer.start(); scene.update(engine, 11); scene.draw(engine.ctx, 11); @@ -537,6 +539,7 @@ describe('A scene', () => { }); scene.add(timer); + timer.start(); scene.update(engine, 11); scene.draw(engine.ctx, 11); diff --git a/src/spec/TimerSpec.ts b/src/spec/TimerSpec.ts index 96d9b7204..89f19ce1e 100644 --- a/src/spec/TimerSpec.ts +++ b/src/spec/TimerSpec.ts @@ -41,66 +41,140 @@ describe('A Timer', () => { expect(timer.id).toBe(newtimer2.id - 2); }); + it('does not start when added to a scene', () => { + const sut = new ex.Timer({ + interval: 42, + fcn: () => { + /* nothing */ + } + }); + + const scene = new ex.Scene(); + + scene.add(sut); + + expect(sut.isRunning).toBe(false); + }); + + it('can be paused and resumed', () => { + const timerSpy = jasmine.createSpy('timer'); + const sut = new ex.Timer({ + interval: 42, + fcn: timerSpy + }); + + scene.add(sut); + sut.start(); + + scene.update(engine, 40); + expect(sut.timeToNextAction).toBe(2); + expect(sut.timeElapsedTowardNextAction).toBe(40); + + sut.pause(); + scene.update(engine, 40); + expect(sut.timeToNextAction).toBe(2); + expect(sut.timeElapsedTowardNextAction).toBe(40); + expect(timerSpy).not.toHaveBeenCalled(); + + sut.resume(); + scene.update(engine, 40); + expect(timerSpy).toHaveBeenCalledTimes(1); + expect(sut.complete).toBe(true); + expect(sut.isRunning).toBe(false); + expect(sut.timeToNextAction).toBe(0); + expect(sut.timeElapsedTowardNextAction).toBe(0); + }); + + it('can be stopped and started', () => { + const timerSpy = jasmine.createSpy('timer'); + const sut = new ex.Timer({ + interval: 42, + fcn: timerSpy + }); + + scene.add(sut); + sut.start(); + + scene.update(engine, 40); + expect(sut.timeToNextAction).toBe(2); + expect(sut.timeElapsedTowardNextAction).toBe(40); + + sut.stop(); + expect(sut.timeElapsedTowardNextAction).toBe(0); + expect(sut.timeToNextAction).toBe(42); + expect(sut.complete).toBe(false); + expect(sut.isRunning).toBe(false); + + sut.start(); + scene.update(engine, 40); + expect(sut.timeToNextAction).toBe(2); + expect(sut.timeElapsedTowardNextAction).toBe(40); + }); + it('fires after a specific interval', () => { - //scene.addTimer(timer); - //scene.update(null, 501); - timer.update(501); - timer.update(501); - expect(timer.complete).toBeTruthy(); + const timerSpy = jasmine.createSpy('timer'); + const sut = new ex.Timer({ + interval: 500, + fcn: timerSpy + }); + sut.start(); + sut.update(501); + sut.update(501); + expect(sut.complete).toBe(true); + expect(timerSpy).toHaveBeenCalledTimes(1); }); it('can repeat itself indefinitely at a specified interval', () => { - // count the number of fires - let count = 0; + const timerSpy = jasmine.createSpy('timer'); timer = new ex.Timer({ interval: 500, - fcn: function () { - count++; - }, + fcn: timerSpy, repeats: true }); + timer.start(); timer.update(501); timer.update(501); timer.update(501); - expect(count).toBe(3); + expect(timerSpy).toHaveBeenCalledTimes(3); }); it('can repeat itself a finite number of times', () => { - // count the number of fires + const timerSpy = jasmine.createSpy('timer'); timer = new ex.Timer({ interval: 500, - fcn: function () { - const dummy = 0; - }, + fcn: timerSpy, repeats: true, numberOfRepeats: 2 }); + timer.start(); + timer.update(501); + timer.update(501); timer.update(501); timer.update(501); timer.update(501); - expect(timer.timesRepeated).toBe(2); + expect(timerSpy).toHaveBeenCalledTimes(2); }); it('can return how long it has been running', () => { + timer.start(); timer.update(372); expect(timer.getTimeRunning()).toEqual(372); }); it('can be canceled', () => { - let count = 0; + const timerSpy = jasmine.createSpy('timer'); timer = new ex.Timer({ interval: 500, - fcn: function () { - count++; - }, + fcn: timerSpy, repeats: true }); scene.addTimer(timer); + timer.start(); scene.update(engine, 501); scene.update(engine, 501); @@ -110,11 +184,12 @@ describe('A Timer', () => { scene.update(engine, 501); scene.update(engine, 501); - expect(count).toBe(3); + expect(timerSpy).toHaveBeenCalledTimes(3); }); it('is no longer active in the scene when it is completed', () => { scene.addTimer(timer); + timer.start(); expect(scene.isTimerActive(timer)).toBeTruthy(); scene.update(engine, 501); @@ -123,64 +198,63 @@ describe('A Timer', () => { it('is in the completed state once it is finished', () => { scene.addTimer(timer); + timer.start(); scene.update(engine, 501); expect(timer.complete).toBeTruthy(); }); it('has no completed state when running forever', () => { - // count the number of fires - let count = 0; + const timerSpy = jasmine.createSpy('timer'); timer = new ex.Timer({ interval: 500, - fcn: function () { - count++; - }, + fcn: timerSpy, repeats: true }); scene.addTimer(timer); + timer.start(); scene.update(engine, 501); - expect(count).toBe(1); + expect(timerSpy).toHaveBeenCalledTimes(1); expect(timer.repeats).toBeTruthy(); expect(timer.complete).toBeFalsy(); scene.update(engine, 501); - expect(count).toBe(2); + expect(timerSpy).toHaveBeenCalledTimes(2); expect(timer.repeats).toBeTruthy(); expect(timer.complete).toBeFalsy(); scene.update(engine, 501); - expect(count).toBe(3); + expect(timerSpy).toHaveBeenCalledTimes(3); expect(timer.repeats).toBeTruthy(); expect(timer.complete).toBeFalsy(); }); it('can be reset at the same interval', () => { - let count = 0; + const timerSpy = jasmine.createSpy('timer'); // non-repeating timer timer = new ex.Timer({ interval: 500, - fcn: function () { - count++; - }, + fcn: timerSpy, repeats: false }); scene.add(timer); + timer.start(); // tick the timer scene.update(engine, 501); - expect(count).toBe(1); + expect(timerSpy).toHaveBeenCalledTimes(1); // tick the timer again, but it shouldn't fire until reset scene.update(engine, 501); - expect(count).toBe(1); + expect(timerSpy).toHaveBeenCalledTimes(1); expect(timer.complete).toBe(true); // once reset the timer should fire again timer.reset(); + timer.start(); expect(timer.complete).toBe(false); scene.update(engine, 501); - expect(count).toBe(2); + expect(timerSpy).toHaveBeenCalledTimes(2); }); it('can be reset at a different interval', () => { @@ -194,6 +268,7 @@ describe('A Timer', () => { repeats: false }); scene.add(timer); + timer.start(); // tick the timer scene.update(engine, 501); @@ -206,6 +281,7 @@ describe('A Timer', () => { // once reset at a larger the timer should fire again timer.reset(900); + timer.start(); expect(timer.complete).toBe(false); scene.update(engine, 901); expect(count).toBe(2); @@ -222,6 +298,7 @@ describe('A Timer', () => { repeats: true }); scene.add(timer); + timer.start(); // tick the timer scene.update(engine, 501); @@ -247,6 +324,7 @@ describe('A Timer', () => { numberOfRepeats: 3 }); scene.add(timer); + timer.start(); // tick the timer scene.update(engine, 501); @@ -261,6 +339,7 @@ describe('A Timer', () => { // once reset the timer should fire again timer.reset(500, 4); + timer.start(); expect(timer.complete).toBe(false); scene.update(engine, 501); scene.update(engine, 501); @@ -280,6 +359,7 @@ describe('A Timer', () => { repeats: true }); scene.add(timer); + timer.start(); // act timer.pause(); @@ -301,11 +381,12 @@ describe('A Timer', () => { repeats: true }); scene.add(timer); + timer.start(); // act timer.pause(); scene.update(engine, 200); - timer.unpause(); + timer.resume(); scene.update(engine, 100); scene.update(engine, 100); @@ -322,6 +403,7 @@ describe('A Timer', () => { repeats: true }); scene.add(timer); + timer.start(); scene.update(engine, 100); // assert @@ -345,6 +427,8 @@ describe('A Timer', () => { }); scene.add(timer); + timer.start(); + timer.on(() => { count++; }); @@ -374,6 +458,7 @@ describe('A Timer', () => { }); scene.add(timer); + timer.start(); const fnc = () => { count++;