diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b39d8d8..35692a1f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added -- +- Added `actionstart` and `actioncomplete` events to the Actor that are fired when an action starts and completes ### Fixed diff --git a/src/engine/Actions/ActionQueue.ts b/src/engine/Actions/ActionQueue.ts index a643407d7..78aaf0b62 100644 --- a/src/engine/Actions/ActionQueue.ts +++ b/src/engine/Actions/ActionQueue.ts @@ -1,3 +1,4 @@ +import { ActionCompleteEvent, ActionStartEvent } from '../Events'; import { Entity } from '../EntityComponentSystem/Entity'; import { Action } from './Action'; @@ -90,10 +91,15 @@ export class ActionQueue { */ public update(elapsedMs: number) { if (this._actions.length > 0) { - this._currentAction = this._actions[0]; + if (this._currentAction !== this._actions[0]) { + this._currentAction = this._actions[0]; + this._entity.emit('actionstart', new ActionStartEvent(this._currentAction, this._entity)); + } + this._currentAction.update(elapsedMs); if (this._currentAction.isComplete(this._entity)) { + this._entity.emit('actioncomplete', new ActionCompleteEvent(this._currentAction, this._entity)); this._completedActions.push(this._actions.shift()); } } diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 19931ef31..31d14458b 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -13,7 +13,9 @@ import { PreDrawEvent, PostDrawEvent, PreDebugDrawEvent, - PostDebugDrawEvent + PostDebugDrawEvent, + ActionStartEvent, + ActionCompleteEvent } from './Events'; import { Engine } from './Engine'; import { Color } from './Color'; @@ -166,6 +168,8 @@ export type ActorEvents = EntityEvents & { pointerdragmove: PointerEvent; enterviewport: EnterViewPortEvent; exitviewport: ExitViewPortEvent; + actionstart: ActionStartEvent; + actioncomplete: ActionCompleteEvent; } export const ActorEvents = { @@ -193,7 +197,9 @@ export const ActorEvents = { PointerDragLeave: 'pointerdragleave', PointerDragMove: 'pointerdragmove', EnterViewPort: 'enterviewport', - ExitViewPort: 'exitviewport' + ExitViewPort: 'exitviewport', + ActionStart: 'actionstart', + ActionComplete: 'actioncomplete' }; /** diff --git a/src/engine/Events.ts b/src/engine/Events.ts index 2e55fc586..07dedd0b3 100644 --- a/src/engine/Events.ts +++ b/src/engine/Events.ts @@ -13,6 +13,7 @@ import { OnInitialize, OnPreUpdate, OnPostUpdate, SceneActivationContext } from import { BodyComponent } from './Collision/BodyComponent'; import { ExcaliburGraphicsContext } from './Graphics'; import { Axes, Buttons, Gamepad } from './Input/Gamepad'; +import { Action } from './Actions/Action'; export enum EventTypes { Kill = 'kill', @@ -80,7 +81,10 @@ export enum EventTypes { PointerDragEnd = 'pointerdragend', PointerDragEnter = 'pointerdragenter', PointerDragLeave = 'pointerdragleave', - PointerDragMove = 'pointerdragmove' + PointerDragMove = 'pointerdragmove', + + ActionStart = 'actionstart', + ActionComplete = 'actioncomplete' } /* istanbul ignore next */ @@ -545,3 +549,21 @@ export class ExitTriggerEvent extends GameEvent { super(); } } + +/** + * Event thrown on an [[Actor]] when an action starts. + */ +export class ActionStartEvent extends GameEvent { + constructor(public action: Action, public target: Entity) { + super(); + } +} + +/** + * Event thrown on an [[Actor]] when an action completes. + */ +export class ActionCompleteEvent extends GameEvent { + constructor(public action: Action, public target: Entity) { + super(); + } +} diff --git a/src/spec/ActionSpec.ts b/src/spec/ActionSpec.ts index 96ccbf1d5..1a2936c8f 100644 --- a/src/spec/ActionSpec.ts +++ b/src/spec/ActionSpec.ts @@ -1244,4 +1244,51 @@ describe('Action', () => { expect(actor.graphics.opacity).toBe(0); }); }); + + describe('events', () => { + it('emits actionstart event', () => { + const spy = jasmine.createSpy(); + actor.actions.moveTo(20, 0, 20); + actor.on('actionstart', spy); + scene.update(engine, 500); + scene.update(engine, 500); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.MoveTo) })); + }); + + it('emits actioncomplete event', () => { + const spy = jasmine.createSpy(); + actor.actions.moveTo(20, 0, 20); + actor.on('actioncomplete', spy); + for (let i = 0; i < 10; i++) { + scene.update(engine, 200); + } + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.MoveTo) })); + }); + + it('emits actionstart and actioncomplete events for each action in a repeat', () => { + const startSpy = jasmine.createSpy(); + const completeSpy = jasmine.createSpy(); + actor.actions.repeat((ctx) => ctx.moveTo(20, 0, 20).moveTo(0, 0, 20), 1); + actor.on('actionstart', startSpy); + actor.on('actioncomplete', completeSpy); + + for (let i = 0; i < 10; i++) { + scene.update(engine, 500); + } + + const startCalls = startSpy.calls.all(); + expect(startSpy).toHaveBeenCalledTimes(3); + expect(startCalls[0].args[0]).toEqual(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.Repeat) })); + expect(startCalls[1].args[0]).toEqual(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.MoveTo) })); + expect(startCalls[2].args[0]).toEqual(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.MoveTo) })); + + const completeCalls = completeSpy.calls.all(); + expect(completeSpy).toHaveBeenCalledTimes(3); + expect(completeCalls[0].args[0]).toEqual(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.MoveTo) })); + expect(completeCalls[1].args[0]).toEqual(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.MoveTo) })); + expect(completeCalls[2].args[0]).toEqual(jasmine.objectContaining({ target: actor, action: jasmine.any(ex.Repeat) })); + }); + }); });