diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb5e051..c467e80e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes +- `Actor.actions.repeat()` and `Actor.actions.repeatForever()` now require a handler that specifies the actions to repeat. This is more clear and helps prevent bugs like #1891 + + ```typescript + const actor = new ex.Actor(); + + actor.actions + // Move up in a zig-zag by repeating 5 times + .repeat((ctx) => { + ctx.moveBy(10, 0, 10); + ctx.moveBy(0, 10, 10); + }, 5) + .callMethod(() => { + console.log('Done repeating!'); + }); + ``` + - Removes `Entity.components` as a way to access, add, and remove components - Camera.z has been renamed to property `zoom` which is the zoom factor - Camera.zoom(...) has been renamed to function `zoomOverTime()` @@ -63,6 +79,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed N+1 repeat/repeatForever bug ([#1891](https://github.com/excaliburjs/Excalibur/issues/1891)) +- Fixed repeat/repeatForever issue with `rotateTo` ([#635](https://github.com/excaliburjs/Excalibur/issues/635)) - Entity update lifecycle is now called correctly - Fixed GraphicsSystem `enterviewport` and `exitviewport` event - Fixed DOM element leak when restarting games, play button elements piled up in the DOM. diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index 60b9e8f7..bf3d3b39 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -390,19 +390,19 @@ for (var i = 0; i < 36; i++) { var platform = new ex.Actor(400, 300, 200, 50, new ex.Color(0, 200, 0)); platform.graphics.add(new ex.Graphics.Rectangle({ color: new ex.Color(0, 200, 0), width: 200, height: 50 })); platform.body.collider.type = ex.CollisionType.Fixed; -platform.actions.moveTo(200, 300, 100).moveTo(600, 300, 100).moveTo(400, 300, 100).repeatForever(); +platform.actions.repeatForever(ctx => ctx.moveTo(200, 300, 100).moveTo(600, 300, 100).moveTo(400, 300, 100)); game.add(platform); var platform2 = new ex.Actor(800, 300, 200, 20, new ex.Color(0, 0, 140)); platform2.graphics.add(new ex.Graphics.Rectangle({ color: new ex.Color(0, 0, 140), width: 200, height: 20 })); platform2.body.collider.type = ex.CollisionType.Fixed; -platform2.actions.moveTo(2000, 300, 100).moveTo(2000, 100, 100).moveTo(800, 100, 100).moveTo(800, 300, 100).repeatForever(); +platform2.actions.repeatForever(ctx => ctx.moveTo(2000, 300, 100).moveTo(2000, 100, 100).moveTo(800, 100, 100).moveTo(800, 300, 100)); game.add(platform2); var platform3 = new ex.Actor(-200, 400, 200, 20, new ex.Color(50, 0, 100)); platform3.graphics.add(new ex.Graphics.Rectangle({ color: new ex.Color(50, 0, 100), width: 200, height: 20 })); platform3.body.collider.type = ex.CollisionType.Fixed; -platform3.actions.moveTo(-200, 800, 300).moveTo(-200, 400, 50).delay(3000).moveTo(-200, 300, 800).moveTo(-200, 400, 800).repeatForever(); +platform3.actions.repeatForever(ctx => ctx.moveTo(-200, 800, 300).moveTo(-200, 400, 50).delay(3000).moveTo(-200, 300, 800).moveTo(-200, 400, 800)); game.add(platform3); var platform4 = new ex.Actor(75, 300, 100, 50, ex.Color.Azure); diff --git a/sandbox/tests/camera/strategy.ts b/sandbox/tests/camera/strategy.ts index 3eb917f0..f3bd1079 100644 --- a/sandbox/tests/camera/strategy.ts +++ b/sandbox/tests/camera/strategy.ts @@ -7,9 +7,7 @@ var game = new ex.Engine({ }); var actor = new ex.Actor(100, 100, 50, 50, ex.Color.Red); -actor.actions.moveTo(300, 300, 100); -actor.actions.moveTo(100, 100, 100); -actor.actions.repeatForever(); +actor.actions.repeatForever(ctx => ctx.moveTo(300, 300, 100).moveTo(100, 100, 100)); game.add(actor); diff --git a/sandbox/tests/collision/index.ts b/sandbox/tests/collision/index.ts index 7278634f..6c5909f7 100644 --- a/sandbox/tests/collision/index.ts +++ b/sandbox/tests/collision/index.ts @@ -13,10 +13,10 @@ active.on('postupdate', () => { var fixed = new ex.Actor(0, 50, 100, 100, ex.Color.Green); fixed.body.collider.type = ex.CollisionType.Fixed; -fixed.actions - .moveTo(0, 100, 300) - .moveTo(0, 50, 300) - .repeatForever(); +fixed.actions.repeatForever(ctx => { + ctx.moveTo(0, 100, 300) + .moveTo(0, 50, 300) +}); engine.add(active); engine.add(fixed); diff --git a/sandbox/tests/engine/timescale.ts b/sandbox/tests/engine/timescale.ts index d44771b9..674ca556 100644 --- a/sandbox/tests/engine/timescale.ts +++ b/sandbox/tests/engine/timescale.ts @@ -20,10 +20,10 @@ game.on('postupdate', () => { game.start().then(() => { var rect = new ex.Actor(200, 200, 50, 50, ex.Color.Red); - rect.actions - .easeTo(300, 200, 600, ex.EasingFunctions.EaseOutCubic) - .easeTo(200, 200, 600, ex.EasingFunctions.EaseOutCubic) - .repeatForever(); + rect.actions.repeatForever(ctx => { + ctx.easeTo(300, 200, 600, ex.EasingFunctions.EaseOutCubic) + .easeTo(200, 200, 600, ex.EasingFunctions.EaseOutCubic) + }); game.add(rect); }); diff --git a/src/engine/Actions/Action.ts b/src/engine/Actions/Action.ts index 5c48c96b..1d62386a 100644 --- a/src/engine/Actions/Action.ts +++ b/src/engine/Actions/Action.ts @@ -1,10 +1,4 @@ -import { RotationType } from './RotationType'; - import { Actor } from '../Actor'; -import { vec, Vector } from '../Algebra'; -import { Logger } from '../Util/Log'; -import * as Util from '../Util/Util'; -import { obsolete } from '../Util/Decorators'; /** * Used for implementing actions for the [[ActionContext|Action API]]. @@ -15,1006 +9,3 @@ export interface Action { reset(): void; stop(): void; } - -export class EaseTo implements Action { - private _currentLerpTime: number = 0; - private _lerpDuration: number = 1 * 1000; // 1 second - private _lerpStart: Vector = new Vector(0, 0); - private _lerpEnd: Vector = new Vector(0, 0); - private _initialized: boolean = false; - private _stopped: boolean = false; - private _distance: number = 0; - constructor( - public actor: Actor, - x: number, - y: number, - duration: number, - public easingFcn: (currentTime: number, startValue: number, endValue: number, duration: number) => number - ) { - this._lerpDuration = duration; - this._lerpEnd = new Vector(x, y); - } - private _initialize() { - this._lerpStart = new Vector(this.actor.pos.x, this.actor.pos.y); - this._currentLerpTime = 0; - this._distance = this._lerpStart.distance(this._lerpEnd); - } - - public update(delta: number): void { - if (!this._initialized) { - this._initialize(); - this._initialized = true; - } - - // Need to update lerp time first, otherwise the first update will always be zero - this._currentLerpTime += delta; - let newX = this.actor.pos.x; - let newY = this.actor.pos.y; - if (this._currentLerpTime < this._lerpDuration) { - if (this._lerpEnd.x < this._lerpStart.x) { - newX = - this._lerpStart.x - - (this.easingFcn(this._currentLerpTime, this._lerpEnd.x, this._lerpStart.x, this._lerpDuration) - this._lerpEnd.x); - } else { - newX = this.easingFcn(this._currentLerpTime, this._lerpStart.x, this._lerpEnd.x, this._lerpDuration); - } - - if (this._lerpEnd.y < this._lerpStart.y) { - newY = - this._lerpStart.y - - (this.easingFcn(this._currentLerpTime, this._lerpEnd.y, this._lerpStart.y, this._lerpDuration) - this._lerpEnd.y); - } else { - newY = this.easingFcn(this._currentLerpTime, this._lerpStart.y, this._lerpEnd.y, this._lerpDuration); - } - // Given the lerp position figure out the velocity in pixels per second - this.actor.vel = vec((newX - this.actor.pos.x) / (delta / 1000), (newY - this.actor.pos.y) / (delta / 1000)); - } else { - this.actor.pos = vec(this._lerpEnd.x, this._lerpEnd.y); - this.actor.vel = Vector.Zero; - } - } - public isComplete(actor: Actor): boolean { - return this._stopped || new Vector(actor.pos.x, actor.pos.y).distance(this._lerpStart) >= this._distance; - } - - public reset(): void { - this._initialized = false; - } - public stop(): void { - this.actor.vel = vec(0, 0); - this._stopped = true; - } -} - -export class MoveTo implements Action { - private _actor: Actor; - public x: number; - public y: number; - private _start: Vector; - private _end: Vector; - private _dir: Vector; - private _speed: number; - private _distance: number; - private _started = false; - private _stopped = false; - constructor(actor: Actor, destx: number, desty: number, speed: number) { - this._actor = actor; - this._end = new Vector(destx, desty); - this._speed = speed; - } - - public update(_delta: number): void { - if (!this._started) { - this._started = true; - this._start = new Vector(this._actor.pos.x, this._actor.pos.y); - this._distance = this._start.distance(this._end); - this._dir = this._end.sub(this._start).normalize(); - } - const m = this._dir.scale(this._speed); - this._actor.vel = vec(m.x, m.y); - - if (this.isComplete(this._actor)) { - this._actor.pos = vec(this._end.x, this._end.y); - this._actor.vel = vec(0, 0); - } - } - - public isComplete(actor: Actor): boolean { - return this._stopped || new Vector(actor.pos.x, actor.pos.y).distance(this._start) >= this._distance; - } - - public stop(): void { - this._actor.vel = vec(0, 0); - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -export class MoveBy implements Action { - private _actor: Actor; - public x: number; - public y: number; - private _distance: number; - private _speed: number; - - private _start: Vector; - private _offset: Vector; - private _end: Vector; - private _dir: Vector; - private _started = false; - private _stopped = false; - - constructor(actor: Actor, offsetX: number, offsetY: number, speed: number) { - this._actor = actor; - this._speed = speed; - this._offset = new Vector(offsetX, offsetY); - if (speed <= 0) { - Logger.getInstance().error('Attempted to moveBy with speed less than or equal to zero : ' + speed); - throw new Error('Speed must be greater than 0 pixels per second'); - } - } - - public update(_delta: number) { - if (!this._started) { - this._started = true; - this._start = new Vector(this._actor.pos.x, this._actor.pos.y); - this._end = this._start.add(this._offset); - this._distance = this._offset.size; - this._dir = this._end.sub(this._start).normalize(); - } - - this._actor.vel = this._dir.scale(this._speed); - - if (this.isComplete(this._actor)) { - this._actor.pos = vec(this._end.x, this._end.y); - this._actor.vel = vec(0, 0); - } - } - - public isComplete(actor: Actor): boolean { - return this._stopped || actor.pos.distance(this._start) >= this._distance; - } - - public stop(): void { - this._actor.vel = vec(0, 0); - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -export class Follow implements Action { - private _actor: Actor; - private _actorToFollow: Actor; - public x: number; - public y: number; - private _current: Vector; - private _end: Vector; - private _dir: Vector; - private _speed: number; - private _maximumDistance: number; - private _distanceBetween: number; - private _started = false; - private _stopped = false; - - constructor(actor: Actor, actorToFollow: Actor, followDistance?: number) { - this._actor = actor; - this._actorToFollow = actorToFollow; - this._current = new Vector(this._actor.pos.x, this._actor.pos.y); - this._end = new Vector(actorToFollow.pos.x, actorToFollow.pos.y); - this._maximumDistance = followDistance !== undefined ? followDistance : this._current.distance(this._end); - this._speed = 0; - } - - public update(_delta: number): void { - if (!this._started) { - this._started = true; - this._distanceBetween = this._current.distance(this._end); - this._dir = this._end.sub(this._current).normalize(); - } - - const actorToFollowSpeed = Math.sqrt(Math.pow(this._actorToFollow.vel.x, 2) + Math.pow(this._actorToFollow.vel.y, 2)); - if (actorToFollowSpeed !== 0) { - this._speed = actorToFollowSpeed; - } - this._current = vec(this._actor.pos.x, this._actor.pos.y); - - this._end = vec(this._actorToFollow.pos.x, this._actorToFollow.pos.y); - this._distanceBetween = this._current.distance(this._end); - this._dir = this._end.sub(this._current).normalize(); - - if (this._distanceBetween >= this._maximumDistance) { - const m = this._dir.scale(this._speed); - this._actor.vel = vec(m.x, m.y); - } else { - this._actor.vel = vec(0, 0); - } - - if (this.isComplete()) { - this._actor.pos = vec(this._end.x, this._end.y); - this._actor.vel = vec(0, 0); - } - } - - public stop(): void { - this._actor.vel = vec(0, 0); - this._stopped = true; - } - - public isComplete(): boolean { - // the actor following should never stop unless specified to do so - return this._stopped; - } - - public reset(): void { - this._started = false; - } -} - -export class Meet implements Action { - private _actor: Actor; - private _actorToMeet: Actor; - public x: number; - public y: number; - private _current: Vector; - private _end: Vector; - private _dir: Vector; - private _speed: number; - private _distanceBetween: number; - private _started = false; - private _stopped = false; - private _speedWasSpecified = false; - - constructor(actor: Actor, actorToMeet: Actor, speed?: number) { - this._actor = actor; - this._actorToMeet = actorToMeet; - this._current = new Vector(this._actor.pos.x, this._actor.pos.y); - this._end = new Vector(actorToMeet.pos.x, actorToMeet.pos.y); - this._speed = speed || 0; - - if (speed !== undefined) { - this._speedWasSpecified = true; - } - } - - public update(_delta: number): void { - if (!this._started) { - this._started = true; - this._distanceBetween = this._current.distance(this._end); - this._dir = this._end.sub(this._current).normalize(); - } - - const actorToMeetSpeed = Math.sqrt(Math.pow(this._actorToMeet.vel.x, 2) + Math.pow(this._actorToMeet.vel.y, 2)); - if (actorToMeetSpeed !== 0 && !this._speedWasSpecified) { - this._speed = actorToMeetSpeed; - } - this._current = vec(this._actor.pos.x, this._actor.pos.y); - - this._end = vec(this._actorToMeet.pos.x, this._actorToMeet.pos.y); - this._distanceBetween = this._current.distance(this._end); - this._dir = this._end.sub(this._current).normalize(); - - const m = this._dir.scale(this._speed); - this._actor.vel = vec(m.x, m.y); - - if (this.isComplete()) { - this._actor.pos = vec(this._end.x, this._end.y); - this._actor.vel = vec(0, 0); - } - } - - public isComplete(): boolean { - return this._stopped || this._distanceBetween <= 1; - } - - public stop(): void { - this._actor.vel = vec(0, 0); - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -export class RotateTo implements Action { - private _actor: Actor; - public x: number; - public y: number; - private _start: number; - private _end: number; - private _speed: number; - private _rotationType: RotationType; - private _direction: number; - private _distance: number; - private _shortDistance: number; - private _longDistance: number; - private _shortestPathIsPositive: boolean; - private _currentNonCannonAngle: number; - private _started = false; - private _stopped = false; - constructor(actor: Actor, angleRadians: number, speed: number, rotationType?: RotationType) { - this._actor = actor; - this._end = angleRadians; - this._speed = speed; - this._rotationType = rotationType || RotationType.ShortestPath; - } - - public update(_delta: number): void { - if (!this._started) { - this._started = true; - this._start = this._actor.rotation; - this._currentNonCannonAngle = this._actor.rotation; - const distance1 = Math.abs(this._end - this._start); - const distance2 = Util.TwoPI - distance1; - if (distance1 > distance2) { - this._shortDistance = distance2; - this._longDistance = distance1; - } else { - this._shortDistance = distance1; - this._longDistance = distance2; - } - - this._shortestPathIsPositive = (this._start - this._end + Util.TwoPI) % Util.TwoPI >= Math.PI; - - switch (this._rotationType) { - case RotationType.ShortestPath: - this._distance = this._shortDistance; - if (this._shortestPathIsPositive) { - this._direction = 1; - } else { - this._direction = -1; - } - break; - case RotationType.LongestPath: - this._distance = this._longDistance; - if (this._shortestPathIsPositive) { - this._direction = -1; - } else { - this._direction = 1; - } - break; - case RotationType.Clockwise: - this._direction = 1; - if (this._shortestPathIsPositive) { - this._distance = this._shortDistance; - } else { - this._distance = this._longDistance; - } - break; - case RotationType.CounterClockwise: - this._direction = -1; - if (!this._shortestPathIsPositive) { - this._distance = this._shortDistance; - } else { - this._distance = this._longDistance; - } - break; - } - } - - this._actor.rx = this._direction * this._speed; - this._currentNonCannonAngle += (this._direction * this._speed) * (_delta / 1000); - - if (this.isComplete()) { - this._actor.rotation = this._end; - this._actor.rx = 0; - this._stopped = true; - } - } - - public isComplete(): boolean { - const distanceTravelled = Math.abs(this._currentNonCannonAngle - this._start); - return this._stopped || distanceTravelled >= Math.abs(this._distance); - } - - public stop(): void { - this._actor.rx = 0; - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -export class RotateBy implements Action { - private _actor: Actor; - public x: number; - public y: number; - private _start: number; - private _end: number; - private _speed: number; - private _offset: number; - - private _rotationType: RotationType; - private _direction: number; - private _distance: number; - private _shortDistance: number; - private _longDistance: number; - private _shortestPathIsPositive: boolean; - private _currentNonCannonAngle: number; - private _started = false; - private _stopped = false; - constructor(actor: Actor, angleRadiansOffset: number, speed: number, rotationType?: RotationType) { - this._actor = actor; - this._speed = speed; - this._offset = angleRadiansOffset; - this._rotationType = rotationType || RotationType.ShortestPath; - } - - public update(_delta: number): void { - if (!this._started) { - this._started = true; - this._start = this._actor.rotation; - this._currentNonCannonAngle = this._actor.rotation; - this._end = this._start + this._offset; - - const distance1 = Math.abs(this._end - this._start); - const distance2 = Util.TwoPI - distance1; - if (distance1 > distance2) { - this._shortDistance = distance2; - this._longDistance = distance1; - } else { - this._shortDistance = distance1; - this._longDistance = distance2; - } - - this._shortestPathIsPositive = (this._start - this._end + Util.TwoPI) % Util.TwoPI >= Math.PI; - - switch (this._rotationType) { - case RotationType.ShortestPath: - this._distance = this._shortDistance; - if (this._shortestPathIsPositive) { - this._direction = 1; - } else { - this._direction = -1; - } - break; - case RotationType.LongestPath: - this._distance = this._longDistance; - if (this._shortestPathIsPositive) { - this._direction = -1; - } else { - this._direction = 1; - } - break; - case RotationType.Clockwise: - this._direction = 1; - if (this._shortDistance >= 0) { - this._distance = this._shortDistance; - } else { - this._distance = this._longDistance; - } - break; - case RotationType.CounterClockwise: - this._direction = -1; - if (this._shortDistance <= 0) { - this._distance = this._shortDistance; - } else { - this._distance = this._longDistance; - } - break; - } - } - - this._actor.rx = this._direction * this._speed; - this._currentNonCannonAngle += (this._direction * this._speed) * (_delta / 1000); - - if (this.isComplete()) { - this._actor.rotation = this._end; - this._actor.rx = 0; - this._stopped = true; - } - } - - public isComplete(): boolean { - const distanceTravelled = Math.abs(this._currentNonCannonAngle - this._start); - return this._stopped || distanceTravelled >= Math.abs(this._distance); - } - - public stop(): void { - this._actor.rx = 0; - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -@obsolete({ message: 'ex.Action.ScaleTo will be removed in v0.25.0', alternateMethod: 'Set width and hight directly' }) -export class ScaleTo implements Action { - private _actor: Actor; - public x: number; - public y: number; - private _startX: number; - private _startY: number; - private _endX: number; - private _endY: number; - private _speedX: number; - private _speedY: number; - private _distanceX: number; - private _distanceY: number; - private _started = false; - private _stopped = false; - constructor(actor: Actor, scaleX: number, scaleY: number, speedX: number, speedY: number) { - this._actor = actor; - this._endX = scaleX; - this._endY = scaleY; - this._speedX = speedX; - this._speedY = speedY; - } - - public update(_delta: number): void { - if (!this._started) { - this._started = true; - this._startX = this._actor.scale.x; - this._startY = this._actor.scale.y; - this._distanceX = Math.abs(this._endX - this._startX); - this._distanceY = Math.abs(this._endY - this._startY); - } - - if (!(Math.abs(this._actor.scale.x - this._startX) >= this._distanceX)) { - const directionX = this._endY < this._startY ? -1 : 1; - this._actor.sx = this._speedX * directionX; - } else { - this._actor.sx = 0; - } - - if (!(Math.abs(this._actor.scale.y - this._startY) >= this._distanceY)) { - const directionY = this._endY < this._startY ? -1 : 1; - this._actor.sy = this._speedY * directionY; - } else { - this._actor.sy = 0; - } - - if (this.isComplete()) { - this._actor.scale = vec(this._endX, this._endY); - this._actor.sx = 0; - this._actor.sy = 0; - } - } - - public isComplete(): boolean { - return ( - this._stopped || - (Math.abs(this._actor.scale.y - this._startX) >= this._distanceX && Math.abs(this._actor.scale.y - this._startY) >= this._distanceY) - ); - } - - public stop(): void { - this._actor.sx = 0; - this._actor.sy = 0; - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -@obsolete({ message: 'ex.Action.ScaleBy will be removed in v0.25.0', alternateMethod: 'Set width and hight directly' }) -export class ScaleBy implements Action { - private _actor: Actor; - public x: number; - public y: number; - private _startScale: Vector; - private _endScale: Vector; - private _offset: Vector; - private _distanceX: number; - private _distanceY: number; - private _directionX: number; - private _directionY: number; - private _started = false; - private _stopped = false; - private _speedX: number; - private _speedY: number; - constructor(actor: Actor, scaleOffsetX: number, scaleOffsetY: number, speed: number) { - this._actor = actor; - this._offset = new Vector(scaleOffsetX, scaleOffsetY); - this._speedX = this._speedY = speed; - } - - public update(_delta: number): void { - if (!this._started) { - this._started = true; - this._startScale = this._actor.scale.clone(); - this._endScale = this._startScale.add(this._offset); - this._distanceX = Math.abs(this._endScale.x - this._startScale.x); - this._distanceY = Math.abs(this._endScale.y - this._startScale.y); - this._directionX = this._endScale.x < this._startScale.x ? -1 : 1; - this._directionY = this._endScale.y < this._startScale.y ? -1 : 1; - } - - this._actor.sx = this._speedX * this._directionX; - this._actor.sy = this._speedY * this._directionY; - - if (this.isComplete()) { - this._actor.scale = this._endScale; - this._actor.sx = 0; - this._actor.sy = 0; - } - } - - public isComplete(): boolean { - return ( - this._stopped || - (Math.abs(this._actor.scale.x - this._startScale.x) >= this._distanceX && - Math.abs(this._actor.scale.y - this._startScale.y) >= this._distanceY) - ); - } - - public stop(): void { - this._actor.sx = 0; - this._actor.sy = 0; - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -export class Delay implements Action { - public x: number; - public y: number; - private _actor: Actor; - private _elapsedTime: number = 0; - private _delay: number; - private _started: boolean = false; - private _stopped = false; - constructor(actor: Actor, delay: number) { - this._actor = actor; - this._delay = delay; - } - - public update(delta: number): void { - if (!this._started) { - this._started = true; - } - - this.x = this._actor.pos.x; - this.y = this._actor.pos.y; - - this._elapsedTime += delta; - } - - isComplete(): boolean { - return this._stopped || this._elapsedTime >= this._delay; - } - - public stop(): void { - this._stopped = true; - } - - reset(): void { - this._elapsedTime = 0; - this._started = false; - } -} - -export class Blink implements Action { - private _timeVisible: number = 0; - private _timeNotVisible: number = 0; - private _elapsedTime: number = 0; - private _totalTime: number = 0; - private _actor: Actor; - private _duration: number; - private _stopped: boolean = false; - private _started: boolean = false; - constructor(actor: Actor, timeVisible: number, timeNotVisible: number, numBlinks: number = 1) { - this._actor = actor; - this._timeVisible = timeVisible; - this._timeNotVisible = timeNotVisible; - this._duration = (timeVisible + timeNotVisible) * numBlinks; - } - - public update(delta: number): void { - if (!this._started) { - this._started = true; - } - - this._elapsedTime += delta; - this._totalTime += delta; - if (this._actor.visible && this._elapsedTime >= this._timeVisible) { - this._actor.visible = false; - this._elapsedTime = 0; - } - - if (!this._actor.visible && this._elapsedTime >= this._timeNotVisible) { - this._actor.visible = true; - this._elapsedTime = 0; - } - - if (this.isComplete()) { - this._actor.visible = true; - } - } - - public isComplete(): boolean { - return this._stopped || this._totalTime >= this._duration; - } - - public stop(): void { - this._actor.visible = true; - this._stopped = true; - } - - public reset() { - this._started = false; - this._elapsedTime = 0; - this._totalTime = 0; - } -} - -export class Fade implements Action { - public x: number; - public y: number; - - private _actor: Actor; - private _endOpacity: number; - private _speed: number; - private _multiplier: number = 1; - private _started = false; - private _stopped = false; - - constructor(actor: Actor, endOpacity: number, speed: number) { - this._actor = actor; - this._endOpacity = endOpacity; - this._speed = speed; - } - - public update(delta: number): void { - if (!this._started) { - this._started = true; - - // determine direction when we start - if (this._endOpacity < this._actor.opacity) { - this._multiplier = -1; - } else { - this._multiplier = 1; - } - } - - if (this._speed > 0) { - this._actor.opacity += (this._multiplier * (Math.abs(this._actor.opacity - this._endOpacity) * delta)) / this._speed; - } - - this._speed -= delta; - - if (this.isComplete()) { - this._actor.opacity = this._endOpacity; - } - - Logger.getInstance().debug('[Action fade] Actor opacity:', this._actor.opacity); - } - - public isComplete(): boolean { - return this._stopped || Math.abs(this._actor.opacity - this._endOpacity) < 0.05; - } - - public stop(): void { - this._stopped = true; - } - - public reset(): void { - this._started = false; - } -} - -export class Die implements Action { - public x: number; - public y: number; - - private _actor: Actor; - private _stopped = false; - - constructor(actor: Actor) { - this._actor = actor; - } - - public update(_delta: number): void { - this._actor.actionQueue.clearActions(); - this._actor.kill(); - this._stopped = true; - } - - public isComplete(): boolean { - return this._stopped; - } - - public stop(): void { - return; - } - - public reset(): void { - return; - } -} - -export class CallMethod implements Action { - public x: number; - public y: number; - private _method: () => any = null; - private _actor: Actor = null; - private _hasBeenCalled: boolean = false; - constructor(actor: Actor, method: () => any) { - this._actor = actor; - this._method = method; - } - - public update(_delta: number) { - this._method.call(this._actor); - this._hasBeenCalled = true; - } - public isComplete() { - return this._hasBeenCalled; - } - public reset() { - this._hasBeenCalled = false; - } - public stop() { - this._hasBeenCalled = true; - } -} - -export class Repeat implements Action { - public x: number; - public y: number; - private _actor: Actor; - private _actionQueue: ActionQueue; - private _repeat: number; - private _originalRepeat: number; - private _stopped: boolean = false; - constructor(actor: Actor, repeat: number, actions: Action[]) { - this._actor = actor; - this._actionQueue = new ActionQueue(actor); - this._repeat = repeat; - this._originalRepeat = repeat; - - const len = actions.length; - for (let i = 0; i < len; i++) { - actions[i].reset(); - this._actionQueue.add(actions[i]); - } - } - - public update(delta: number): void { - this.x = this._actor.pos.x; - this.y = this._actor.pos.y; - if (!this._actionQueue.hasNext()) { - this._actionQueue.reset(); - this._repeat--; - } - this._actionQueue.update(delta); - } - - public isComplete(): boolean { - return this._stopped || this._repeat <= 0; - } - - public stop(): void { - this._stopped = true; - } - - public reset(): void { - this._repeat = this._originalRepeat; - } -} - -export class RepeatForever implements Action { - public x: number; - public y: number; - private _actor: Actor; - private _actionQueue: ActionQueue; - private _stopped: boolean = false; - constructor(actor: Actor, actions: Action[]) { - this._actor = actor; - this._actionQueue = new ActionQueue(actor); - - const len = actions.length; - for (let i = 0; i < len; i++) { - actions[i].reset(); - this._actionQueue.add(actions[i]); - } - } - - public update(delta: number): void { - this.x = this._actor.pos.x; - this.y = this._actor.pos.y; - if (this._stopped) { - return; - } - - if (!this._actionQueue.hasNext()) { - this._actionQueue.reset(); - } - - this._actionQueue.update(delta); - } - - public isComplete(): boolean { - return this._stopped; - } - - public stop(): void { - this._stopped = true; - this._actionQueue.clearActions(); - } - - public reset(): void { - return; - } -} - -/** - * Action Queues - * - * Action queues are part of the [[ActionContext|Action API]] and - * store the list of actions to be executed for an [[Actor]]. - * - * Actors implement [[Actor.actions]] which can be manipulated by - * advanced users to adjust the actions currently being executed in the - * queue. - */ -export class ActionQueue { - private _actor: Actor; - private _actions: Action[] = []; - private _currentAction: Action; - private _completedActions: Action[] = []; - constructor(actor: Actor) { - this._actor = actor; - } - - public add(action: Action) { - this._actions.push(action); - } - - public remove(action: Action) { - const index = this._actions.indexOf(action); - this._actions.splice(index, 1); - } - - public clearActions(): void { - this._actions.length = 0; - this._completedActions.length = 0; - if (this._currentAction) { - this._currentAction.stop(); - } - } - - public getActions(): Action[] { - return this._actions.concat(this._completedActions); - } - - public hasNext(): boolean { - return this._actions.length > 0; - } - - public reset(): void { - this._actions = this.getActions(); - - const len = this._actions.length; - for (let i = 0; i < len; i++) { - this._actions[i].reset(); - } - this._completedActions = []; - } - - public update(delta: number) { - if (this._actions.length > 0) { - this._currentAction = this._actions[0]; - this._currentAction.update(delta); - - if (this._currentAction.isComplete(this._actor)) { - this._completedActions.push(this._actions.shift()); - } - } - } -} diff --git a/src/engine/Actions/Action/Blink.ts b/src/engine/Actions/Action/Blink.ts new file mode 100644 index 00000000..c424169f --- /dev/null +++ b/src/engine/Actions/Action/Blink.ts @@ -0,0 +1,56 @@ +import { Actor } from '../../Actor'; +import { Action } from '../Action'; + +export class Blink implements Action { + private _timeVisible: number = 0; + private _timeNotVisible: number = 0; + private _elapsedTime: number = 0; + private _totalTime: number = 0; + private _actor: Actor; + private _duration: number; + private _stopped: boolean = false; + private _started: boolean = false; + constructor(actor: Actor, timeVisible: number, timeNotVisible: number, numBlinks: number = 1) { + this._actor = actor; + this._timeVisible = timeVisible; + this._timeNotVisible = timeNotVisible; + this._duration = (timeVisible + timeNotVisible) * numBlinks; + } + + public update(delta: number): void { + if (!this._started) { + this._started = true; + } + + this._elapsedTime += delta; + this._totalTime += delta; + if (this._actor.visible && this._elapsedTime >= this._timeVisible) { + this._actor.visible = false; + this._elapsedTime = 0; + } + + if (!this._actor.visible && this._elapsedTime >= this._timeNotVisible) { + this._actor.visible = true; + this._elapsedTime = 0; + } + + if (this.isComplete()) { + this._actor.visible = true; + } + } + + public isComplete(): boolean { + return this._stopped || this._totalTime >= this._duration; + } + + public stop(): void { + this._actor.visible = true; + this._stopped = true; + } + + public reset() { + this._started = false; + this._elapsedTime = 0; + this._totalTime = 0; + } +} diff --git a/src/engine/Actions/Action/CallMethod.ts b/src/engine/Actions/Action/CallMethod.ts new file mode 100644 index 00000000..6b99eb00 --- /dev/null +++ b/src/engine/Actions/Action/CallMethod.ts @@ -0,0 +1,23 @@ +import { Action } from '../Action'; + +export class CallMethod implements Action { + private _method: () => any = null; + private _hasBeenCalled: boolean = false; + constructor(method: () => any) { + this._method = method; + } + + public update(_delta: number) { + this._method(); + this._hasBeenCalled = true; + } + public isComplete() { + return this._hasBeenCalled; + } + public reset() { + this._hasBeenCalled = false; + } + public stop() { + this._hasBeenCalled = true; + } +} diff --git a/src/engine/Actions/Action/Delay.ts b/src/engine/Actions/Action/Delay.ts new file mode 100644 index 00000000..29da8199 --- /dev/null +++ b/src/engine/Actions/Action/Delay.ts @@ -0,0 +1,40 @@ +import { Actor } from '../../Actor'; +import { Action } from '../Action'; + +export class Delay implements Action { + public x: number; + public y: number; + private _actor: Actor; + private _elapsedTime: number = 0; + private _delay: number; + private _started: boolean = false; + private _stopped = false; + constructor(actor: Actor, delay: number) { + this._actor = actor; + this._delay = delay; + } + + public update(delta: number): void { + if (!this._started) { + this._started = true; + } + + this.x = this._actor.pos.x; + this.y = this._actor.pos.y; + + this._elapsedTime += delta; + } + + isComplete(): boolean { + return this._stopped || this._elapsedTime >= this._delay; + } + + public stop(): void { + this._stopped = true; + } + + reset(): void { + this._elapsedTime = 0; + this._started = false; + } +} diff --git a/src/engine/Actions/Action/Die.ts b/src/engine/Actions/Action/Die.ts new file mode 100644 index 00000000..1a977be3 --- /dev/null +++ b/src/engine/Actions/Action/Die.ts @@ -0,0 +1,32 @@ +import { Actor } from '../../Actor'; +import { Action } from '../Action'; + +export class Die implements Action { + public x: number; + public y: number; + + private _actor: Actor; + private _stopped = false; + + constructor(actor: Actor) { + this._actor = actor; + } + + public update(_delta: number): void { + this._actor.actions.clearActions(); + this._actor.kill(); + this._stopped = true; + } + + public isComplete(): boolean { + return this._stopped; + } + + public stop(): void { + return; + } + + public reset(): void { + return; + } +} diff --git a/src/engine/Actions/Action/EaseTo.ts b/src/engine/Actions/Action/EaseTo.ts new file mode 100644 index 00000000..f914367c --- /dev/null +++ b/src/engine/Actions/Action/EaseTo.ts @@ -0,0 +1,73 @@ +import { Actor } from '../../Actor'; +import { vec, Vector } from '../../Algebra'; +import { Action } from '../Action'; + +export class EaseTo implements Action { + private _currentLerpTime: number = 0; + private _lerpDuration: number = 1 * 1000; // 1 second + private _lerpStart: Vector = new Vector(0, 0); + private _lerpEnd: Vector = new Vector(0, 0); + private _initialized: boolean = false; + private _stopped: boolean = false; + private _distance: number = 0; + constructor( + public actor: Actor, + x: number, + y: number, + duration: number, + public easingFcn: (currentTime: number, startValue: number, endValue: number, duration: number) => number + ) { + this._lerpDuration = duration; + this._lerpEnd = new Vector(x, y); + } + private _initialize() { + this._lerpStart = new Vector(this.actor.pos.x, this.actor.pos.y); + this._currentLerpTime = 0; + this._distance = this._lerpStart.distance(this._lerpEnd); + } + + public update(delta: number): void { + if (!this._initialized) { + this._initialize(); + this._initialized = true; + } + + // Need to update lerp time first, otherwise the first update will always be zero + this._currentLerpTime += delta; + let newX = this.actor.pos.x; + let newY = this.actor.pos.y; + if (this._currentLerpTime < this._lerpDuration) { + if (this._lerpEnd.x < this._lerpStart.x) { + newX = + this._lerpStart.x - + (this.easingFcn(this._currentLerpTime, this._lerpEnd.x, this._lerpStart.x, this._lerpDuration) - this._lerpEnd.x); + } else { + newX = this.easingFcn(this._currentLerpTime, this._lerpStart.x, this._lerpEnd.x, this._lerpDuration); + } + + if (this._lerpEnd.y < this._lerpStart.y) { + newY = + this._lerpStart.y - + (this.easingFcn(this._currentLerpTime, this._lerpEnd.y, this._lerpStart.y, this._lerpDuration) - this._lerpEnd.y); + } else { + newY = this.easingFcn(this._currentLerpTime, this._lerpStart.y, this._lerpEnd.y, this._lerpDuration); + } + // Given the lerp position figure out the velocity in pixels per second + this.actor.vel = vec((newX - this.actor.pos.x) / (delta / 1000), (newY - this.actor.pos.y) / (delta / 1000)); + } else { + this.actor.pos = vec(this._lerpEnd.x, this._lerpEnd.y); + this.actor.vel = Vector.Zero; + } + } + public isComplete(actor: Actor): boolean { + return this._stopped || new Vector(actor.pos.x, actor.pos.y).distance(this._lerpStart) >= this._distance; + } + + public reset(): void { + this._initialized = false; + } + public stop(): void { + this.actor.vel = vec(0, 0); + this._stopped = true; + } +} diff --git a/src/engine/Actions/Action/Fade.ts b/src/engine/Actions/Action/Fade.ts new file mode 100644 index 00000000..7b3855e7 --- /dev/null +++ b/src/engine/Actions/Action/Fade.ts @@ -0,0 +1,58 @@ +import { Actor } from '../../Actor'; +import { Logger } from '../../Util/Log'; +import { Action } from '../Action'; + +export class Fade implements Action { + public x: number; + public y: number; + + private _actor: Actor; + private _endOpacity: number; + private _speed: number; + private _multiplier: number = 1; + private _started = false; + private _stopped = false; + + constructor(actor: Actor, endOpacity: number, speed: number) { + this._actor = actor; + this._endOpacity = endOpacity; + this._speed = speed; + } + + public update(delta: number): void { + if (!this._started) { + this._started = true; + + // determine direction when we start + if (this._endOpacity < this._actor.opacity) { + this._multiplier = -1; + } else { + this._multiplier = 1; + } + } + + if (this._speed > 0) { + this._actor.opacity += (this._multiplier * (Math.abs(this._actor.opacity - this._endOpacity) * delta)) / this._speed; + } + + this._speed -= delta; + + if (this.isComplete()) { + this._actor.opacity = this._endOpacity; + } + + Logger.getInstance().debug('[Action fade] Actor opacity:', this._actor.opacity); + } + + public isComplete(): boolean { + return this._stopped || Math.abs(this._actor.opacity - this._endOpacity) < 0.05; + } + + public stop(): void { + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/Follow.ts b/src/engine/Actions/Action/Follow.ts new file mode 100644 index 00000000..80673727 --- /dev/null +++ b/src/engine/Actions/Action/Follow.ts @@ -0,0 +1,71 @@ +import { Actor } from '../../Actor'; +import { vec, Vector } from '../../Algebra'; +import { Action } from '../Action'; + +export class Follow implements Action { + private _actor: Actor; + private _actorToFollow: Actor; + public x: number; + public y: number; + private _current: Vector; + private _end: Vector; + private _dir: Vector; + private _speed: number; + private _maximumDistance: number; + private _distanceBetween: number; + private _started = false; + private _stopped = false; + + constructor(actor: Actor, actorToFollow: Actor, followDistance?: number) { + this._actor = actor; + this._actorToFollow = actorToFollow; + this._current = new Vector(this._actor.pos.x, this._actor.pos.y); + this._end = new Vector(actorToFollow.pos.x, actorToFollow.pos.y); + this._maximumDistance = followDistance !== undefined ? followDistance : this._current.distance(this._end); + this._speed = 0; + } + + public update(_delta: number): void { + if (!this._started) { + this._started = true; + this._distanceBetween = this._current.distance(this._end); + this._dir = this._end.sub(this._current).normalize(); + } + + const actorToFollowSpeed = Math.sqrt(Math.pow(this._actorToFollow.vel.x, 2) + Math.pow(this._actorToFollow.vel.y, 2)); + if (actorToFollowSpeed !== 0) { + this._speed = actorToFollowSpeed; + } + this._current = vec(this._actor.pos.x, this._actor.pos.y); + + this._end = vec(this._actorToFollow.pos.x, this._actorToFollow.pos.y); + this._distanceBetween = this._current.distance(this._end); + this._dir = this._end.sub(this._current).normalize(); + + if (this._distanceBetween >= this._maximumDistance) { + const m = this._dir.scale(this._speed); + this._actor.vel = vec(m.x, m.y); + } else { + this._actor.vel = vec(0, 0); + } + + if (this.isComplete()) { + this._actor.pos = vec(this._end.x, this._end.y); + this._actor.vel = vec(0, 0); + } + } + + public stop(): void { + this._actor.vel = vec(0, 0); + this._stopped = true; + } + + public isComplete(): boolean { + // the actor following should never stop unless specified to do so + return this._stopped; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/Meet.ts b/src/engine/Actions/Action/Meet.ts new file mode 100644 index 00000000..6e970ecf --- /dev/null +++ b/src/engine/Actions/Action/Meet.ts @@ -0,0 +1,69 @@ +import { Actor } from '../../Actor'; +import { vec, Vector } from '../../Algebra'; +import { Action } from '../Action'; + +export class Meet implements Action { + private _actor: Actor; + private _actorToMeet: Actor; + public x: number; + public y: number; + private _current: Vector; + private _end: Vector; + private _dir: Vector; + private _speed: number; + private _distanceBetween: number; + private _started = false; + private _stopped = false; + private _speedWasSpecified = false; + + constructor(actor: Actor, actorToMeet: Actor, speed?: number) { + this._actor = actor; + this._actorToMeet = actorToMeet; + this._current = new Vector(this._actor.pos.x, this._actor.pos.y); + this._end = new Vector(actorToMeet.pos.x, actorToMeet.pos.y); + this._speed = speed || 0; + + if (speed !== undefined) { + this._speedWasSpecified = true; + } + } + + public update(_delta: number): void { + if (!this._started) { + this._started = true; + this._distanceBetween = this._current.distance(this._end); + this._dir = this._end.sub(this._current).normalize(); + } + + const actorToMeetSpeed = Math.sqrt(Math.pow(this._actorToMeet.vel.x, 2) + Math.pow(this._actorToMeet.vel.y, 2)); + if (actorToMeetSpeed !== 0 && !this._speedWasSpecified) { + this._speed = actorToMeetSpeed; + } + this._current = vec(this._actor.pos.x, this._actor.pos.y); + + this._end = vec(this._actorToMeet.pos.x, this._actorToMeet.pos.y); + this._distanceBetween = this._current.distance(this._end); + this._dir = this._end.sub(this._current).normalize(); + + const m = this._dir.scale(this._speed); + this._actor.vel = vec(m.x, m.y); + + if (this.isComplete()) { + this._actor.pos = vec(this._end.x, this._end.y); + this._actor.vel = vec(0, 0); + } + } + + public isComplete(): boolean { + return this._stopped || this._distanceBetween <= 1; + } + + public stop(): void { + this._actor.vel = vec(0, 0); + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/MoveBy.ts b/src/engine/Actions/Action/MoveBy.ts new file mode 100644 index 00000000..77ecc3b0 --- /dev/null +++ b/src/engine/Actions/Action/MoveBy.ts @@ -0,0 +1,59 @@ +import { Actor } from '../../Actor'; +import { vec, Vector } from '../../Algebra'; +import { Logger } from '../../Util/Log'; +import { Action } from '../Action'; + +export class MoveBy implements Action { + private _actor: Actor; + public x: number; + public y: number; + private _distance: number; + private _speed: number; + + private _start: Vector; + private _offset: Vector; + private _end: Vector; + private _dir: Vector; + private _started = false; + private _stopped = false; + + constructor(actor: Actor, offsetX: number, offsetY: number, speed: number) { + this._actor = actor; + this._speed = speed; + this._offset = new Vector(offsetX, offsetY); + if (speed <= 0) { + Logger.getInstance().error('Attempted to moveBy with speed less than or equal to zero : ' + speed); + throw new Error('Speed must be greater than 0 pixels per second'); + } + } + + public update(_delta: number) { + if (!this._started) { + this._started = true; + this._start = new Vector(this._actor.pos.x, this._actor.pos.y); + this._end = this._start.add(this._offset); + this._distance = this._offset.size; + this._dir = this._end.sub(this._start).normalize(); + } + + if (this.isComplete(this._actor)) { + this._actor.pos = vec(this._end.x, this._end.y); + this._actor.vel = vec(0, 0); + } else { + this._actor.vel = this._dir.scale(this._speed); + } + } + + public isComplete(actor: Actor): boolean { + return this._stopped || actor.pos.distance(this._start) >= this._distance; + } + + public stop(): void { + this._actor.vel = vec(0, 0); + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/MoveTo.ts b/src/engine/Actions/Action/MoveTo.ts new file mode 100644 index 00000000..418af064 --- /dev/null +++ b/src/engine/Actions/Action/MoveTo.ts @@ -0,0 +1,50 @@ +import { Actor } from '../../Actor'; +import { vec, Vector } from '../../Algebra'; +import { Action } from '../Action'; + +export class MoveTo implements Action { + private _actor: Actor; + public x: number; + public y: number; + private _start: Vector; + private _end: Vector; + private _dir: Vector; + private _speed: number; + private _distance: number; + private _started = false; + private _stopped = false; + constructor(actor: Actor, destx: number, desty: number, speed: number) { + this._actor = actor; + this._end = new Vector(destx, desty); + this._speed = speed; + } + + public update(_delta: number): void { + if (!this._started) { + this._started = true; + this._start = new Vector(this._actor.pos.x, this._actor.pos.y); + this._distance = this._start.distance(this._end); + this._dir = this._end.sub(this._start).normalize(); + } + const m = this._dir.scale(this._speed); + this._actor.vel = vec(m.x, m.y); + + if (this.isComplete(this._actor)) { + this._actor.pos = vec(this._end.x, this._end.y); + this._actor.vel = vec(0, 0); + } + } + + public isComplete(actor: Actor): boolean { + return this._stopped || new Vector(actor.pos.x, actor.pos.y).distance(this._start) >= this._distance; + } + + public stop(): void { + this._actor.vel = vec(0, 0); + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/Repeat.ts b/src/engine/Actions/Action/Repeat.ts new file mode 100644 index 00000000..a3e827c1 --- /dev/null +++ b/src/engine/Actions/Action/Repeat.ts @@ -0,0 +1,45 @@ +import { Actor } from '../../Actor'; +import { Action } from '../Action'; +import { ActionContext } from '../ActionContext'; +import { ActionQueue } from '../ActionQueue'; + +export class Repeat implements Action { + private _actionQueue: ActionQueue; + private _repeat: number; + private _originalRepeat: number; + private _stopped: boolean = false; + private _repeatContext: ActionContext; + private _repeatBuilder: (repeatContext: ActionContext) => any; + constructor(actor: Actor, repeatBuilder: (repeatContext: ActionContext) => any, repeat: number) { + this._repeatBuilder = repeatBuilder; + this._repeatContext = new ActionContext(actor); + this._actionQueue = this._repeatContext.getQueue(); + + this._repeat = repeat; + this._originalRepeat = repeat; + + this._repeatBuilder(this._repeatContext); + this._repeat--; // current execution is the first repeat + } + + public update(delta: number): void { + if (this._actionQueue.isComplete()) { + this._actionQueue.clearActions(); + this._repeatBuilder(this._repeatContext); + this._repeat--; + } + this._actionQueue.update(delta); + } + + public isComplete(): boolean { + return this._stopped || (this._repeat <= 0 && this._actionQueue.isComplete()); + } + + public stop(): void { + this._stopped = true; + } + + public reset(): void { + this._repeat = this._originalRepeat; + } +} diff --git a/src/engine/Actions/Action/RepeatForever.ts b/src/engine/Actions/Action/RepeatForever.ts new file mode 100644 index 00000000..99058fd5 --- /dev/null +++ b/src/engine/Actions/Action/RepeatForever.ts @@ -0,0 +1,50 @@ +import { Actor } from '../../Actor'; +import { Action } from '../Action'; +import { ActionContext } from '../ActionContext'; +import { ActionQueue } from '../ActionQueue'; + +/** + * RepeatForever Action implementation, it is recommended you use the fluent action + * context API. + * + * + */ +export class RepeatForever implements Action { + private _actionQueue: ActionQueue; + private _stopped: boolean = false; + private _repeatContext: ActionContext; + private _repeatBuilder: (repeatContext: ActionContext) => any; + constructor(actor: Actor, repeatBuilder: (repeatContext: ActionContext) => any) { + this._repeatBuilder = repeatBuilder; + this._repeatContext = new ActionContext(actor); + this._actionQueue = this._repeatContext.getQueue(); + + this._repeatBuilder(this._repeatContext); + } + + public update(delta: number): void { + if (this._stopped) { + return; + } + + if (this._actionQueue.isComplete()) { + this._actionQueue.clearActions(); + this._repeatBuilder(this._repeatContext); + } + + this._actionQueue.update(delta); + } + + public isComplete(): boolean { + return this._stopped; + } + + public stop(): void { + this._stopped = true; + this._actionQueue.clearActions(); + } + + public reset(): void { + return; + } +} diff --git a/src/engine/Actions/Action/RotateBy.ts b/src/engine/Actions/Action/RotateBy.ts new file mode 100644 index 00000000..1ea9bb69 --- /dev/null +++ b/src/engine/Actions/Action/RotateBy.ts @@ -0,0 +1,109 @@ +import { Actor } from '../../Actor'; +import { Action } from '../Action'; +import { RotationType } from '../RotationType'; +import * as Util from '../../Util/Util'; + +export class RotateBy implements Action { + private _actor: Actor; + public x: number; + public y: number; + private _start: number; + private _end: number; + private _speed: number; + private _offset: number; + + private _rotationType: RotationType; + private _direction: number; + private _distance: number; + private _shortDistance: number; + private _longDistance: number; + private _shortestPathIsPositive: boolean; + private _currentNonCannonAngle: number; + private _started = false; + private _stopped = false; + constructor(actor: Actor, angleRadiansOffset: number, speed: number, rotationType?: RotationType) { + this._actor = actor; + this._speed = speed; + this._offset = angleRadiansOffset; + this._rotationType = rotationType || RotationType.ShortestPath; + } + + public update(_delta: number): void { + if (!this._started) { + this._started = true; + this._start = this._actor.rotation; + this._currentNonCannonAngle = this._actor.rotation; + this._end = this._start + this._offset; + + const distance1 = Math.abs(this._end - this._start); + const distance2 = Util.TwoPI - distance1; + if (distance1 > distance2) { + this._shortDistance = distance2; + this._longDistance = distance1; + } else { + this._shortDistance = distance1; + this._longDistance = distance2; + } + + this._shortestPathIsPositive = (this._start - this._end + Util.TwoPI) % Util.TwoPI >= Math.PI; + + switch (this._rotationType) { + case RotationType.ShortestPath: + this._distance = this._shortDistance; + if (this._shortestPathIsPositive) { + this._direction = 1; + } else { + this._direction = -1; + } + break; + case RotationType.LongestPath: + this._distance = this._longDistance; + if (this._shortestPathIsPositive) { + this._direction = -1; + } else { + this._direction = 1; + } + break; + case RotationType.Clockwise: + this._direction = 1; + if (this._shortDistance >= 0) { + this._distance = this._shortDistance; + } else { + this._distance = this._longDistance; + } + break; + case RotationType.CounterClockwise: + this._direction = -1; + if (this._shortDistance <= 0) { + this._distance = this._shortDistance; + } else { + this._distance = this._longDistance; + } + break; + } + } + + this._actor.rx = this._direction * this._speed; + this._currentNonCannonAngle += this._direction * this._speed * (_delta / 1000); + + if (this.isComplete()) { + this._actor.rotation = this._end; + this._actor.rx = 0; + this._stopped = true; + } + } + + public isComplete(): boolean { + const distanceTravelled = Math.abs(this._currentNonCannonAngle - this._start); + return this._stopped || distanceTravelled >= Math.abs(this._distance); + } + + public stop(): void { + this._actor.rx = 0; + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/RotateTo.ts b/src/engine/Actions/Action/RotateTo.ts new file mode 100644 index 00000000..951479bb --- /dev/null +++ b/src/engine/Actions/Action/RotateTo.ts @@ -0,0 +1,105 @@ +import { Actor } from '../../Actor'; +import { Action } from '../Action'; +import { RotationType } from '../RotationType'; +import * as Util from '../../Util/Util'; + +export class RotateTo implements Action { + private _actor: Actor; + public x: number; + public y: number; + private _start: number; + private _end: number; + private _speed: number; + private _rotationType: RotationType; + private _direction: number; + private _distance: number; + private _shortDistance: number; + private _longDistance: number; + private _shortestPathIsPositive: boolean; + private _currentNonCannonAngle: number; + private _started = false; + private _stopped = false; + constructor(actor: Actor, angleRadians: number, speed: number, rotationType?: RotationType) { + this._actor = actor; + this._end = angleRadians; + this._speed = speed; + this._rotationType = rotationType || RotationType.ShortestPath; + } + + public update(_delta: number): void { + if (!this._started) { + this._started = true; + this._start = this._actor.rotation; + this._currentNonCannonAngle = this._actor.rotation; + const distance1 = Math.abs(this._end - this._start); + const distance2 = Util.TwoPI - distance1; + if (distance1 > distance2) { + this._shortDistance = distance2; + this._longDistance = distance1; + } else { + this._shortDistance = distance1; + this._longDistance = distance2; + } + + this._shortestPathIsPositive = (this._start - this._end + Util.TwoPI) % Util.TwoPI >= Math.PI; + + switch (this._rotationType) { + case RotationType.ShortestPath: + this._distance = this._shortDistance; + if (this._shortestPathIsPositive) { + this._direction = 1; + } else { + this._direction = -1; + } + break; + case RotationType.LongestPath: + this._distance = this._longDistance; + if (this._shortestPathIsPositive) { + this._direction = -1; + } else { + this._direction = 1; + } + break; + case RotationType.Clockwise: + this._direction = 1; + if (this._shortestPathIsPositive) { + this._distance = this._shortDistance; + } else { + this._distance = this._longDistance; + } + break; + case RotationType.CounterClockwise: + this._direction = -1; + if (!this._shortestPathIsPositive) { + this._distance = this._shortDistance; + } else { + this._distance = this._longDistance; + } + break; + } + } + + this._actor.rx = this._direction * this._speed; + this._currentNonCannonAngle += this._direction * this._speed * (_delta / 1000); + + if (this.isComplete()) { + this._actor.rotation = this._end; + this._actor.rx = 0; + this._stopped = true; + } + } + + public isComplete(): boolean { + const distanceTravelled = Math.abs(this._currentNonCannonAngle - this._start); + return this._stopped || distanceTravelled >= Math.abs(this._distance); + } + + public stop(): void { + this._actor.rx = 0; + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/ScaleBy.ts b/src/engine/Actions/Action/ScaleBy.ts new file mode 100644 index 00000000..4db305a6 --- /dev/null +++ b/src/engine/Actions/Action/ScaleBy.ts @@ -0,0 +1,64 @@ +import { Actor } from '../../Actor'; +import { Vector } from '../../Algebra'; +import { Action } from '../Action'; + +export class ScaleBy implements Action { + private _actor: Actor; + public x: number; + public y: number; + private _startScale: Vector; + private _endScale: Vector; + private _offset: Vector; + private _distanceX: number; + private _distanceY: number; + private _directionX: number; + private _directionY: number; + private _started = false; + private _stopped = false; + private _speedX: number; + private _speedY: number; + constructor(actor: Actor, scaleOffsetX: number, scaleOffsetY: number, speed: number) { + this._actor = actor; + this._offset = new Vector(scaleOffsetX, scaleOffsetY); + this._speedX = this._speedY = speed; + } + + public update(_delta: number): void { + if (!this._started) { + this._started = true; + this._startScale = this._actor.scale.clone(); + this._endScale = this._startScale.add(this._offset); + this._distanceX = Math.abs(this._endScale.x - this._startScale.x); + this._distanceY = Math.abs(this._endScale.y - this._startScale.y); + this._directionX = this._endScale.x < this._startScale.x ? -1 : 1; + this._directionY = this._endScale.y < this._startScale.y ? -1 : 1; + } + + this._actor.sx = this._speedX * this._directionX; + this._actor.sy = this._speedY * this._directionY; + + if (this.isComplete()) { + this._actor.scale = this._endScale; + this._actor.sx = 0; + this._actor.sy = 0; + } + } + + public isComplete(): boolean { + return ( + this._stopped || + (Math.abs(this._actor.scale.x - this._startScale.x) >= this._distanceX && + Math.abs(this._actor.scale.y - this._startScale.y) >= this._distanceY) + ); + } + + public stop(): void { + this._actor.sx = 0; + this._actor.sy = 0; + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/Action/ScaleTo.ts b/src/engine/Actions/Action/ScaleTo.ts new file mode 100644 index 00000000..3e625572 --- /dev/null +++ b/src/engine/Actions/Action/ScaleTo.ts @@ -0,0 +1,73 @@ +import { Actor } from '../../Actor'; +import { vec } from '../../Algebra'; +import { Action } from '../Action'; + +export class ScaleTo implements Action { + private _actor: Actor; + public x: number; + public y: number; + private _startX: number; + private _startY: number; + private _endX: number; + private _endY: number; + private _speedX: number; + private _speedY: number; + private _distanceX: number; + private _distanceY: number; + private _started = false; + private _stopped = false; + constructor(actor: Actor, scaleX: number, scaleY: number, speedX: number, speedY: number) { + this._actor = actor; + this._endX = scaleX; + this._endY = scaleY; + this._speedX = speedX; + this._speedY = speedY; + } + + public update(_delta: number): void { + if (!this._started) { + this._started = true; + this._startX = this._actor.scale.x; + this._startY = this._actor.scale.y; + this._distanceX = Math.abs(this._endX - this._startX); + this._distanceY = Math.abs(this._endY - this._startY); + } + + if (!(Math.abs(this._actor.scale.x - this._startX) >= this._distanceX)) { + const directionX = this._endY < this._startY ? -1 : 1; + this._actor.sx = this._speedX * directionX; + } else { + this._actor.sx = 0; + } + + if (!(Math.abs(this._actor.scale.y - this._startY) >= this._distanceY)) { + const directionY = this._endY < this._startY ? -1 : 1; + this._actor.sy = this._speedY * directionY; + } else { + this._actor.sy = 0; + } + + if (this.isComplete()) { + this._actor.scale = vec(this._endX, this._endY); + this._actor.sx = 0; + this._actor.sy = 0; + } + } + + public isComplete(): boolean { + return ( + this._stopped || + (Math.abs(this._actor.scale.y - this._startX) >= this._distanceX && Math.abs(this._actor.scale.y - this._startY) >= this._distanceY) + ); + } + + public stop(): void { + this._actor.sx = 0; + this._actor.sy = 0; + this._stopped = true; + } + + public reset(): void { + this._started = false; + } +} diff --git a/src/engine/Actions/ActionContext.ts b/src/engine/Actions/ActionContext.ts index f551d3a1..66d8d8c9 100644 --- a/src/engine/Actions/ActionContext.ts +++ b/src/engine/Actions/ActionContext.ts @@ -1,8 +1,24 @@ -import * as Actions from './Action'; import { RotationType } from './RotationType'; import { Actor } from '../Actor'; import { EasingFunction, EasingFunctions } from '../Util/EasingFunctions'; +import { ActionQueue } from './ActionQueue'; +import { Repeat } from './Action/Repeat'; +import { RepeatForever } from './Action/RepeatForever'; +import { MoveBy } from './Action/MoveBy'; +import { MoveTo } from './Action/MoveTo'; +import { RotateTo } from './Action/RotateTo'; +import { RotateBy } from './Action/RotateBy'; +import { ScaleTo } from './Action/ScaleTo'; +import { ScaleBy } from './Action/ScaleBy'; +import { CallMethod } from './Action/CallMethod'; +import { EaseTo } from './Action/EaseTo'; +import { Blink } from './Action/Blink'; +import { Fade } from './Action/Fade'; +import { Delay } from './Action/Delay'; +import { Die } from './Action/Die'; +import { Follow } from './Action/Follow'; +import { Meet } from './Action/Meet'; /** * The fluent Action API allows you to perform "actions" on @@ -11,43 +27,27 @@ import { EasingFunction, EasingFunctions } from '../Util/EasingFunctions'; * the [[Action]] interface. */ export class ActionContext { - private _actors: Actor[] = []; - private _queues: Actions.ActionQueue[] = []; + private _actor: Actor; + private _queue: ActionQueue; - constructor(); - constructor(actor: Actor); - constructor(actors: Actor[]); - constructor() { - if (arguments !== null) { - this._actors = Array.prototype.slice.call(arguments, 0); - this._queues = this._actors.map((a) => { - return a.actionQueue; - }); - } + constructor(actor: Actor) { + this._actor = actor; + this._queue = new ActionQueue(actor); } - /** - * Clears all queued actions from the Actor - */ - public clearActions(): void { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].clearActions(); - } + public getQueue(): ActionQueue { + return this._queue; } - public addActorToContext(actor: Actor) { - this._actors.push(actor); - // if we run into problems replace the line below with: - this._queues.push(actor.actionQueue); + public update(elapsedMs: number) { + this._queue.update(elapsedMs); } - public removeActorFromContext(actor: Actor) { - const index = this._actors.indexOf(actor); - if (index > -1) { - this._actors.splice(index, 1); - this._queues.splice(index, 1); - } + /** + * Clears all queued actions from the Actor + */ + public clearActions(): void { + this._queue.clearActions(); } /** @@ -60,12 +60,7 @@ export class ActionContext { * @param easingFcn Use [[EasingFunctions]] or a custom function to use to calculate position */ public easeTo(x: number, y: number, duration: number, easingFcn: EasingFunction = EasingFunctions.Linear) { - const len = this._queues.length; - - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.EaseTo(this._actors[i], x, y, duration, easingFcn)); - } - + this._queue.add(new EaseTo(this._actor, x, y, duration, easingFcn)); return this; } @@ -78,11 +73,7 @@ export class ActionContext { * @param speed The speed in pixels per second to move */ public moveTo(x: number, y: number, speed: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.MoveTo(this._actors[i], x, y, speed)); - } - + this._queue.add(new MoveTo(this._actor, x, y, speed)); return this; } @@ -94,10 +85,7 @@ export class ActionContext { * @param speed The speed in pixels per second the actor should move */ public moveBy(xOffset: number, yOffset: number, speed: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.MoveBy(this._actors[i], xOffset, yOffset, speed)); - } + this._queue.add(new MoveBy(this._actor, xOffset, yOffset, speed)); return this; } @@ -110,10 +98,7 @@ export class ActionContext { * @param rotationType The [[RotationType]] to use for this rotation */ public rotateTo(angleRadians: number, speed: number, rotationType?: RotationType): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.RotateTo(this._actors[i], angleRadians, speed, rotationType)); - } + this._queue.add(new RotateTo(this._actor, angleRadians, speed, rotationType)); return this; } @@ -126,10 +111,7 @@ export class ActionContext { * @param rotationType The [[RotationType]] to use for this rotation, default is shortest path */ public rotateBy(angleRadiansOffset: number, speed: number, rotationType?: RotationType): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.RotateBy(this._actors[i], angleRadiansOffset, speed, rotationType)); - } + this._queue.add(new RotateBy(this._actor, angleRadiansOffset, speed, rotationType)); return this; } @@ -144,10 +126,7 @@ export class ActionContext { * @param speedY The speed of scaling specified in magnitude increase per second on Y axis */ public scaleTo(sizeX: number, sizeY: number, speedX: number, speedY: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.ScaleTo(this._actors[i], sizeX, sizeY, speedX, speedY)); - } + this._queue.add(new ScaleTo(this._actor, sizeX, sizeY, speedX, speedY)); return this; } @@ -160,10 +139,7 @@ export class ActionContext { * @param speed The speed to scale at in scale units/sec */ public scaleBy(sizeOffsetX: number, sizeOffsetY: number, speed: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.ScaleBy(this._actors[i], sizeOffsetX, sizeOffsetY, speed)); - } + this._queue.add(new ScaleBy(this._actor, sizeOffsetX, sizeOffsetY, speed)); return this; } @@ -177,10 +153,7 @@ export class ActionContext { * @param numBlinks The number of times to blink */ public blink(timeVisible: number, timeNotVisible: number, numBlinks: number = 1): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.Blink(this._actors[i], timeVisible, timeNotVisible, numBlinks)); - } + this._queue.add(new Blink(this._actor, timeVisible, timeNotVisible, numBlinks)); return this; } @@ -192,10 +165,7 @@ export class ActionContext { * @param time The time it should take to fade the actor (in milliseconds) */ public fade(opacity: number, time: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.Fade(this._actors[i], opacity, time)); - } + this._queue.add(new Fade(this._actor, opacity, time)); return this; } @@ -206,23 +176,17 @@ export class ActionContext { * @param time The amount of time to delay the next action in the queue from executing in milliseconds */ public delay(time: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.Delay(this._actors[i], time)); - } + this._queue.add(new Delay(this._actor, time)); return this; } /** * This method will add an action to the queue that will remove the actor from the - * scene once it has completed its previous actions. Any actions on the + * scene once it has completed its previous Any actions on the * action queue after this action will not be executed. */ public die(): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.Die(this._actors[i])); - } + this._queue.add(new Die(this._actor)); return this; } @@ -232,44 +196,56 @@ export class ActionContext { * action, i.e An actor arrives at a destination after traversing a path */ public callMethod(method: () => any): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.CallMethod(this._actors[i], method)); - } + this._queue.add(new CallMethod(method)); return this; } /** - * This method will cause the actor to repeat all of the previously - * called actions a certain number of times. If the number of repeats + * This method will cause the actor to repeat all of the actions built in + * the `repeatBuilder` callback. If the number of repeats * is not specified it will repeat forever. This method is part of * the actor 'Action' fluent API allowing action chaining + * + * ```typescript + * // Move up in a zig-zag by repeated moveBy's + * actor.actions.repeat(repeatCtx => { + * repeatCtx.moveBy(10, 0, 10); + * repeatCtx.moveBy(0, 10, 10); + * }, 5); + * ``` + * + * @param repeatBuilder The builder to specify the repeatable list of actions * @param times The number of times to repeat all the previous actions in the action queue. If nothing is specified the actions * will repeat forever */ - public repeat(times?: number): ActionContext { + public repeat(repeatBuilder: (repeatContext: ActionContext) => any, times?: number): ActionContext { if (!times) { - this.repeatForever(); + this.repeatForever(repeatBuilder); return this; } - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.Repeat(this._actors[i], times, this._actors[i].actionQueue.getActions())); - } + this._queue.add(new Repeat(this._actor, repeatBuilder, times)); return this; } /** - * This method will cause the actor to repeat all of the previously - * called actions forever. This method is part of the actor 'Action' - * fluent API allowing action chaining. + * This method will cause the actor to repeat all of the actions built in + * the `repeatBuilder` callback. If the number of repeats + * is not specified it will repeat forever. This method is part of + * the actor 'Action' fluent API allowing action chaining + * + * ```typescript + * // Move up in a zig-zag by repeated moveBy's + * actor.actions.repeat(repeatCtx => { + * repeatCtx.moveBy(10, 0, 10); + * repeatCtx.moveBy(0, 10, 10); + * }, 5); + * ``` + * + * @param repeatBuilder The builder to specify the repeatable list of actions */ - public repeatForever(): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - this._queues[i].add(new Actions.RepeatForever(this._actors[i], this._actors[i].actionQueue.getActions())); - } + public repeatForever(repeatBuilder: (repeatContext: ActionContext) => any): ActionContext { + this._queue.add(new RepeatForever(this._actor, repeatBuilder)); return this; } @@ -279,13 +255,10 @@ export class ActionContext { * @param followDistance The distance to maintain when following, if not specified the actor will follow at the current distance. */ public follow(actor: Actor, followDistance?: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - if (followDistance === undefined) { - this._queues[i].add(new Actions.Follow(this._actors[i], actor)); - } else { - this._queues[i].add(new Actions.Follow(this._actors[i], actor, followDistance)); - } + if (followDistance === undefined) { + this._queue.add(new Follow(this._actor, actor)); + } else { + this._queue.add(new Follow(this._actor, actor, followDistance)); } return this; } @@ -297,13 +270,10 @@ export class ActionContext { * @param speed The speed in pixels per second to move, if not specified it will match the speed of the other actor */ public meet(actor: Actor, speed?: number): ActionContext { - const len = this._queues.length; - for (let i = 0; i < len; i++) { - if (speed === undefined) { - this._queues[i].add(new Actions.Meet(this._actors[i], actor)); - } else { - this._queues[i].add(new Actions.Meet(this._actors[i], actor, speed)); - } + if (speed === undefined) { + this._queue.add(new Meet(this._actor, actor)); + } else { + this._queue.add(new Meet(this._actor, actor, speed)); } return this; } @@ -312,17 +282,14 @@ export class ActionContext { * Returns a promise that resolves when the current action queue up to now * is finished. */ - public asPromise(): Promise { - const promises = this._queues.map((q, i) => { - const temp = new Promise((resolve) => { - q.add( - new Actions.CallMethod(this._actors[i], () => { - resolve(); - }) - ); - }); - return temp; + public asPromise(): Promise { + const temp = new Promise((resolve) => { + this._queue.add( + new CallMethod(() => { + resolve(); + }) + ); }); - return Promise.all(promises); + return temp; } } diff --git a/src/engine/Actions/ActionQueue.ts b/src/engine/Actions/ActionQueue.ts new file mode 100644 index 00000000..9842da26 --- /dev/null +++ b/src/engine/Actions/ActionQueue.ts @@ -0,0 +1,101 @@ +import { Actor } from '../Actor'; +import { Action } from './Action'; + +/** + * Action Queues represent an ordered sequence of actions + * + * Action queues are part of the [[ActionContext|Action API]] and + * store the list of actions to be executed for an [[Actor]]. + * + * Actors implement [[Actor.actions]] which can be manipulated by + * advanced users to adjust the actions currently being executed in the + * queue. + */ +export class ActionQueue { + private _actor: Actor; + private _actions: Action[] = []; + private _currentAction: Action; + private _completedActions: Action[] = []; + constructor(actor: Actor) { + this._actor = actor; + } + + /** + * Add an action to the sequence + * @param action + */ + public add(action: Action) { + this._actions.push(action); + } + + /** + * Remove an action by reference from the sequence + * @param action + */ + public remove(action: Action) { + const index = this._actions.indexOf(action); + this._actions.splice(index, 1); + } + + /** + * Removes all actions from this sequence + */ + public clearActions(): void { + this._actions.length = 0; + this._completedActions.length = 0; + if (this._currentAction) { + this._currentAction.stop(); + } + } + + /** + * + * @returns The total list of actions in this sequence complete or not + */ + public getActions(): Action[] { + return this._actions.concat(this._completedActions); + } + + /** + * + * @returns `true` if there are more actions to process in the sequence + */ + public hasNext(): boolean { + return this._actions.length > 0; + } + + /** + * @returns `true` if the current sequence of actions is done + */ + public isComplete(): boolean { + return this._actions.length === 0; + } + + /** + * Resets the sequence of actions, this is used to restart a sequence from the beginning + */ + public reset(): void { + this._actions = this.getActions(); + + const len = this._actions.length; + for (let i = 0; i < len; i++) { + this._actions[i].reset(); + } + this._completedActions = []; + } + + /** + * Update the queue which updates actions and handles completing actions + * @param elapsedMs + */ + public update(elapsedMs: number) { + if (this._actions.length > 0) { + this._currentAction = this._actions[0]; + this._currentAction.update(elapsedMs); + + if (this._currentAction.isComplete(this._actor)) { + this._completedActions.push(this._actions.shift()); + } + } + } +} diff --git a/src/engine/Actions/Index.ts b/src/engine/Actions/Index.ts index ab33d54e..b9c18f9f 100644 --- a/src/engine/Actions/Index.ts +++ b/src/engine/Actions/Index.ts @@ -1,9 +1,20 @@ export * from './ActionContext'; +export * from './ActionQueue'; export * from './Actionable'; export * from './RotationType'; -import * as actions from './Action'; -export { actions as Actions }; +export * from './Action/Repeat'; +export * from './Action/RepeatForever'; -// legacy Internal.Actions namespace support -export const Internal = { Actions: actions }; +export * from './Action/Blink'; +export * from './Action/Die'; +export * from './Action/EaseTo'; +export * from './Action/Fade'; +export * from './Action/Follow'; +export * from './Action/Meet'; +export * from './Action/MoveBy'; +export * from './Action/MoveTo'; +export * from './Action/RotateBy'; +export * from './Action/RotateTo'; +export * from './Action/ScaleBy'; +export * from './Action/ScaleTo'; diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 4d577ee9..a0313059 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -31,7 +31,6 @@ import { CanInitialize, CanUpdate, CanDraw, CanBeKilled } from './Interfaces/Lif import { Scene } from './Scene'; import { Logger } from './Util/Log'; import { ActionContext } from './Actions/ActionContext'; -import { ActionQueue } from './Actions/Action'; import { vec, Vector } from './Algebra'; import { Body } from './Collision/Body'; import { Eventable } from './Interfaces/Evented'; @@ -85,9 +84,7 @@ export interface ActorDefaults { * @hidden */ -export class ActorImpl - extends Entity - implements Actionable, Eventable, PointerEvents, CanInitialize, CanUpdate, CanDraw, CanBeKilled { +export class ActorImpl extends Entity implements Actionable, Eventable, PointerEvents, CanInitialize, CanUpdate, CanDraw, CanBeKilled { // #region Properties /** @@ -151,7 +148,7 @@ export class ActorImpl * Sets the velocity vector of the actor in pixels/sec */ public set vel(theVel: Vector) { - this.body.vel.setTo(theVel.x, theVel.y); + this.body.vel = theVel; } /** @@ -333,11 +330,6 @@ export class ActorImpl public opacity: number = 1; public previousOpacity: number = 1; - /** - * Direct access to the actor's [[ActionQueue]]. Useful if you are building custom actions. - */ - public actionQueue: ActionQueue; - /** * [[ActionContext|Action context]] of the actor. Useful for scripting actor behavior. */ @@ -460,11 +452,7 @@ export class ActorImpl * initial [[opacity]]. */ constructor(xOrConfig?: number | ActorArgs, y?: number, width?: number, height?: number, color?: Color) { - super([ - new TransformComponent(), - new GraphicsComponent(), - new CanvasDrawComponent((ctx, delta) => this.draw(ctx, delta)) - ]); + super([new TransformComponent(), new GraphicsComponent(), new CanvasDrawComponent((ctx, delta) => this.draw(ctx, delta))]); this.transform = this.get(TransformComponent); this.graphics = this.get(GraphicsComponent); @@ -543,7 +531,6 @@ export class ActorImpl this.traits.push(new Traits.CapturePointer()); // Build the action queue - this.actionQueue = new ActionQueue(this); this.actions = new ActionContext(this); } @@ -1107,8 +1094,8 @@ export class ActorImpl drawing.tick(delta, engine.stats.currFrame.id); } - // Update action queue - this.actionQueue.update(delta); + // Update action context + this.actions.update(delta); // Update color only opacity if (this.color) { diff --git a/src/engine/Trigger.ts b/src/engine/Trigger.ts index 29c6af08..e95f8523 100644 --- a/src/engine/Trigger.ts +++ b/src/engine/Trigger.ts @@ -1,6 +1,5 @@ import { Color } from './Drawing/Color'; import { Engine } from './Engine'; -import { ActionQueue } from './Actions/Action'; import { EventDispatcher } from './EventDispatcher'; import { Actor, isActor } from './Actor'; import { Vector } from './Algebra'; @@ -83,7 +82,6 @@ export class Trigger extends Actor { this.visible = opts.visible; this.body.collider.type = CollisionType.Passive; this.eventDispatcher = new EventDispatcher(this); - this.actionQueue = new ActionQueue(this); this.on('collisionstart', (evt: CollisionStartEvent) => { if (isActor(evt.other) && this.filter(evt.other)) { diff --git a/src/spec/ActionSpec.ts b/src/spec/ActionSpec.ts index 40812912..60db1669 100644 --- a/src/spec/ActionSpec.ts +++ b/src/spec/ActionSpec.ts @@ -35,15 +35,14 @@ describe('Action', () => { it('can blink at a frequency forever', () => { expect(actor.visible).toBe(true); - actor.actions.blink(200, 200).repeatForever(); + actor.actions.repeatForever((ctx) => ctx.blink(200, 200)); + actor.update(engine, 200); for (let i = 0; i < 2; i++) { - actor.update(engine, 200); expect(actor.visible).toBe(false); - actor.update(engine, 200); - expect(actor.visible).toBe(true); + expect(actor.visible).toBe(true); actor.update(engine, 200); } }); @@ -254,11 +253,65 @@ describe('Action', () => { }); describe('repeat', () => { + it('can repeat X times', () => { + const repeatCallback = jasmine.createSpy('repeat'); + actor.actions.repeat((ctx) => { + ctx.callMethod(repeatCallback); + }, 11); + for (let i = 0; i < 12; i++) { + actor.update(engine, 200); + } + // should run an action per update + expect(repeatCallback).toHaveBeenCalledTimes(11); + }); + + it('recalls the builder every repeat', () => { + const repeatCallback = jasmine.createSpy('repeat'); + actor.actions.repeat((ctx) => { + ctx.delay(200); + repeatCallback(); + }, 33); + // Overshoot + for (let i = 0; i < 50; i++) { + actor.update(engine, 200); + } + // should run an action per update + expect(repeatCallback).toHaveBeenCalledTimes(33); + }); + + it('can repeat moveBy X times', () => { + const repeatCallback = jasmine.createSpy('repeat'); + actor.actions.repeat((ctx) => { + ctx.moveBy(10, 0, 10); // move 10 pixels right at 10 px/sec + ctx.callMethod(repeatCallback); + }, 11); + + // Over shoot + for (let i = 0; i < 50; i++) { + actor.update(engine, 1000); + } + // should run an action per update + expect(actor.pos.x).toBe(110); + expect(actor.vel).toBeVector(ex.Vector.Zero); + expect(repeatCallback).toHaveBeenCalledTimes(11); + }); + + it('can repeat 1 time', () => { + const repeatCallback = jasmine.createSpy('repeat'); + actor.actions.repeat((ctx) => { + ctx.callMethod(repeatCallback); + }, 1); + for (let i = 0; i < 20; i++) { + actor.actions.update(200); + } + expect(repeatCallback).toHaveBeenCalledTimes(1); + }); + it('can repeat previous actions', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); - actor.actions.moveTo(20, 0, 10).moveTo(0, 0, 10).repeat(); + actor.actions.repeat((ctx) => ctx.moveTo(20, 0, 10).moveTo(0, 0, 10)); actor.update(engine, 1000); expect(actor.pos.x).toBe(10); @@ -295,7 +348,7 @@ describe('Action', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); - actor.actions.moveTo(20, 0, 10).moveTo(0, 0, 10).repeat(); + actor.actions.repeat((ctx) => ctx.moveTo(20, 0, 10).moveTo(0, 0, 10)); actor.update(engine, 1000); expect(actor.pos.x).toBe(10); @@ -335,7 +388,7 @@ describe('Action', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); - actor.actions.moveTo(20, 0, 10).moveTo(0, 0, 10).repeatForever(); + actor.actions.repeatForever((ctx) => ctx.moveTo(20, 0, 10).moveTo(0, 0, 10)); for (let i = 0; i < 20; i++) { actor.update(engine, 1000); @@ -359,11 +412,25 @@ describe('Action', () => { } }); + it('recalls the builder every repeat', () => { + const repeatCallback = jasmine.createSpy('repeat'); + actor.actions.repeatForever((ctx) => { + ctx.delay(200); + repeatCallback(); + }); + // Overshoot + for (let i = 0; i < 33; i++) { + actor.update(engine, 200); + } + // should run an action per update + expect(repeatCallback).toHaveBeenCalledTimes(33); + }); + it('can be stopped', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); - actor.actions.moveTo(20, 0, 10).moveTo(0, 0, 10).repeatForever(); + actor.actions.repeatForever((ctx) => ctx.moveTo(20, 0, 10).moveTo(0, 0, 10)); actor.update(engine, 1000); expect(actor.pos.x).toBe(10); @@ -685,7 +752,7 @@ describe('Action', () => { it('can go back and forth from 0 to 1 more than once (#512)', () => { actor.opacity = 0; - actor.actions.fade(1, 200).fade(0, 200).repeat(1); + actor.actions.repeat((ctx) => ctx.fade(1, 200).fade(0, 200), 1); for (let i = 0; i < 40; i++) { actor.update(engine, 20); }