diff --git a/CHANGELOG.md b/CHANGELOG.md index fd132b19..daeeb8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,23 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added new Timer events! + ```typescript + const timer = new ex.Timer({...}); + timer.events.on('complete', () => {...}); // after the last repeat + timer.events.on('action', () => {...}); // every fire of the timer + timer.events.on('start', () => {...}); // after the timer is started + timer.events.on('stop', () => {...}); // after the timer is stopped + timer.events.on('pause', () => {...}); // after every pause + timer.events.on('resume', () => {...}); // after every resume + timer.events.on('cancel', () => {...}); // after cancel + + // or specify the onComplete in the constructor + const timer2 = new ex.Timer({ + onComplete: () => {...}, + ... + }); + ``` - Added a way to configure general debug settings on text ```typescript class DebugConfig { diff --git a/src/engine/Timer.ts b/src/engine/Timer.ts index 9901dc93..d4d4cbdf 100644 --- a/src/engine/Timer.ts +++ b/src/engine/Timer.ts @@ -2,6 +2,32 @@ import type { Scene } from './Scene'; import { Logger } from './Util/Log'; import type * as ex from './index'; import { Random } from './Math/Random'; +import { EventEmitter } from './EventEmitter'; + +/** + * Built in events supported by all entities + */ +export interface TimerEvents { + start: void; + stop: void; + pause: void; + resume: void; + cancel: void; + + action: void; + complete: void; +} + +export const TimerEvents = { + Start: 'start', + Stop: 'stop', + Pause: 'pause', + Resume: 'resume', + Cancel: 'cancel', + + Action: 'action', + Complete: 'complete' +} as const; export interface TimerOptions { /** @@ -32,6 +58,10 @@ export interface TimerOptions { * Optionally provide a random instance to use for random behavior, otherwise a new random will be created seeded from the current time. */ random?: ex.Random; + /** + * Optionally provide a callback to fire once when the timer completes its last action callback. + */ + onComplete?: () => void; } /** @@ -42,6 +72,7 @@ export class Timer { private _logger = Logger.getInstance(); private static _MAX_ID: number = 0; public id: number = 0; + public events = new EventEmitter(); private _elapsedTime: number = 0; private _totalTimeAlive: number = 0; @@ -61,6 +92,8 @@ export class Timer { return this._baseInterval + this.random.integer(this.randomRange[0], this.randomRange[1]); }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + private _onComplete: () => void = () => {}; private _complete = false; public get complete() { return this._complete; @@ -75,6 +108,7 @@ export class Timer { const numberOfRepeats = options.numberOfRepeats; const randomRange = options.randomRange; const random = options.random; + this._onComplete = options.onComplete ?? this._onComplete; if (!!numberOfRepeats && numberOfRepeats >= 0) { this.maxNumberOfRepeats = numberOfRepeats; @@ -134,6 +168,8 @@ export class Timer { this._complete = true; this._running = false; this._elapsedTime = 0; + this._onComplete(); + this.events.emit('complete'); } if (!this.complete && this._elapsedTime >= this.interval) { @@ -141,12 +177,15 @@ export class Timer { c.call(this); }); this._numberOfTicks++; + this.events.emit('action'); if (this.repeats) { this._elapsedTime = 0; } else { this._complete = true; this._running = false; this._elapsedTime = 0; + this._onComplete(); + this.events.emit('complete'); } } } @@ -210,6 +249,7 @@ export class Timer { */ public pause(): Timer { this._running = false; + this.events.emit('pause'); return this; } @@ -218,6 +258,7 @@ export class Timer { */ public resume(): Timer { this._running = true; + this.events.emit('resume'); return this; } @@ -234,6 +275,8 @@ export class Timer { this._complete = false; this._elapsedTime = 0; this._numberOfTicks = 0; + } else { + this.events.emit('start'); } return this; @@ -246,6 +289,7 @@ export class Timer { this._running = false; this._elapsedTime = 0; this._numberOfTicks = 0; + this.events.emit('stop'); return this; } @@ -256,6 +300,7 @@ export class Timer { this.pause(); if (this.scene) { this.scene.cancelTimer(this); + this.events.emit('cancel'); } } } diff --git a/src/spec/vitest/TimerSpec.ts b/src/spec/vitest/TimerSpec.ts index fb2e2e23..160c1be6 100644 --- a/src/spec/vitest/TimerSpec.ts +++ b/src/spec/vitest/TimerSpec.ts @@ -595,4 +595,185 @@ describe('A Timer', () => { expect(timer.interval).toBe(905); timer.update(905); }); + + describe('events', () => { + it("fires 'action' event for every timer tick", () => { + const actionSpy = vi.fn(); + const actionEventSpy = vi.fn(); + + const sut = new ex.Timer({ + interval: 42, + repeats: true, + numberOfRepeats: 2, + action: actionSpy + }); + sut.events.on('action', actionEventSpy); + + scene.add(sut); + + expect(actionSpy).not.toHaveBeenCalled(); + expect(actionEventSpy).not.toHaveBeenCalled(); + sut.start(); + expect(actionSpy).not.toHaveBeenCalled(); + expect(actionEventSpy).not.toHaveBeenCalledOnce(); + + scene.update(engine, 43); + scene.update(engine, 43); + scene.update(engine, 43); + scene.update(engine, 43); + + expect(actionSpy).toHaveBeenCalledTimes(2); + expect(actionEventSpy).toHaveBeenCalledTimes(2); + }); + + it("fires 'complete' event when the timer has fired its last action", () => { + const completeSpy = vi.fn(); + const completeEventSpy = vi.fn(); + + const sut = new ex.Timer({ + interval: 42, + repeats: true, + numberOfRepeats: 2, + onComplete: completeSpy + }); + sut.events.on('complete', completeEventSpy); + + scene.add(sut); + + expect(completeSpy).not.toHaveBeenCalled(); + expect(completeEventSpy).not.toHaveBeenCalled(); + sut.start(); + expect(completeSpy).not.toHaveBeenCalled(); + expect(completeEventSpy).not.toHaveBeenCalledOnce(); + + scene.update(engine, 43); + scene.update(engine, 43); + scene.update(engine, 43); + scene.update(engine, 43); + + expect(completeSpy).toHaveBeenCalledTimes(1); + expect(completeEventSpy).toHaveBeenCalledTimes(1); + }); + + it("fires 'start' event when started", () => { + const startSpy = vi.fn(); + + const sut = new ex.Timer({ + interval: 42 + }); + sut.events.on('start', startSpy); + + scene.add(sut); + + expect(startSpy).not.toHaveBeenCalled(); + sut.start(); + expect(startSpy).toHaveBeenCalledOnce(); + }); + + it("fires 'pause' event when paused", () => { + const pauseSpy = vi.fn(); + + const sut = new ex.Timer({ + interval: 42 + }); + sut.events.on('pause', pauseSpy); + + scene.add(sut); + + expect(pauseSpy).not.toHaveBeenCalled(); + sut.start(); + expect(pauseSpy).not.toHaveBeenCalled(); + + scene.update(engine, 40); + + sut.pause(); + expect(pauseSpy).toHaveBeenCalledOnce(); + }); + + it("fires 'resume' event when resumed", () => { + const resumeSpy = vi.fn(); + + const sut = new ex.Timer({ + interval: 42 + }); + sut.events.on('resume', resumeSpy); + + scene.add(sut); + + expect(resumeSpy).not.toHaveBeenCalled(); + sut.start(); + expect(resumeSpy).not.toHaveBeenCalled(); + + scene.update(engine, 40); + + sut.pause(); + expect(resumeSpy).not.toHaveBeenCalledOnce(); + + scene.update(engine, 40); + sut.resume(); + + expect(resumeSpy).toHaveBeenCalledOnce(); + }); + + it("fires 'stop' event when stopped", () => { + const stopSpy = vi.fn(); + + const sut = new ex.Timer({ + interval: 42 + }); + sut.events.on('stop', stopSpy); + + scene.add(sut); + + expect(stopSpy).not.toHaveBeenCalled(); + sut.start(); + expect(stopSpy).not.toHaveBeenCalled(); + + scene.update(engine, 40); + + sut.pause(); + expect(stopSpy).not.toHaveBeenCalledOnce(); + + scene.update(engine, 40); + sut.resume(); + + expect(stopSpy).not.toHaveBeenCalledOnce(); + + scene.update(engine, 40); + sut.stop(); + + expect(stopSpy).toHaveBeenCalledOnce(); + }); + + it("fires 'cancel' event when cancelled", () => { + const cancelSpy = vi.fn(); + + const sut = new ex.Timer({ + interval: 42 + }); + sut.events.on('cancel', cancelSpy); + + scene.add(sut); + + expect(cancelSpy).not.toHaveBeenCalled(); + sut.start(); + expect(cancelSpy).not.toHaveBeenCalled(); + + scene.update(engine, 40); + + sut.pause(); + expect(cancelSpy).not.toHaveBeenCalledOnce(); + + scene.update(engine, 40); + sut.resume(); + + expect(cancelSpy).not.toHaveBeenCalledOnce(); + + scene.update(engine, 40); + sut.stop(); + sut.cancel(); + + expect(cancelSpy).toHaveBeenCalledOnce(); + }); + }); });