From cc7c8052d6ba64f9c68dd0e1705fe28242e0d2aa Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Wed, 27 Nov 2024 11:55:01 -0600 Subject: [PATCH] feat: Actions with option bags and duration (#3289) - Added new option bag style input to actions with durations in milliseconds instead of speed ```typescript player.actions.rotateTo({angleRadians: angle, durationMs: 1000, rotationType}); player.actions.moveTo({pos: ex.vec(100, 100), durationMs: 1000}); player.actions.scaleTo({scale: ex.vec(2, 2), durationMs: 1000}); player.actions.repeatForever(ctx => { ctx.curveTo({ controlPoints: [cp1, cp2, dest], durationMs: 5000, mode: 'uniform' }); ctx.curveTo({ controlPoints: [cp2, cp1, start1], durationMs: 5000, mode: 'uniform' }); }); ``` --- CHANGELOG.md | 21 +- sandbox/tests/bezier/index.ts | 221 ++++++++++++--- sandbox/tests/rotation/rotation.ts | 2 +- src/engine/Actions/Action/CurveBy.ts | 4 +- src/engine/Actions/Action/CurveTo.ts | 2 +- src/engine/Actions/Action/Delay.ts | 4 +- src/engine/Actions/Action/EaseBy.ts | 3 + src/engine/Actions/Action/EaseTo.ts | 3 + src/engine/Actions/Action/Fade.ts | 4 +- src/engine/Actions/Action/Flash.ts | 6 +- src/engine/Actions/Action/Meet.ts | 10 +- src/engine/Actions/Action/MoveBy.ts | 93 ++++++- src/engine/Actions/Action/MoveTo.ts | 93 ++++++- src/engine/Actions/Action/RotateBy.ts | 113 +++++++- src/engine/Actions/Action/RotateTo.ts | 103 ++++++- src/engine/Actions/Action/ScaleBy.ts | 96 ++++++- src/engine/Actions/Action/ScaleTo.ts | 91 +++++- src/engine/Actions/ActionContext.ts | 195 ++++++++----- src/engine/Actions/ActionQueue.ts | 8 + src/engine/Actions/ActionsComponent.ts | 133 +++++++-- src/engine/Actions/RotationType.ts | 8 +- src/engine/Actions/{Index.ts => index.ts} | 0 src/engine/Collision/BoundingBox.ts | 2 +- src/engine/index.ts | 2 +- src/spec/ActionSpec.ts | 321 ++++++++++++++++++++++ 25 files changed, 1327 insertions(+), 211 deletions(-) rename src/engine/Actions/{Index.ts => index.ts} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 163a06514..a459901c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Deprecated - +- `easeTo(...)` and `easeBy(...)` actions marked deprecated, use `moveTo({easing: ...})` instead - `Vector.size` is deprecated, use `Vector.magnitude` instead - `ScreenShader` v_texcoord is deprecated, use v_uv. This is changed to match the materials shader API - `actor.getGlobalPos()` - use `actor.globalPos` instead @@ -76,6 +76,25 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added `easing` option to `moveTo(...)` +- Added new option bag style input to actions with durations in milliseconds instead of speed + ```typescript + player.actions.rotateTo({angleRadians: angle, durationMs: 1000, rotationType}); + player.actions.moveTo({pos: ex.vec(100, 100), durationMs: 1000}); + player.actions.scaleTo({scale: ex.vec(2, 2), durationMs: 1000}); + player.actions.repeatForever(ctx => { + ctx.curveTo({ + controlPoints: [cp1, cp2, dest], + durationMs: 5000, + mode: 'uniform' + }); + ctx.curveTo({ + controlPoints: [cp2, cp1, start1], + durationMs: 5000, + mode: 'uniform' + }); + }); + ``` - Added `ex.lerpAngle(startAngleRadians: number, endAngleRadians: number, rotationType: RotationType, time: number): number` in order to lerp angles between each other - Added `pointerenter` and `pointerleave` events to `ex.TileMap` tiles! - Added `pointerenter` and `pointerleave` events to `ex.IsometricMap` tiles! diff --git a/sandbox/tests/bezier/index.ts b/sandbox/tests/bezier/index.ts index f6f718489..387e311aa 100644 --- a/sandbox/tests/bezier/index.ts +++ b/sandbox/tests/bezier/index.ts @@ -2,9 +2,15 @@ var game = new ex.Engine({ width: 600, height: 400, displayMode: ex.DisplayMode.FillScreen + // physics: { + // spatialPartition: ex.SpatialPartitionStrategy.DynamicTree + // } }); game.toggleDebug(); +var hashGrid = (game.currentScene.world.systemManager.get(ex.PointerSystem) as any) + ._graphicsHashGrid as ex.SparseHashGrid; + var curve = new ex.BezierCurve({ controlPoints: [ex.vec(0, 700), ex.vec(100, -300), ex.vec(150, 800), ex.vec(500, 100)], quality: 10 @@ -14,6 +20,7 @@ var reverseCurve = curve.clone(); reverseCurve.controlPoints = [...reverseCurve.controlPoints].reverse() as any; var actor = new ex.Actor({ + name: 'Red Box', pos: ex.vec(500, 500), width: 100, height: 100, @@ -22,57 +29,185 @@ var actor = new ex.Actor({ }); game.add(actor); +let selection: ex.Actor | null = null; + +const start1 = ex.vec(500, 500); +const dest = ex.vec(500, 100); +const cp1 = ex.vec(100, 300); +const cp2 = ex.vec(150, 800); + +const curve2 = new ex.BezierCurve({ + controlPoints: [start1, cp1, cp2, dest], + quality: 10 +}); + +var points: ex.Vector[] = []; +const drawCurve = () => { + points.length = 0; + for (let i = 0; i < 100; i++) { + points.push(curve2.getPoint(i / 100)); + } +}; +drawCurve(); + +const startActor = new ex.Actor({ + name: 'start', + pos: start1, + radius: 20, + color: ex.Color.Black +}); +startActor.on('postupdate', () => { + if (selection === startActor) { + start1.x = startActor.pos.x; + start1.y = startActor.pos.y; + } +}); +startActor.on('pointerdown', (evt) => { + selection = startActor; +}); +game.add(startActor); + +const cp1Actor = new ex.Actor({ + name: 'cp1', + pos: cp1, + radius: 20, + color: ex.Color.Red +}); +cp1Actor.on('postupdate', () => { + if (selection === cp1Actor) { + cp1.x = cp1Actor.pos.x; + cp1.y = cp1Actor.pos.y; + } +}); +cp1Actor.on('pointerdown', (evt) => { + selection = cp1Actor; +}); +game.add(cp1Actor); + +const cp2Actor = new ex.Actor({ + name: 'cp2', + pos: cp2, + radius: 20, + color: ex.Color.Red +}); +cp2Actor.on('postupdate', () => { + if (selection === cp2Actor) { + cp2.x = cp2Actor.pos.x; + cp2.y = cp2Actor.pos.y; + } +}); +cp2Actor.on('pointerdown', (evt) => { + selection = cp2Actor; +}); +game.add(cp2Actor); + +const destActor = new ex.Actor({ + name: 'dest', + pos: dest, + radius: 20, + color: ex.Color.Black +}); +destActor.on('postupdate', (evt) => { + if (selection === destActor) { + dest.x = destActor.pos.x; + dest.y = destActor.pos.y; + } +}); +destActor.on('pointerdown', () => { + selection = destActor; +}); +game.add(destActor); + +game.input.pointers.primary.on('up', () => { + selection = null; + drawCurve(); +}); +game.input.pointers.primary.on('move', (evt) => { + if (selection) { + selection.pos = evt.worldPos; + drawCurve(); + } +}); + +actor.onPostUpdate = () => { + // ex.Debug.drawPoint(start1, {color: ex.Color.Black, size: 10}); + + ex.Debug.drawLine(start1, cp1, { color: ex.Color.Black }); + // ex.Debug.drawPoint(cp1, { color: ex.Color.Red, size: 10 }); + + ex.Debug.drawLine(dest, cp2, { color: ex.Color.Black }); + // ex.Debug.drawPoint(cp2, { color: ex.Color.Red, size: 10 }); + + // ex.Debug.drawPoint(dest, {color: ex.Color.Black, size: 10}); + // ex.Debug.draw(ctx => { + // (game.currentScene.physics.collisionProcessor as ex.SparseHashGridCollisionProcessor).hashGrid.debug(ctx, 1); + // }); +}; + actor.actions.repeatForever((ctx) => { ctx.curveTo({ - controlPoints: [ex.vec(100, -300), ex.vec(150, 800), ex.vec(500, 100)], - durationMs: 6000 - }); - ctx.curveBy({ - controlPoints: [ex.vec(100, 0), ex.vec(-100, 0), ex.vec(0, 300)], - durationMs: 1000 + controlPoints: [cp1, cp2, dest], + durationMs: 5000, + mode: 'uniform' }); ctx.curveTo({ - controlPoints: [ex.vec(150, 800), ex.vec(100, -300), ex.vec(0, 700)], - durationMs: 6000 + controlPoints: [cp2, cp1, start1], + durationMs: 5000, + mode: 'uniform' }); }); -var time = 0; -var points: ex.Vector[] = []; +// actor.actions.repeatForever((ctx) => { +// ctx.curveTo({ +// controlPoints: [ex.vec(100, -300), ex.vec(150, 800), ex.vec(500, 100)], +// durationMs: 6000 +// }); +// ctx.curveBy({ +// controlPoints: [ex.vec(100, 0), ex.vec(-100, 0), ex.vec(0, 300)], +// durationMs: 1000 +// }); +// ctx.curveTo({ +// controlPoints: [ex.vec(150, 800), ex.vec(100, -300), ex.vec(0, 700)], +// durationMs: 6000 +// }); +// }); + +// var time = 0; + game.onPostDraw = (ctx: ex.ExcaliburGraphicsContext, elapsedMs) => { - if (time < 5000) { - var t = ex.clamp(ex.remap(0, 5000, 0, 1, time), 0, 1); - - var p = curve.getPoint(t); - ctx.drawCircle(p, 20, ex.Color.Red); - var p2 = curve.getUniformPoint(t); - ctx.drawCircle(p2, 20, ex.Color.Purple); - points.push(p2); - - var tangent = curve.getTangent(t); - var normal = curve.getNormal(t); - ex.Debug.drawRay(new ex.Ray(p, tangent), { - distance: 100, - color: ex.Color.Yellow - }); - ex.Debug.drawRay(new ex.Ray(p, normal), { - distance: 100, - color: ex.Color.Green - }); - - var uTangent = curve.getUniformTangent(t); - var uNormal = curve.getUniformNormal(t); - ex.Debug.drawRay(new ex.Ray(p2, uTangent), { - distance: 100, - color: ex.Color.Yellow - }); - ex.Debug.drawRay(new ex.Ray(p2, uNormal), { - distance: 100, - color: ex.Color.Green - }); - - time += elapsedMs; - } + // if (time < 5000) { + // var t = ex.clamp(ex.remap(0, 5000, 0, 1, time), 0, 1); + + // var p = curve.getPoint(t); + // ctx.drawCircle(p, 20, ex.Color.Red); + // var p2 = curve.getUniformPoint(t); + // ctx.drawCircle(p2, 20, ex.Color.Purple); + // points.push(p2); + + // var tangent = curve.getTangent(t); + // var normal = curve.getNormal(t); + // ex.Debug.drawRay(new ex.Ray(p, tangent), { + // distance: 100, + // color: ex.Color.Yellow + // }); + // ex.Debug.drawRay(new ex.Ray(p, normal), { + // distance: 100, + // color: ex.Color.Green + // }); + + // var uTangent = curve.getUniformTangent(t); + // var uNormal = curve.getUniformNormal(t); + // ex.Debug.drawRay(new ex.Ray(p2, uTangent), { + // distance: 100, + // color: ex.Color.Yellow + // }); + // ex.Debug.drawRay(new ex.Ray(p2, uNormal), { + // distance: 100, + // color: ex.Color.Green + // }); + + // time += elapsedMs; + // } for (let i = 0; i < points.length - 1; i++) { ctx.drawLine(points[i], points[i + 1], ex.Color.Purple, 2); diff --git a/sandbox/tests/rotation/rotation.ts b/sandbox/tests/rotation/rotation.ts index 260f5d1c0..dce7314a3 100644 --- a/sandbox/tests/rotation/rotation.ts +++ b/sandbox/tests/rotation/rotation.ts @@ -85,7 +85,7 @@ engine.input.pointers.primary.on('down', (e: ex.PointerEvent) => { var vector = new ex.Vector(e.worldPos.x - player.pos.x, e.worldPos.y - player.pos.y); var angle = vector.toAngle(); - player.actions.rotateTo(angle, 1, rotationType); + player.actions.rotateTo({ angleRadians: angle, durationMs: 1000, rotationType }); //console.log('rotating from ' + player.rotation + ' to ' + angle); } }); diff --git a/src/engine/Actions/Action/CurveBy.ts b/src/engine/Actions/Action/CurveBy.ts index be74b0bd2..12b11a967 100644 --- a/src/engine/Actions/Action/CurveBy.ts +++ b/src/engine/Actions/Action/CurveBy.ts @@ -23,7 +23,7 @@ export interface CurveByOptions { /** * Quality when sampling uniform points on the curve. Samples = 4 * quality; * - * For bigger 'uniform' curves you may want to increase quality + * For bigger 'uniform' curves you may want to increase quality to make the motion appear smooth * * Default 4 */ @@ -64,13 +64,13 @@ export class CurveBy implements Action { this._curve.setControlPoint(3, this._curve.controlPoints[3].add(this._tx.globalPos)); this._started = true; } + this._currentMs -= elapsedMs; const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); if (this._mode === 'dynamic') { this._tx.pos = this._curve.getPoint(t); } else { this._tx.pos = this._curve.getUniformPoint(t); } - this._currentMs -= elapsedMs; if (this.isComplete(this._entity)) { if (this._mode === 'dynamic') { this._tx.pos = this._curve.getPoint(1); diff --git a/src/engine/Actions/Action/CurveTo.ts b/src/engine/Actions/Action/CurveTo.ts index e136b60a1..73a5ae81c 100644 --- a/src/engine/Actions/Action/CurveTo.ts +++ b/src/engine/Actions/Action/CurveTo.ts @@ -62,13 +62,13 @@ export class CurveTo implements Action { this._curve.setControlPoint(0, this._tx.globalPos.clone()); this._started = true; } + this._currentMs -= elapsedMs; const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); if (this._mode === 'dynamic') { this._tx.pos = this._curve.getPoint(t); } else { this._tx.pos = this._curve.getUniformPoint(t); } - this._currentMs -= elapsedMs; if (this.isComplete(this._entity)) { if (this._mode === 'dynamic') { this._tx.pos = this._curve.getPoint(1); diff --git a/src/engine/Actions/Action/Delay.ts b/src/engine/Actions/Action/Delay.ts index cbd9c7f2f..f8a0ed49d 100644 --- a/src/engine/Actions/Action/Delay.ts +++ b/src/engine/Actions/Action/Delay.ts @@ -6,8 +6,8 @@ export class Delay implements Action { private _delay: number; private _started: boolean = false; private _stopped = false; - constructor(delay: number) { - this._delay = delay; + constructor(durationMs: number) { + this._delay = durationMs; } public update(elapsedMs: number): void { diff --git a/src/engine/Actions/Action/EaseBy.ts b/src/engine/Actions/Action/EaseBy.ts index 3377f8383..de57362ca 100644 --- a/src/engine/Actions/Action/EaseBy.ts +++ b/src/engine/Actions/Action/EaseBy.ts @@ -4,6 +4,9 @@ import { Entity } from '../../EntityComponentSystem/Entity'; import { vec, Vector } from '../../Math/vector'; import { Action, nextActionId } from '../Action'; +/** + * @deprecated use moveBy({offset: Vector, durationMs: number, easing: EasingFunction}) + */ export class EaseBy implements Action { id = nextActionId(); private _tx: TransformComponent; diff --git a/src/engine/Actions/Action/EaseTo.ts b/src/engine/Actions/Action/EaseTo.ts index b743733d8..26537d404 100644 --- a/src/engine/Actions/Action/EaseTo.ts +++ b/src/engine/Actions/Action/EaseTo.ts @@ -4,6 +4,9 @@ import { MotionComponent } from '../../EntityComponentSystem/Components/MotionCo import { vec, Vector } from '../../Math/vector'; import { Action, nextActionId } from '../Action'; +/** + * @deprecated use moveTo({pos: Vector, durationMs: number, easing: EasingFunction}) + */ export class EaseTo implements Action { id = nextActionId(); private _tx: TransformComponent; diff --git a/src/engine/Actions/Action/Fade.ts b/src/engine/Actions/Action/Fade.ts index 786bd28a8..96007d338 100644 --- a/src/engine/Actions/Action/Fade.ts +++ b/src/engine/Actions/Action/Fade.ts @@ -14,10 +14,10 @@ export class Fade implements Action { private _started = false; private _stopped = false; - constructor(entity: Entity, endOpacity: number, time: number) { + constructor(entity: Entity, endOpacity: number, durationMs: number) { this._graphics = entity.get(GraphicsComponent); this._endOpacity = endOpacity; - this._remainingTime = this._originalTime = time; + this._remainingTime = this._originalTime = durationMs; } public update(elapsedMs: number): void { diff --git a/src/engine/Actions/Action/Flash.ts b/src/engine/Actions/Action/Flash.ts index d5d049e79..661d63059 100644 --- a/src/engine/Actions/Action/Flash.ts +++ b/src/engine/Actions/Action/Flash.ts @@ -17,9 +17,9 @@ export class Flash implements Action { private _total: number = 0; private _currentDuration: number = 0; - constructor(entity: Entity, color: Color, duration: number = 1000) { + constructor(entity: Entity, color: Color, durationMs: number = 1000) { this._graphics = entity.get(GraphicsComponent); - this._duration = duration; + this._duration = durationMs; this._entity = entity; this._material = entity.scene?.engine.graphicsContext.createMaterial({ name: 'flash-material', @@ -40,7 +40,7 @@ export class Flash implements Action { color.rgb = color.rgb * color.a; }` }) as Material; - this._total = duration; + this._total = durationMs; } public update(elapsedMs: number): void { diff --git a/src/engine/Actions/Action/Meet.ts b/src/engine/Actions/Action/Meet.ts index 749086cd2..79e78c1b8 100644 --- a/src/engine/Actions/Action/Meet.ts +++ b/src/engine/Actions/Action/Meet.ts @@ -10,13 +10,13 @@ export class Meet implements Action { private _motion: MotionComponent; private _meetTx: TransformComponent; private _meetMotion: MotionComponent; - public x: number; - public y: number; + public x!: number; + public y!: number; private _current: Vector; private _end: Vector; - private _dir: Vector; + private _dir!: Vector; private _speed: number; - private _distanceBetween: number; + private _distanceBetween!: number; private _started = false; private _stopped = false; private _speedWasSpecified = false; @@ -73,6 +73,6 @@ export class Meet implements Action { public reset(): void { this._started = false; this._stopped = false; - this._distanceBetween = undefined; + this._distanceBetween = Infinity; } } diff --git a/src/engine/Actions/Action/MoveBy.ts b/src/engine/Actions/Action/MoveBy.ts index fd541e7cc..8ea176a3d 100644 --- a/src/engine/Actions/Action/MoveBy.ts +++ b/src/engine/Actions/Action/MoveBy.ts @@ -1,24 +1,103 @@ import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent'; import { Entity } from '../../EntityComponentSystem/Entity'; +import { clamp, remap } from '../../Math'; import { Vector, vec } from '../../Math/vector'; +import { EasingFunction, EasingFunctions } from '../../Util/EasingFunctions'; import { Logger } from '../../Util/Log'; import { Action, nextActionId } from '../Action'; +export interface MoveByOptions { + offset: Vector; + durationMs: number; + easing?: EasingFunction; +} + +/** + * + */ +export function isMoveByOptions(x: any): x is MoveByOptions { + return x.offset instanceof Vector && typeof x.durationMs === 'number'; +} + +export class MoveByWithOptions implements Action { + id = nextActionId(); + private _start!: Vector; + private _end!: Vector; + private _durationMs: number; + private _tx: TransformComponent; + private _started: boolean = false; + private _currentMs: number; + private _stopped: boolean = false; + private _motion: MotionComponent; + private _offset: Vector; + private _easing: EasingFunction = EasingFunctions.Linear; + constructor( + public entity: Entity, + options: MoveByOptions + ) { + this._offset = options.offset; + this._easing = options.easing ?? this._easing; + this._tx = entity.get(TransformComponent); + this._motion = entity.get(MotionComponent); + if (!this._tx) { + throw new Error(`Entity ${entity.name} has no TransformComponent, can only MoveBy on Entities with TransformComponents.`); + } + this._durationMs = options.durationMs; + this._currentMs = this._durationMs; + } + update(elapsedMs: number): void { + if (!this._started) { + this._start = this._tx.pos.clone(); + this._end = this._start.add(this._offset); + this._started = true; + } + this._currentMs -= elapsedMs; + const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); + const currentPos = this._tx.pos; + const newPosX = this._easing(t, this._start.x, this._end.x, 1); + const newPosY = this._easing(t, this._start.y, this._end.y, 1); + const velX = (newPosX - currentPos.x) / (elapsedMs / 1000); + const velY = (newPosY - currentPos.y) / (elapsedMs / 1000); + this._motion.vel.x = velX; + this._motion.vel.y = velY; + + if (this.isComplete(this.entity)) { + this._tx.pos = vec(this._end.x, this._end.y); + this._motion.vel = vec(0, 0); + } + } + public isComplete(entity: Entity): boolean { + return this._stopped || this._currentMs < 0; + } + + public stop(): void { + this._motion.vel = vec(0, 0); + this._stopped = true; + this._currentMs = 0; + } + + public reset(): void { + this._currentMs = this._durationMs; + this._started = false; + this._stopped = false; + } +} + export class MoveBy implements Action { id = nextActionId(); private _tx: TransformComponent; private _motion: MotionComponent; private _entity: Entity; - public x: number; - public y: number; - private _distance: number; + 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 _start!: Vector; + private _offset!: Vector; + private _end!: Vector; + private _dir!: Vector; private _started = false; private _stopped = false; diff --git a/src/engine/Actions/Action/MoveTo.ts b/src/engine/Actions/Action/MoveTo.ts index 6523de7ef..fb163937c 100644 --- a/src/engine/Actions/Action/MoveTo.ts +++ b/src/engine/Actions/Action/MoveTo.ts @@ -1,31 +1,108 @@ import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent'; import { Entity } from '../../EntityComponentSystem/Entity'; +import { clamp, remap } from '../../Math'; import { Vector, vec } from '../../Math/vector'; +import { EasingFunction, EasingFunctions } from '../../Util/EasingFunctions'; import { Action, nextActionId } from '../Action'; +export interface MoveToOptions { + pos: Vector; + durationMs: number; + easing?: EasingFunction; +} + +/** + * + */ +export function isMoveToOptions(x: any): x is MoveToOptions { + return x.pos instanceof Vector && typeof x.durationMs === 'number'; +} + +export class MoveToWithOptions implements Action { + id = nextActionId(); + private _end: Vector; + private _durationMs: number; + private _tx: TransformComponent; + private _started: boolean = false; + private _start!: Vector; + private _currentMs: number; + private _stopped: boolean = false; + private _motion: MotionComponent; + private _easing: EasingFunction = EasingFunctions.Linear; + constructor( + public entity: Entity, + options: MoveToOptions + ) { + this._end = options.pos; + this._easing = options.easing ?? this._easing; + this._tx = entity.get(TransformComponent); + this._motion = entity.get(MotionComponent); + if (!this._tx) { + throw new Error(`Entity ${entity.name} has no TransformComponent, can only moveTo on Entities with TransformComponents.`); + } + this._durationMs = options.durationMs; + this._currentMs = this._durationMs; + } + update(elapsedMs: number): void { + if (!this._started) { + this._start = this._tx.pos.clone(); + this._started = true; + } + this._currentMs -= elapsedMs; + const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); + const currentPos = this._tx.pos; + const newPosX = this._easing(t, this._start.x, this._end.x, 1); + const newPosY = this._easing(t, this._start.y, this._end.y, 1); + const velX = (newPosX - currentPos.x) / (elapsedMs / 1000); + const velY = (newPosY - currentPos.y) / (elapsedMs / 1000); + this._motion.vel.x = velX; + this._motion.vel.y = velY; + + if (this.isComplete(this.entity)) { + this._tx.pos = vec(this._end.x, this._end.y); + this._motion.vel = vec(0, 0); + } + } + public isComplete(entity: Entity): boolean { + return this._stopped || this._currentMs < 0; + } + + public stop(): void { + this._motion.vel = vec(0, 0); + this._stopped = true; + this._currentMs = 0; + } + + public reset(): void { + this._currentMs = this._durationMs; + this._started = false; + this._stopped = false; + } +} + export class MoveTo implements Action { id = nextActionId(); private _tx: TransformComponent; private _motion: MotionComponent; - public x: number; - public y: number; - private _start: Vector; + public x!: number; + public y!: number; + private _start!: Vector; private _end: Vector; - private _dir: Vector; + private _dir!: Vector; private _speed: number; - private _distance: number; + private _distance!: number; private _started = false; private _stopped = false; constructor( public entity: Entity, - destx: number, - desty: number, + destX: number, + destY: number, speed: number ) { this._tx = entity.get(TransformComponent); this._motion = entity.get(MotionComponent); - this._end = new Vector(destx, desty); + this._end = new Vector(destX, destY); this._speed = speed; } diff --git a/src/engine/Actions/Action/RotateBy.ts b/src/engine/Actions/Action/RotateBy.ts index 67085d7c0..57e8716ba 100644 --- a/src/engine/Actions/Action/RotateBy.ts +++ b/src/engine/Actions/Action/RotateBy.ts @@ -3,26 +3,111 @@ import { RotationType } from '../RotationType'; import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent'; import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent'; import { Entity } from '../../EntityComponentSystem/Entity'; -import { TwoPI } from '../../Math/util'; +import { canonicalizeAngle, clamp, TwoPI } from '../../Math/util'; +import { lerpAngle, remap } from '../../Math'; + +export interface RotateByOptions { + /** + * Angle in radians to offset from the current and rotate + */ + angleRadiansOffset: number; + /** + * Duration to take in milliseconds + */ + durationMs: number; + /** + * Optionally provide type of rotation, default is RotationType.ShortestPath + */ + rotationType?: RotationType; +} + +/** + * + */ +export function isRotateByOptions(x: any): x is RotateByOptions { + return typeof x.angleRadiansOffset === 'number' && typeof x.durationMs === 'number'; +} + +export class RotateByWithOptions implements Action { + id = nextActionId(); + private _durationMs: number; + private _tx: TransformComponent; + private _started: boolean = false; + private _currentMs: number; + private _stopped: boolean = false; + private _motion: MotionComponent; + private _offset: number = 0; + private _startAngle: number = 0; + private _rotationType: RotationType; + private _endAngle: number = 0; + constructor( + public entity: Entity, + options: RotateByOptions + ) { + this._offset = options.angleRadiansOffset; + this._tx = entity.get(TransformComponent); + this._motion = entity.get(MotionComponent); + if (!this._tx) { + throw new Error(`Entity ${entity.name} has no TransformComponent, can only RotateBy on Entities with TransformComponents.`); + } + this._durationMs = options.durationMs; + this._rotationType = options.rotationType ?? RotationType.ShortestPath; + this._currentMs = this._durationMs; + } + update(elapsedMs: number): void { + if (!this._started) { + this._startAngle = this._tx.rotation; + this._endAngle = canonicalizeAngle(this._startAngle + this._offset); + this._started = true; + } + this._currentMs -= elapsedMs; + const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); + const newAngle = lerpAngle(this._startAngle, this._endAngle, this._rotationType, t); + const currentAngle = this._tx.rotation; + + const rx = (newAngle - currentAngle) / (elapsedMs / 1000); + this._motion.angularVelocity = rx; + + if (this.isComplete()) { + this._tx.rotation = this._endAngle; + this._motion.angularVelocity = 0; + } + } + public isComplete(): boolean { + return this._stopped || this._currentMs < 0; + } + + public stop(): void { + this._motion.angularVelocity = 0; + this._stopped = true; + this._currentMs = 0; + } + + public reset(): void { + this._currentMs = this._durationMs; + this._started = false; + this._stopped = false; + } +} export class RotateBy implements Action { id = nextActionId(); private _tx: TransformComponent; private _motion: MotionComponent; - public x: number; - public y: number; - private _start: number; - private _end: number; + 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 _direction!: number; + private _distance!: number; + private _shortDistance!: number; + private _longDistance!: number; + private _shortestPathIsPositive!: boolean; + private _currentNonCannonAngle!: number; private _started = false; private _stopped = false; constructor(entity: Entity, angleRadiansOffset: number, speed: number, rotationType?: RotationType) { @@ -111,8 +196,8 @@ export class RotateBy implements Action { public reset(): void { this._started = false; this._stopped = false; - this._start = undefined; - this._currentNonCannonAngle = undefined; - this._distance = undefined; + this._start = undefined as any; + this._currentNonCannonAngle = undefined as any; + this._distance = undefined as any; } } diff --git a/src/engine/Actions/Action/RotateTo.ts b/src/engine/Actions/Action/RotateTo.ts index 21f55ff83..3d9e45471 100644 --- a/src/engine/Actions/Action/RotateTo.ts +++ b/src/engine/Actions/Action/RotateTo.ts @@ -3,24 +3,107 @@ import { RotationType } from '../RotationType'; import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent'; import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent'; import { Entity } from '../../EntityComponentSystem/Entity'; -import { TwoPI } from '../../Math/util'; +import { clamp, TwoPI } from '../../Math/util'; +import { lerpAngle, remap } from '../../Math'; + +export interface RotateToOptions { + /** + * Absolute angle to rotate to in radians + */ + angleRadians: number; + /** + * Duration to take in milliseconds + */ + durationMs: number; + /** + * Optionally provide type of rotation, default is RotationType.ShortestPath + */ + rotationType?: RotationType; +} + +/** + * + */ +export function isRotateToOptions(x: any): x is RotateToOptions { + return typeof x.angleRadians === 'number' && typeof x.durationMs === 'number'; +} + +export class RotateToWithOptions implements Action { + id = nextActionId(); + private _durationMs: number; + private _tx: TransformComponent; + private _started: boolean = false; + private _currentMs: number; + private _stopped: boolean = false; + private _motion: MotionComponent; + private _endAngle: number = 0; + private _startAngle: number = 0; + private _rotationType: RotationType; + constructor( + public entity: Entity, + options: RotateToOptions + ) { + this._endAngle = options.angleRadians; + this._tx = entity.get(TransformComponent); + this._motion = entity.get(MotionComponent); + if (!this._tx) { + throw new Error(`Entity ${entity.name} has no TransformComponent, can only RotateTo on Entities with TransformComponents.`); + } + this._durationMs = options.durationMs; + this._rotationType = options.rotationType ?? RotationType.ShortestPath; + this._currentMs = this._durationMs; + } + update(elapsedMs: number): void { + if (!this._started) { + this._startAngle = this._tx.rotation; + this._started = true; + } + this._currentMs -= elapsedMs; + const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); + const newAngle = lerpAngle(this._startAngle, this._endAngle, this._rotationType, t); + const currentAngle = this._tx.rotation; + + const rx = (newAngle - currentAngle) / (elapsedMs / 1000); + this._motion.angularVelocity = rx; + + if (this.isComplete(this.entity)) { + this._tx.rotation = this._endAngle; + this._motion.angularVelocity = 0; + } + } + public isComplete(entity: Entity): boolean { + return this._stopped || this._currentMs < 0; + } + + public stop(): void { + this._motion.angularVelocity = 0; + this._stopped = true; + this._currentMs = 0; + } + + public reset(): void { + this._currentMs = this._durationMs; + this._started = false; + this._stopped = false; + } +} export class RotateTo implements Action { id = nextActionId(); private _tx: TransformComponent; private _motion: MotionComponent; - public x: number; - public y: number; - private _start: number; + 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 _direction!: number; + private _distance!: number; + private _shortDistance!: number; + private _longDistance!: number; + private _shortestPathIsPositive!: boolean; + private _currentNonCannonAngle!: number; private _started = false; private _stopped = false; constructor(entity: Entity, angleRadians: number, speed: number, rotationType?: RotationType) { diff --git a/src/engine/Actions/Action/ScaleBy.ts b/src/engine/Actions/Action/ScaleBy.ts index 703eb6d53..6832563e7 100644 --- a/src/engine/Actions/Action/ScaleBy.ts +++ b/src/engine/Actions/Action/ScaleBy.ts @@ -1,22 +1,100 @@ -import { Vector } from '../../Math/vector'; +import { vec, Vector } from '../../Math/vector'; import { Entity } from '../../EntityComponentSystem/Entity'; import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent'; import { Action, nextActionId } from '../Action'; +import { clamp, lerpVector, remap } from '../../Math'; + +export interface ScaleByOptions { + /** + * Absolute scale to change to + */ + scaleOffset: Vector; + /** + * Duration to take in milliseconds + */ + durationMs: number; +} + +/** + * + */ +export function isScaleByOptions(x: any): x is ScaleByOptions { + return typeof x.scaleOffset === 'object' && typeof x.durationMs === 'number'; +} + +export class ScaleByWithOptions implements Action { + id = nextActionId(); + private _durationMs: number; + private _tx: TransformComponent; + private _started: boolean = false; + private _currentMs: number; + private _stopped: boolean = false; + private _motion: MotionComponent; + private _endScale: Vector = vec(1, 1); + private _scaleOffset: Vector = vec(0, 0); + private _startScale: Vector = vec(1, 1); + constructor( + public entity: Entity, + options: ScaleByOptions + ) { + this._scaleOffset = options.scaleOffset; + this._tx = entity.get(TransformComponent); + this._motion = entity.get(MotionComponent); + if (!this._tx) { + throw new Error(`Entity ${entity.name} has no TransformComponent, can only ScaleBy on Entities with TransformComponents.`); + } + this._durationMs = options.durationMs; + this._currentMs = this._durationMs; + } + update(elapsedMs: number): void { + if (!this._started) { + this._startScale = this._tx.scale; + this._endScale = this._startScale.add(this._scaleOffset); + this._started = true; + } + this._currentMs -= elapsedMs; + const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); + const newScale = lerpVector(this._startScale, this._endScale, t); + const currentScale = this._tx.scale; + const sx = newScale.sub(currentScale).scale(1 / (elapsedMs / 1000)); + this._motion.scaleFactor = sx; + + if (this.isComplete()) { + this._tx.scale = this._endScale; + this._motion.angularVelocity = 0; + } + } + public isComplete(): boolean { + return this._stopped || this._currentMs < 0; + } + + public stop(): void { + this._motion.scaleFactor = Vector.Zero; + this._stopped = true; + this._currentMs = 0; + } + + public reset(): void { + this._currentMs = this._durationMs; + this._started = false; + this._stopped = false; + } +} export class ScaleBy implements Action { id = nextActionId(); private _tx: TransformComponent; private _motion: MotionComponent; - public x: number; - public y: number; - private _startScale: Vector; - private _endScale: Vector; + 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 _distanceX!: number; + private _distanceY!: number; + private _directionX!: number; + private _directionY!: number; private _started = false; private _stopped = false; private _speedX: number; diff --git a/src/engine/Actions/Action/ScaleTo.ts b/src/engine/Actions/Action/ScaleTo.ts index 234bb89c3..fbafd8676 100644 --- a/src/engine/Actions/Action/ScaleTo.ts +++ b/src/engine/Actions/Action/ScaleTo.ts @@ -1,23 +1,100 @@ -import { vec } from '../../Math/vector'; +import { vec, Vector } from '../../Math/vector'; import { MotionComponent } from '../../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent'; import { Action, nextActionId } from '../Action'; import { Entity } from '../../EntityComponentSystem/Entity'; +import { clamp, lerpVector, remap } from '../../Math'; + +export interface ScaleToOptions { + /** + * Absolute scale to change to + */ + scale: Vector; + /** + * Duration to take in milliseconds + */ + durationMs: number; +} + +/** + * + */ +export function isScaleToOptions(x: any): x is ScaleToOptions { + return typeof x.scale === 'object' && typeof x.durationMs === 'number'; +} + +export class ScaleToWithOptions implements Action { + id = nextActionId(); + private _durationMs: number; + private _tx: TransformComponent; + private _started: boolean = false; + private _currentMs: number; + private _stopped: boolean = false; + private _motion: MotionComponent; + private _endScale: Vector = vec(1, 1); + private _startScale: Vector = vec(1, 1); + constructor( + public entity: Entity, + options: ScaleToOptions + ) { + this._endScale = options.scale; + this._tx = entity.get(TransformComponent); + this._motion = entity.get(MotionComponent); + if (!this._tx) { + throw new Error(`Entity ${entity.name} has no TransformComponent, can only ScaleTo on Entities with TransformComponents.`); + } + this._durationMs = options.durationMs; + this._currentMs = this._durationMs; + } + update(elapsedMs: number): void { + if (!this._started) { + this._startScale = this._tx.scale; + this._started = true; + } + this._currentMs -= elapsedMs; + const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1); + const newScale = lerpVector(this._startScale, this._endScale, t); + const currentScale = this._tx.scale; + + const sx = newScale.sub(currentScale).scale(1 / (elapsedMs / 1000)); + this._motion.scaleFactor = sx; + + if (this.isComplete()) { + this._tx.scale = this._endScale; + this._motion.angularVelocity = 0; + } + } + public isComplete(): boolean { + return this._stopped || this._currentMs < 0; + } + + public stop(): void { + this._motion.scaleFactor = Vector.Zero; + this._stopped = true; + this._currentMs = 0; + } + + public reset(): void { + this._currentMs = this._durationMs; + this._started = false; + this._stopped = false; + } +} export class ScaleTo implements Action { id = nextActionId(); private _tx: TransformComponent; private _motion: MotionComponent; - public x: number; - public y: number; - private _startX: number; - private _startY: number; + 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 _distanceX!: number; + private _distanceY!: number; private _started = false; private _stopped = false; constructor(entity: Entity, scaleX: number, scaleY: number, speedX: number, speedY: number) { diff --git a/src/engine/Actions/ActionContext.ts b/src/engine/Actions/ActionContext.ts index 3026688a7..c67509da5 100644 --- a/src/engine/Actions/ActionContext.ts +++ b/src/engine/Actions/ActionContext.ts @@ -4,12 +4,12 @@ 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 { isMoveByOptions, MoveBy, MoveByOptions, MoveByWithOptions } from './Action/MoveBy'; +import { isMoveToOptions, MoveTo, MoveToOptions, MoveToWithOptions } from './Action/MoveTo'; +import { RotateTo, RotateToOptions, RotateToWithOptions } from './Action/RotateTo'; +import { RotateBy, RotateByOptions, RotateByWithOptions } from './Action/RotateBy'; +import { isScaleToOptions, ScaleTo, ScaleToOptions, ScaleToWithOptions } from './Action/ScaleTo'; +import { isScaleByOptions, ScaleBy, ScaleByOptions, ScaleByWithOptions } from './Action/ScaleBy'; import { CallMethod } from './Action/CallMethod'; import { EaseTo } from './Action/EaseTo'; import { EaseBy } from './Action/EaseBy'; @@ -64,8 +64,8 @@ export class ActionContext { } /** - * Animates an actor with a specified bezier curve, overrides the first control point - * to be the actor's current position. + * Animates an actor with a specified bezier curve by an offset to the current position, the start point is assumed + * to be the actors current position * @param options */ public curveBy(options: CurveByOptions): ActionContext { @@ -74,8 +74,8 @@ export class ActionContext { } /** - * Animates an actor with a specified bezier curve, overrides the first control point - * to be the actor's current position. + * Animates an actor with a specified bezier curve to an absolute world space coordinate, the start point is assumed + * to be the actors current position * @param options */ public curveTo(options: CurveToOptions): ActionContext { @@ -88,20 +88,22 @@ export class ActionContext { * specified duration using a given {@apilink EasingFunctions} and return back the actor. This * method is part of the actor 'Action' fluent API allowing action chaining. * @param pos The x,y vector location to move the actor to - * @param duration The time it should take the actor to move to the new location in milliseconds + * @param durationMs The time it should take the actor to move to the new location in milliseconds * @param easingFcn Use {@apilink EasingFunction} or a custom function to use to calculate position, Default is {@apilink EasingFunctions.Linear} + * @deprecated use new moveTo({pos: Vector, durationMs: number, easing: EasingFunction}) */ - public easeTo(pos: Vector, duration: number, easingFcn?: EasingFunction): ActionContext; + public easeTo(pos: Vector, durationMs: number, easingFcn?: EasingFunction): ActionContext; /** * This method will move an actor to the specified `x` and `y` position over the * specified duration using a given {@apilink EasingFunctions} and return back the actor. This * method is part of the actor 'Action' fluent API allowing action chaining. * @param x The x location to move the actor to * @param y The y location to move the actor to - * @param duration The time it should take the actor to move to the new location in milliseconds + * @param durationMs The time it should take the actor to move to the new location in milliseconds * @param easingFcn Use {@apilink EasingFunction} or a custom function to use to calculate position, Default is {@apilink EasingFunctions.Linear} + * @deprecated use new moveTo({pos: Vector, durationMs: number, easing: EasingFunction}) */ - public easeTo(x: number, y: number, duration: number, easingFcn?: EasingFunction): ActionContext; + public easeTo(x: number, y: number, durationMs: number, easingFcn?: EasingFunction): ActionContext; public easeTo(...args: any[]): ActionContext { let x = 0; let y = 0; @@ -127,18 +129,20 @@ export class ActionContext { * This method will move an actor by a specified vector offset relative to the current position given * a duration and a {@apilink EasingFunction}. This method is part of the actor 'Action' fluent API allowing action chaining. * @param offset Vector offset relative to the current position - * @param duration The duration in milliseconds + * @param durationMs The duration in milliseconds * @param easingFcn Use {@apilink EasingFunction} or a custom function to use to calculate position, Default is {@apilink EasingFunctions.Linear} + * @deprecated use new moveBy({offset: Vector, durationMs: number, easing: EasingFunction}) */ - public easeBy(offset: Vector, duration: number, easingFcn?: EasingFunction): ActionContext; + public easeBy(offset: Vector, durationMs: number, easingFcn?: EasingFunction): ActionContext; /** * This method will move an actor by a specified x and y offset relative to the current position given * a duration and a {@apilink EasingFunction}. This method is part of the actor 'Action' fluent API allowing action chaining. * @param offset Vector offset relative to the current position - * @param duration The duration in milliseconds + * @param durationMs The duration in milliseconds * @param easingFcn Use {@apilink EasingFunction} or a custom function to use to calculate position, Default is {@apilink EasingFunctions.Linear} + * @deprecated use new moveBy({offset: Vector, durationMs: number, easing: EasingFunction}) */ - public easeBy(offsetX: number, offsetY: number, duration: number, easingFcn?: EasingFunction): ActionContext; + public easeBy(offsetX: number, offsetY: number, durationMs: number, easingFcn?: EasingFunction): ActionContext; public easeBy(...args: any[]): ActionContext { let offsetX = 0; let offsetY = 0; @@ -160,6 +164,12 @@ export class ActionContext { return this; } + /** + * Moves an actor to a specified {@link Vector} in a given duration in milliseconds. + * You may optionally specify an {@link EasingFunction} + * @param options + */ + public moveTo(options: MoveToOptions): ActionContext; /** * This method will move an actor to the specified x and y position at the * speed specified (in pixels per second) and return back the actor. This @@ -177,23 +187,32 @@ export class ActionContext { * @param speed The speed in pixels per second to move */ public moveTo(x: number, y: number, speed: number): ActionContext; - public moveTo(xOrPos: number | Vector, yOrSpeed: number, speedOrUndefined?: number | undefined): ActionContext { + public moveTo(xOrPosOrOptions: number | Vector | MoveToOptions, yOrSpeed?: number, speedOrUndefined?: number): ActionContext { let x = 0; let y = 0; let speed = 0; - if (xOrPos instanceof Vector) { - x = xOrPos.x; - y = xOrPos.y; - speed = yOrSpeed; - } else { - x = xOrPos; + if (xOrPosOrOptions instanceof Vector) { + x = xOrPosOrOptions.x; + y = xOrPosOrOptions.y; + speed = +(yOrSpeed ?? 0); + this._queue.add(new MoveTo(this._entity, x, y, speed)); + } else if (typeof xOrPosOrOptions === 'number' && typeof yOrSpeed === 'number' && typeof speedOrUndefined === 'number') { + x = xOrPosOrOptions; y = yOrSpeed; speed = speedOrUndefined; + this._queue.add(new MoveTo(this._entity, x, y, speed)); + } else if (isMoveToOptions(xOrPosOrOptions)) { + this._queue.add(new MoveToWithOptions(this._entity, xOrPosOrOptions)); } - this._queue.add(new MoveTo(this._entity, x, y, speed)); return this; } + /** + * Moves an actor by a specified offset {@link Vector} in a given duration in milliseconds. + * You may optionally specify an {@link EasingFunction} + * @param options + */ + public moveBy(options: MoveByOptions): ActionContext; /** * This method will move an actor by the specified x offset and y offset from its current position, at a certain speed. * This method is part of the actor 'Action' fluent API allowing action chaining. @@ -203,23 +222,36 @@ export class ActionContext { */ public moveBy(offset: Vector, speed: number): ActionContext; public moveBy(xOffset: number, yOffset: number, speed: number): ActionContext; - public moveBy(xOffsetOrVector: number | Vector, yOffsetOrSpeed: number, speedOrUndefined?: number | undefined): ActionContext { + public moveBy( + xOffsetOrVectorOrOptions: number | Vector | MoveByOptions, + yOffsetOrSpeed?: number, + speedOrUndefined?: number + ): ActionContext { let xOffset = 0; let yOffset = 0; let speed = 0; - if (xOffsetOrVector instanceof Vector) { - xOffset = xOffsetOrVector.x; - yOffset = xOffsetOrVector.y; + if (xOffsetOrVectorOrOptions instanceof Vector && typeof yOffsetOrSpeed === 'number') { + xOffset = xOffsetOrVectorOrOptions.x; + yOffset = xOffsetOrVectorOrOptions.y; speed = yOffsetOrSpeed; - } else { - xOffset = xOffsetOrVector; + this._queue.add(new MoveBy(this._entity, xOffset, yOffset, speed)); + } else if (typeof xOffsetOrVectorOrOptions === 'number' && typeof yOffsetOrSpeed === 'number' && typeof speedOrUndefined === 'number') { + xOffset = xOffsetOrVectorOrOptions; yOffset = yOffsetOrSpeed; speed = speedOrUndefined; + this._queue.add(new MoveBy(this._entity, xOffset, yOffset, speed)); + } else if (isMoveByOptions(xOffsetOrVectorOrOptions)) { + this._queue.add(new MoveByWithOptions(this._entity, xOffsetOrVectorOrOptions)); } - this._queue.add(new MoveBy(this._entity, xOffset, yOffset, speed)); return this; } + /** + * Rotates an actor to a specified angle over a duration in milliseconds, + * you make pick a rotation strategy {@link RotationType} to pick the direction + * @param options + */ + public rotateTo(options: RotateToOptions): ActionContext; /** * This method will rotate an actor to the specified angle at the speed * specified (in radians per second) and return back the actor. This @@ -228,11 +260,22 @@ export class ActionContext { * @param speed The angular velocity of the rotation specified in radians per second * @param rotationType The {@apilink RotationType} to use for this rotation */ - public rotateTo(angleRadians: number, speed: number, rotationType?: RotationType): ActionContext { - this._queue.add(new RotateTo(this._entity, angleRadians, speed, rotationType)); + public rotateTo(angleRadians: number, speed: number, rotationType?: RotationType): ActionContext; + public rotateTo(angleRadiansOrOptions: number | RotateToOptions, speed?: number, rotationType?: RotationType): ActionContext { + if (typeof angleRadiansOrOptions === 'number' && typeof speed === 'number') { + this._queue.add(new RotateTo(this._entity, angleRadiansOrOptions, speed, rotationType)); + } else if (typeof angleRadiansOrOptions === 'object') { + this._queue.add(new RotateToWithOptions(this._entity, angleRadiansOrOptions)); + } return this; } + /** + * Rotates an actor by a specified offset angle over a duration in milliseconds, + * you make pick a rotation strategy {@link RotationType} to pick the direction + * @param options + */ + public rotateBy(options: RotateByOptions): ActionContext; /** * This method will rotate an actor by the specified angle offset, from it's current rotation given a certain speed * in radians/sec and return back the actor. This method is part @@ -241,11 +284,21 @@ export class ActionContext { * @param speed The speed in radians/sec the actor should rotate at * @param rotationType The {@apilink RotationType} to use for this rotation, default is shortest path */ - public rotateBy(angleRadiansOffset: number, speed: number, rotationType?: RotationType): ActionContext { - this._queue.add(new RotateBy(this._entity, angleRadiansOffset, speed, rotationType)); + public rotateBy(angleRadiansOffset: number, speed: number, rotationType?: RotationType): ActionContext; + public rotateBy(angleRadiansOffsetOrOptions: number | RotateByOptions, speed?: number, rotationType?: RotationType): ActionContext { + if (typeof angleRadiansOffsetOrOptions === 'object') { + this._queue.add(new RotateByWithOptions(this._entity, angleRadiansOffsetOrOptions)); + } else { + this._queue.add(new RotateBy(this._entity, angleRadiansOffsetOrOptions, speed as number, rotationType)); + } return this; } + /** + * Scales an actor to a specified scale {@link Vector} over a duration in milliseconds + * @param options + */ + public scaleTo(options: ScaleToOptions): ActionContext; /** * This method will scale an actor to the specified size at the speed * specified (in magnitude increase per second) and return back the @@ -267,8 +320,8 @@ export class ActionContext { */ public scaleTo(sizeX: number, sizeY: number, speedX: number, speedY: number): ActionContext; public scaleTo( - sizeXOrVector: number | Vector, - sizeYOrSpeed: number | Vector, + sizeXOrVectorOrOptions: number | Vector | ScaleToOptions, + sizeYOrSpeed?: number | Vector, speedXOrUndefined?: number | undefined, speedYOrUndefined?: number | undefined ): ActionContext { @@ -277,25 +330,35 @@ export class ActionContext { let speedX = 0; let speedY = 0; - if (sizeXOrVector instanceof Vector && sizeYOrSpeed instanceof Vector) { - sizeX = sizeXOrVector.x; - sizeY = sizeXOrVector.y; + if (isScaleToOptions(sizeXOrVectorOrOptions)) { + this._queue.add(new ScaleToWithOptions(this._entity, sizeXOrVectorOrOptions)); + return this; + } + + if (sizeXOrVectorOrOptions instanceof Vector && sizeYOrSpeed instanceof Vector) { + sizeX = sizeXOrVectorOrOptions.x; + sizeY = sizeXOrVectorOrOptions.y; speedX = sizeYOrSpeed.x; speedY = sizeYOrSpeed.y; } - if (typeof sizeXOrVector === 'number' && typeof sizeYOrSpeed === 'number') { - sizeX = sizeXOrVector; + if (typeof sizeXOrVectorOrOptions === 'number' && typeof sizeYOrSpeed === 'number') { + sizeX = sizeXOrVectorOrOptions; sizeY = sizeYOrSpeed; - speedX = speedXOrUndefined; - speedY = speedYOrUndefined; + speedX = speedXOrUndefined as any; + speedY = speedYOrUndefined as any; } this._queue.add(new ScaleTo(this._entity, sizeX, sizeY, speedX, speedY)); return this; } + /** + * Scales an actor by a specified scale offset {@link Vector} over a duration in milliseconds + * @param options + */ + public scaleBy(options: ScaleByOptions): ActionContext; /** * This method will scale an actor by an amount relative to the current scale at a certain speed in scale units/sec * and return back the actor. This method is part of the @@ -313,22 +376,30 @@ export class ActionContext { * @param speed The speed to scale at in scale units/sec */ public scaleBy(sizeOffsetX: number, sizeOffsetY: number, speed: number): ActionContext; - public scaleBy(sizeOffsetXOrVector: number | Vector, sizeOffsetYOrSpeed: number, speed?: number | undefined): ActionContext { + public scaleBy( + sizeOffsetXOrVectorOrOptions: number | Vector | ScaleByOptions, + sizeOffsetYOrSpeed?: number, + speed?: number | undefined + ): ActionContext { + if (isScaleByOptions(sizeOffsetXOrVectorOrOptions)) { + this._queue.add(new ScaleByWithOptions(this._entity, sizeOffsetXOrVectorOrOptions)); + return this; + } let sizeOffsetX = 1; let sizeOffsetY = 1; - if (sizeOffsetXOrVector instanceof Vector) { - sizeOffsetX = sizeOffsetXOrVector.x; - sizeOffsetY = sizeOffsetXOrVector.y; + if (sizeOffsetXOrVectorOrOptions instanceof Vector) { + sizeOffsetX = sizeOffsetXOrVectorOrOptions.x; + sizeOffsetY = sizeOffsetXOrVectorOrOptions.y; speed = sizeOffsetYOrSpeed; } - if (typeof sizeOffsetXOrVector === 'number' && typeof sizeOffsetYOrSpeed === 'number') { - sizeOffsetX = sizeOffsetXOrVector; + if (typeof sizeOffsetXOrVectorOrOptions === 'number' && typeof sizeOffsetYOrSpeed === 'number') { + sizeOffsetX = sizeOffsetXOrVectorOrOptions; sizeOffsetY = sizeOffsetYOrSpeed; } - this._queue.add(new ScaleBy(this._entity, sizeOffsetX, sizeOffsetY, speed)); + this._queue.add(new ScaleBy(this._entity, sizeOffsetX, sizeOffsetY, speed as any)); return this; } @@ -351,20 +422,20 @@ export class ActionContext { * to the provided value by a specified time (in milliseconds). This method is * part of the actor 'Action' fluent API allowing action chaining. * @param opacity The ending opacity - * @param duration The time it should take to fade the actor (in milliseconds) + * @param durationMs The time it should take to fade the actor (in milliseconds) */ - public fade(opacity: number, duration: number): ActionContext { - this._queue.add(new Fade(this._entity, opacity, duration)); + public fade(opacity: number, durationMs: number): ActionContext { + this._queue.add(new Fade(this._entity, opacity, durationMs)); return this; } /** * This will cause an actor to flash a specific color for a period of time * @param color - * @param duration The duration in milliseconds + * @param durationMs The duration in milliseconds */ - public flash(color: Color, duration: number = 1000) { - this._queue.add(new Flash(this._entity, color, duration)); + public flash(color: Color, durationMs: number = 1000) { + this._queue.add(new Flash(this._entity, color, durationMs)); return this; } @@ -372,10 +443,10 @@ export class ActionContext { * This method will delay the next action from executing for a certain * amount of time (in milliseconds). This method is part of the actor * 'Action' fluent API allowing action chaining. - * @param duration The amount of time to delay the next action in the queue from executing in milliseconds + * @param durationMs The amount of time to delay the next action in the queue from executing in milliseconds */ - public delay(duration: number): ActionContext { - this._queue.add(new Delay(duration)); + public delay(durationMs: number): ActionContext { + this._queue.add(new Delay(durationMs)); return this; } diff --git a/src/engine/Actions/ActionQueue.ts b/src/engine/Actions/ActionQueue.ts index 32655947f..e13af01a0 100644 --- a/src/engine/Actions/ActionQueue.ts +++ b/src/engine/Actions/ActionQueue.ts @@ -57,6 +57,14 @@ export class ActionQueue { return this._actions.concat(this._completedActions); } + public getIncompleteActions(): Action[] { + return this._actions; + } + + public getCurrentAction(): Action | null { + return this._currentAction; + } + /** * * @returns `true` if there are more actions to process in the sequence diff --git a/src/engine/Actions/ActionsComponent.ts b/src/engine/Actions/ActionsComponent.ts index c654a4748..7544e9139 100644 --- a/src/engine/Actions/ActionsComponent.ts +++ b/src/engine/Actions/ActionsComponent.ts @@ -12,6 +12,8 @@ import { Action } from './Action'; import { Color } from '../Color'; import { CurveToOptions } from './Action/CurveTo'; import { CurveByOptions } from './Action/CurveBy'; +import { MoveToOptions } from './Action/MoveTo'; +import { MoveByOptions, RotateByOptions, RotateToOptions, ScaleByOptions, ScaleToOptions } from './index'; export interface ActionContextMethods extends Pick {} @@ -45,6 +47,10 @@ export class ActionsComponent extends Component implements ActionContextMethods return this._ctx.getQueue(); } + /** + * Runs a specific action in the action queue + * @param action + */ public runAction(action: Action): ActionContext { if (!this._ctx) { throw new Error('Actions component not attached to an entity, cannot run action'); @@ -67,10 +73,20 @@ export class ActionsComponent extends Component implements ActionContextMethods this._ctx?.clearActions(); } + /** + * Animates an actor with a specified bezier curve by an offset to the current position, the start point is assumed + * to be the actors current position + * @param options + */ public curveBy(options: CurveByOptions): ActionContext { return this._getCtx().curveBy.apply(this._ctx, [options]); } + /** + * Animates an actor with a specified bezier curve to an absolute world space coordinate, the start point is assumed + * to be the actors current position + * @param options + */ public curveTo(options: CurveToOptions): ActionContext { return this._getCtx().curveTo.apply(this._ctx, [options]); } @@ -80,30 +96,53 @@ export class ActionsComponent extends Component implements ActionContextMethods * specified duration using a given {@apilink EasingFunctions} and return back the actor. This * method is part of the actor 'Action' fluent API allowing action chaining. * @param pos The x,y vector location to move the actor to - * @param duration The time it should take the actor to move to the new location in milliseconds + * @param durationMs The time it should take the actor to move to the new location in milliseconds * @param easingFcn Use {@apilink EasingFunctions} or a custom function to use to calculate position, Default is {@apilink EasingFunctions.Linear} + * @deprecated use new moveTo({pos: Vector, durationMs: number, easing: EasingFunction}) */ - public easeTo(pos: Vector, duration: number, easingFcn?: EasingFunction): ActionContext; + public easeTo(pos: Vector, durationMs: number, easingFcn?: EasingFunction): ActionContext; /** * This method will move an actor to the specified `x` and `y` position over the * specified duration using a given {@apilink EasingFunctions} and return back the actor. This * method is part of the actor 'Action' fluent API allowing action chaining. * @param x The x location to move the actor to * @param y The y location to move the actor to - * @param duration The time it should take the actor to move to the new location in milliseconds + * @param durationMs The time it should take the actor to move to the new location in milliseconds * @param easingFcn Use {@apilink EasingFunctions} or a custom function to use to calculate position, Default is {@apilink EasingFunctions.Linear} + * @deprecated use new moveTo({pos: Vector, durationMs: number, easing: EasingFunction}) */ - public easeTo(x: number, y: number, duration: number, easingFcn?: EasingFunction): ActionContext; + public easeTo(x: number, y: number, durationMs: number, easingFcn?: EasingFunction): ActionContext; public easeTo(...args: any[]): ActionContext { return this._getCtx().easeTo.apply(this._ctx, args as any); } - public easeBy(offset: Vector, duration: number, easingFcn?: EasingFunction): ActionContext; - public easeBy(offsetX: number, offsetY: number, duration: number, easingFcn?: EasingFunction): ActionContext; + /** + * + * @param offset + * @param durationMs + * @param easingFcn + * @deprecated use new moveBy({pos: Vector, durationMs: number, easing: EasingFunction}) + */ + public easeBy(offset: Vector, durationMs: number, easingFcn?: EasingFunction): ActionContext; + /** + * + * @param offsetX + * @param offsetY + * @param durationMs + * @param easingFcn + * @deprecated use new moveBy({pos: Vector, durationMs: number, easing: EasingFunction}) + */ + public easeBy(offsetX: number, offsetY: number, durationMs: number, easingFcn?: EasingFunction): ActionContext; public easeBy(...args: any[]): ActionContext { return this._getCtx().easeBy.apply(this._ctx, args as any); } + /** + * Moves an actor to a specified {@link Vector} in a given duration in milliseconds. + * You may optionally specify an {@link EasingFunction} + * @param options + */ + public moveTo(options: MoveToOptions): ActionContext; /** * This method will move an actor to the specified x and y position at the * speed specified (in pixels per second) and return back the actor. This @@ -121,10 +160,16 @@ export class ActionsComponent extends Component implements ActionContextMethods * @param speed The speed in pixels per second to move */ public moveTo(x: number, y: number, speed: number): ActionContext; - public moveTo(xOrPos: number | Vector, yOrSpeed: number, speedOrUndefined?: number): ActionContext { - return this._getCtx().moveTo.apply(this._ctx, [xOrPos, yOrSpeed, speedOrUndefined] as any); + public moveTo(xOrPosOrOptions: number | Vector | MoveToOptions, yOrSpeed?: number, speedOrUndefined?: number): ActionContext { + return this._getCtx().moveTo.apply(this._ctx, [xOrPosOrOptions, yOrSpeed, speedOrUndefined] as any); } + /** + * Moves an actor by a specified offset {@link Vector} in a given duration in milliseconds. + * You may optionally specify an {@link EasingFunction} + * @param options + */ + public moveBy(options: MoveByOptions): ActionContext; /** * This method will move an actor by the specified x offset and y offset from its current position, at a certain speed. * This method is part of the actor 'Action' fluent API allowing action chaining. @@ -140,10 +185,20 @@ export class ActionsComponent extends Component implements ActionContextMethods * @param speed The speed in pixels per second the actor should move */ public moveBy(xOffset: number, yOffset: number, speed: number): ActionContext; - public moveBy(xOffsetOrVector: number | Vector, yOffsetOrSpeed: number, speedOrUndefined?: number): ActionContext { - return this._getCtx().moveBy.apply(this._ctx, [xOffsetOrVector, yOffsetOrSpeed, speedOrUndefined] as any); + public moveBy( + xOffsetOrVectorOptions: number | Vector | MoveByOptions, + yOffsetOrSpeed?: number, + speedOrUndefined?: number + ): ActionContext { + return this._getCtx().moveBy.apply(this._ctx, [xOffsetOrVectorOptions, yOffsetOrSpeed, speedOrUndefined] as any); } + /** + * Rotates an actor to a specified angle over a duration in milliseconds, + * you make pick a rotation strategy {@link RotationType} to pick the direction + * @param options + */ + public rotateTo(options: RotateToOptions): ActionContext; /** * This method will rotate an actor to the specified angle at the speed * specified (in radians per second) and return back the actor. This @@ -152,10 +207,17 @@ export class ActionsComponent extends Component implements ActionContextMethods * @param speed The angular velocity of the rotation specified in radians per second * @param rotationType The {@apilink RotationType} to use for this rotation */ - public rotateTo(angleRadians: number, speed: number, rotationType?: RotationType): ActionContext { - return this._getCtx().rotateTo(angleRadians, speed, rotationType); + public rotateTo(angleRadians: number, speed: number, rotationType?: RotationType): ActionContext; + public rotateTo(angleRadians: number | RotateToOptions, speed?: number, rotationType?: RotationType): ActionContext { + return this._getCtx().rotateTo.apply(this._ctx, [angleRadians, speed, rotationType] as any); } + /** + * Rotates an actor by a specified offset angle over a duration in milliseconds, + * you make pick a rotation strategy {@link RotationType} to pick the direction + * @param options + */ + public rotateBy(options: RotateByOptions): ActionContext; /** * This method will rotate an actor by the specified angle offset, from it's current rotation given a certain speed * in radians/sec and return back the actor. This method is part @@ -164,10 +226,16 @@ export class ActionsComponent extends Component implements ActionContextMethods * @param speed The speed in radians/sec the actor should rotate at * @param rotationType The {@apilink RotationType} to use for this rotation, default is shortest path */ - public rotateBy(angleRadiansOffset: number, speed: number, rotationType?: RotationType): ActionContext { - return this._getCtx().rotateBy(angleRadiansOffset, speed, rotationType); + public rotateBy(angleRadiansOffset: number, speed: number, rotationType?: RotationType): ActionContext; + public rotateBy(angleRadiansOffsetOrOptions: number | RotateByOptions, speed?: number, rotationType?: RotationType): ActionContext { + return this._getCtx().rotateBy.apply(this._ctx, [angleRadiansOffsetOrOptions, speed, rotationType] as any); } + /** + * Scales an actor to a specified scale {@link Vector} over a duration + * @param options + */ + public scaleTo(options: ScaleToOptions): ActionContext; /** * This method will scale an actor to the specified size at the speed * specified (in magnitude increase per second) and return back the @@ -189,14 +257,19 @@ export class ActionsComponent extends Component implements ActionContextMethods */ public scaleTo(sizeX: number, sizeY: number, speedX: number, speedY: number): ActionContext; public scaleTo( - sizeXOrVector: number | Vector, - sizeYOrSpeed: number | Vector, + sizeXOrVectorOrOptions: number | Vector | ScaleToOptions, + sizeYOrSpeed?: number | Vector, speedXOrUndefined?: number, speedYOrUndefined?: number ): ActionContext { - return this._getCtx().scaleTo.apply(this._ctx, [sizeXOrVector, sizeYOrSpeed, speedXOrUndefined, speedYOrUndefined] as any); + return this._getCtx().scaleTo.apply(this._ctx, [sizeXOrVectorOrOptions, sizeYOrSpeed, speedXOrUndefined, speedYOrUndefined] as any); } + /** + * Scales an actor by a specified scale offset {@link Vector} over a duration in milliseconds + * @param options + */ + public scaleBy(options: ScaleByOptions): ActionContext; /** * This method will scale an actor by an amount relative to the current scale at a certain speed in scale units/sec * and return back the actor. This method is part of the @@ -214,8 +287,12 @@ export class ActionsComponent extends Component implements ActionContextMethods * @param speed The speed to scale at in scale units/sec */ public scaleBy(sizeOffsetX: number, sizeOffsetY: number, speed: number): ActionContext; - public scaleBy(sizeOffsetXOrVector: number | Vector, sizeOffsetYOrSpeed: number, speed?: number): ActionContext { - return this._getCtx().scaleBy.apply(this._ctx, [sizeOffsetXOrVector, sizeOffsetYOrSpeed, speed] as any); + public scaleBy( + sizeOffsetXOrVectorOrOptions: number | Vector | ScaleByOptions, + sizeOffsetYOrSpeed?: number, + speed?: number + ): ActionContext { + return this._getCtx().scaleBy.apply(this._ctx, [sizeOffsetXOrVectorOrOptions, sizeOffsetYOrSpeed, speed] as any); } /** @@ -236,29 +313,29 @@ export class ActionsComponent extends Component implements ActionContextMethods * to the provided value by a specified time (in milliseconds). This method is * part of the actor 'Action' fluent API allowing action chaining. * @param opacity The ending opacity - * @param duration The time it should take to fade the actor (in milliseconds) + * @param durationMs The time it should take to fade the actor (in milliseconds) */ - public fade(opacity: number, duration: number): ActionContext { - return this._getCtx().fade(opacity, duration); + public fade(opacity: number, durationMs: number): ActionContext { + return this._getCtx().fade(opacity, durationMs); } /** * This will cause an actor to flash a specific color for a period of time * @param color - * @param duration The duration in milliseconds + * @param durationMs The duration in milliseconds */ - public flash(color: Color, duration: number = 1000) { - return this._getCtx().flash(color, duration); + public flash(color: Color, durationMs: number = 1000) { + return this._getCtx().flash(color, durationMs); } /** * This method will delay the next action from executing for a certain * amount of time (in milliseconds). This method is part of the actor * 'Action' fluent API allowing action chaining. - * @param duration The amount of time to delay the next action in the queue from executing in milliseconds + * @param durationMs The amount of time to delay the next action in the queue from executing in milliseconds */ - public delay(duration: number): ActionContext { - return this._getCtx().delay(duration); + public delay(durationMs: number): ActionContext { + return this._getCtx().delay(durationMs); } /** diff --git a/src/engine/Actions/RotationType.ts b/src/engine/Actions/RotationType.ts index 08bdad5bc..b97b7507a 100644 --- a/src/engine/Actions/RotationType.ts +++ b/src/engine/Actions/RotationType.ts @@ -6,20 +6,20 @@ export enum RotationType { * Rotation via `ShortestPath` will use the smallest angle * between the starting and ending points. This strategy is the default behavior. */ - ShortestPath = 0, + ShortestPath = 'shortest-path', /** * Rotation via `LongestPath` will use the largest angle * between the starting and ending points. */ - LongestPath = 1, + LongestPath = 'longest-path', /** * Rotation via `Clockwise` will travel in a clockwise direction, * regardless of the starting and ending points. */ - Clockwise = 2, + Clockwise = 'clockwise', /** * Rotation via `CounterClockwise` will travel in a counterclockwise direction, * regardless of the starting and ending points. */ - CounterClockwise = 3 + CounterClockwise = 'counter-clockwise' } diff --git a/src/engine/Actions/Index.ts b/src/engine/Actions/index.ts similarity index 100% rename from src/engine/Actions/Index.ts rename to src/engine/Actions/index.ts diff --git a/src/engine/Collision/BoundingBox.ts b/src/engine/Collision/BoundingBox.ts index 7f99c7b68..e98486b81 100644 --- a/src/engine/Collision/BoundingBox.ts +++ b/src/engine/Collision/BoundingBox.ts @@ -330,7 +330,7 @@ export class BoundingBox { public contains(bb: BoundingBox): boolean; public contains(val: any): boolean { if (val instanceof Vector) { - return this.left <= val.x && this.top <= val.y && this.bottom >= val.y && this.right >= val.x; + return this.left <= val.x && this.top <= val.y && val.y <= this.bottom && val.x <= this.right; } else if (val instanceof BoundingBox) { return this.left <= val.left && this.top <= val.top && val.bottom <= this.bottom && val.right <= this.right; } diff --git a/src/engine/index.ts b/src/engine/index.ts index 6b4fdbe94..72c408a33 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -35,7 +35,7 @@ export * from './Timer'; export * from './Trigger'; export * from './ScreenElement'; -export * from './Actions/Index'; +export * from './Actions/index'; export * from './Collision/Index'; export * from './Interfaces/Index'; diff --git a/src/spec/ActionSpec.ts b/src/spec/ActionSpec.ts index d7a84f797..33caabe3f 100644 --- a/src/spec/ActionSpec.ts +++ b/src/spec/ActionSpec.ts @@ -330,6 +330,17 @@ describe('Action', () => { expect(moveBy.isComplete(actor)).toBeFalse(); }); + it('(with options) can be reset', () => { + const moveBy = new ex.MoveByWithOptions(actor, { offset: ex.vec(100, 0), durationMs: 100 }); + actor.actions.runAction(moveBy); + scene.update(engine, 1000); + expect(moveBy.isComplete(actor)).toBeTrue(); + + moveBy.reset(); + actor.pos = ex.vec(0, 0); + expect(moveBy.isComplete(actor)).toBeFalse(); + }); + it('can be moved to a location by a certain time (x,y) overload', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); @@ -341,6 +352,17 @@ describe('Action', () => { expect(actor.pos.y).toBe(0); }); + it('(with options) can be moved to a location by a certain time (x,y) overload', () => { + expect(actor.pos.x).toBe(0); + expect(actor.pos.y).toBe(0); + + actor.actions.moveBy({ offset: ex.vec(100, 0), durationMs: 2000 }); + + scene.update(engine, 1000); + expect(actor.pos.x).toBe(50); + expect(actor.pos.y).toBe(0); + }); + it('can be moved to a location by a certain time vector overload', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); @@ -374,6 +396,22 @@ describe('Action', () => { expect(actor.pos.x).toBe(10); expect(actor.pos.y).toBe(0); + // Actor should not move after stop + scene.update(engine, 500); + expect(actor.pos.x).toBe(10); + expect(actor.pos.y).toBe(0); + }); + it('(with options) can be stopped', () => { + expect(actor.pos.x).toBe(0); + expect(actor.pos.y).toBe(0); + + actor.actions.moveBy({ offset: ex.vec(20, 0), durationMs: 1000 }); + scene.update(engine, 500); + + actor.actions.clearActions(); + expect(actor.pos.x).toBe(10); + expect(actor.pos.y).toBe(0); + // Actor should not move after stop scene.update(engine, 500); expect(actor.pos.x).toBe(10); @@ -382,6 +420,17 @@ describe('Action', () => { }); describe('moveTo', () => { + it('can be reset', () => { + const moveTo = new ex.MoveToWithOptions(actor, { pos: ex.vec(100, 0), durationMs: 500 }); + actor.actions.runAction(moveTo); + scene.update(engine, 1000); + expect(moveTo.isComplete(actor)).toBeTrue(); + + moveTo.reset(); + actor.pos = ex.vec(0, 0); + expect(moveTo.isComplete(actor)).toBeFalse(); + }); + it('can be moved to a location at a speed (x,y) overload', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); @@ -396,6 +445,20 @@ describe('Action', () => { expect(actor.pos.x).toBe(100); expect(actor.pos.y).toBe(0); }); + it('(with options) can be moved to a location at a speed (x,y) overload', () => { + expect(actor.pos.x).toBe(0); + expect(actor.pos.y).toBe(0); + + actor.actions.moveTo({ pos: ex.vec(100, 0), durationMs: 1000 }); + scene.update(engine, 500); + + expect(actor.pos.x).toBe(50); + expect(actor.pos.y).toBe(0); + + scene.update(engine, 500); + expect(actor.pos.x).toBe(100); + expect(actor.pos.y).toBe(0); + }); it('can be moved to a location at a speed vector overload', () => { expect(actor.pos.x).toBe(0); @@ -443,6 +506,23 @@ describe('Action', () => { expect(actor.pos.x).toBe(5); expect(actor.pos.y).toBe(0); }); + + it('(with options) can be stopped', () => { + expect(actor.pos.x).toBe(0); + expect(actor.pos.y).toBe(0); + + actor.actions.moveTo({ pos: ex.vec(20, 0), durationMs: 2000 }); + scene.update(engine, 500); + + actor.actions.clearActions(); + expect(actor.pos.x).toBe(5); + expect(actor.pos.y).toBe(0); + + // Actor should not move after stop + scene.update(engine, 500); + expect(actor.pos.x).toBe(5); + expect(actor.pos.y).toBe(0); + }); }); describe('easeBy', () => { @@ -804,6 +884,21 @@ describe('Action', () => { expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle at a speed via ShortestPath (default)', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateTo({ angleRadians: Math.PI / 2, durationMs: 1000 }); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 4); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 2); + + scene.update(engine, 500); + expect(actor.angularVelocity).toBe(0); + }); + it('can be rotated to an angle at a speed via LongestPath', () => { expect(actor.rotation).toBe(0); @@ -821,6 +916,23 @@ describe('Action', () => { expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle at a speed via LongestPath', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateTo({ angleRadians: Math.PI / 2, durationMs: 3000, rotationType: ex.RotationType.LongestPath }); + + scene.update(engine, 1000); + //rotation is currently incremented by rx delta ,so will be negative while moving counterclockwise + expect(actor.rotation).toBe(ex.canonicalizeAngle((-1 * Math.PI) / 2)); + + scene.update(engine, 2000); + expect(actor.rotation).toBe(ex.canonicalizeAngle((-3 * Math.PI) / 2)); + + scene.update(engine, 500); + expect(actor.rotation).toBe(ex.canonicalizeAngle(Math.PI / 2)); + expect(actor.angularVelocity).toBe(0); + }); + it('can be rotated to an angle at a speed via Clockwise', () => { expect(actor.rotation).toBe(0); @@ -837,6 +949,22 @@ describe('Action', () => { expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle at a speed via Clockwise', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateTo({ angleRadians: (3 * Math.PI) / 2, durationMs: 3000, rotationType: ex.RotationType.Clockwise }); + + scene.update(engine, 2000); + expect(actor.rotation).toBe(Math.PI); + + scene.update(engine, 1000); + expect(actor.rotation).toBe((3 * Math.PI) / 2); + + scene.update(engine, 500); + expect(actor.rotation).toBe((3 * Math.PI) / 2); + expect(actor.angularVelocity).toBe(0); + }); + it('can be rotated to an angle at a speed via CounterClockwise', () => { expect(actor.rotation).toBe(0); @@ -860,6 +988,29 @@ describe('Action', () => { expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle at a speed via CounterClockwise', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateTo({ angleRadians: Math.PI / 2, durationMs: 3000, rotationType: ex.RotationType.CounterClockwise }); + scene.update(engine, 2000); + expect(actor.rotation).toBe(ex.canonicalizeAngle(-Math.PI)); + + scene.update(engine, 1000); + expect(actor.rotation).toBe(ex.canonicalizeAngle((-3 * Math.PI) / 2)); + + scene.update(engine, 500); + expect(actor.rotation).toBe(ex.canonicalizeAngle(Math.PI / 2)); + expect(actor.angularVelocity).toBe(0); + + // rotating back to 0, starting at PI / 2 + actor.actions.rotateTo(0, Math.PI / 2, ex.RotationType.CounterClockwise); + scene.update(engine, 1000); + expect(actor.rotation).toBe(ex.canonicalizeAngle(0)); + + scene.update(engine, 1); + expect(actor.angularVelocity).toBe(0); + }); + it('can be stopped', () => { expect(actor.rotation).toBe(0); @@ -873,6 +1024,20 @@ describe('Action', () => { scene.update(engine, 500); expect(actor.rotation).toBe(Math.PI / 4); }); + + it('(with options) can be stopped', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateTo({ angleRadians: Math.PI / 2, durationMs: 1000 }); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 4); + + actor.actions.clearActions(); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 4); + }); }); describe('rotateBy', () => { @@ -886,6 +1051,16 @@ describe('Action', () => { actor.rotation = 0; expect(rotateBy.isComplete()).toBeFalse(); }); + it('(with options) can be reset', () => { + const rotateBy = new ex.RotateByWithOptions(actor, { angleRadiansOffset: Math.PI / 2, durationMs: 500 }); + actor.actions.runAction(rotateBy); + scene.update(engine, 1000); + expect(rotateBy.isComplete()).toBeTrue(); + + rotateBy.reset(); + actor.rotation = 0; + expect(rotateBy.isComplete()).toBeFalse(); + }); it('can be rotated to an angle by a certain time via ShortestPath (default)', () => { expect(actor.rotation).toBe(0); @@ -901,6 +1076,21 @@ describe('Action', () => { expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle by a certain time via ShortestPath (default)', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateBy({ angleRadiansOffset: Math.PI / 2, durationMs: 2000 }); + + scene.update(engine, 1000); + expect(actor.rotation).toBe(Math.PI / 4); + + scene.update(engine, 1000); + expect(actor.rotation).toBe(Math.PI / 2); + + scene.update(engine, 500); + expect(actor.angularVelocity).toBe(0); + }); + it('can be rotated to an angle by a certain time via LongestPath', () => { expect(actor.rotation).toBe(0); @@ -916,6 +1106,21 @@ describe('Action', () => { expect(actor.rotation).toBe(Math.PI / 2); expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle by a certain time via LongestPath', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateBy({ angleRadiansOffset: Math.PI / 2, durationMs: 3000, rotationType: ex.RotationType.LongestPath }); + + scene.update(engine, 1000); + expect(actor.rotation).toBe(ex.canonicalizeAngle((-1 * Math.PI) / 2)); + + scene.update(engine, 2000); + expect(actor.rotation).toBe(ex.canonicalizeAngle((-3 * Math.PI) / 2)); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 2); + expect(actor.angularVelocity).toBe(0); + }); it('can be rotated to an angle by a certain time via Clockwise', () => { expect(actor.rotation).toBe(0); @@ -933,6 +1138,22 @@ describe('Action', () => { expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle by a certain time via Clockwise', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateBy({ angleRadiansOffset: Math.PI / 2, durationMs: 1000, rotationType: ex.RotationType.Clockwise }); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 4); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 2); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 2); + expect(actor.angularVelocity).toBe(0); + }); + it('can be rotated to an angle by a certain time via CounterClockwise', () => { expect(actor.rotation).toBe(0); @@ -949,6 +1170,22 @@ describe('Action', () => { expect(actor.angularVelocity).toBe(0); }); + it('(with options) can be rotated to an angle by a certain time via CounterClockwise', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateBy({ angleRadiansOffset: Math.PI / 2, durationMs: 3000, rotationType: ex.RotationType.LongestPath }); + + scene.update(engine, 1000); + expect(actor.rotation).toBe(ex.canonicalizeAngle((-1 * Math.PI) / 2)); + + scene.update(engine, 2000); + expect(actor.rotation).toBe(ex.canonicalizeAngle((-3 * Math.PI) / 2)); + + scene.update(engine, 500); + expect(actor.rotation).toBe(Math.PI / 2); + expect(actor.angularVelocity).toBe(0); + }); + it('can be stopped', () => { expect(actor.rotation).toBe(0); @@ -961,6 +1198,19 @@ describe('Action', () => { scene.update(engine, 1000); expect(actor.rotation).toBe(Math.PI / 4); }); + + it('(with options) can be stopped', () => { + expect(actor.rotation).toBe(0); + + actor.actions.rotateBy({ angleRadiansOffset: Math.PI / 2, durationMs: 2000 }); + + scene.update(engine, 1000); + actor.actions.clearActions(); + expect(actor.rotation).toBe(Math.PI / 4); + + scene.update(engine, 1000); + expect(actor.rotation).toBe(Math.PI / 4); + }); }); describe('scaleTo', () => { @@ -974,6 +1224,16 @@ describe('Action', () => { actor.scale = ex.vec(1, 1); expect(scaleTo.isComplete()).toBeFalse(); }); + it('(with options) can be reset', () => { + const scaleTo = new ex.ScaleToWithOptions(actor, { scale: ex.vec(2, 2), durationMs: 500 }); + actor.actions.runAction(scaleTo); + scene.update(engine, 1000); + expect(scaleTo.isComplete()).toBeTrue(); + + scaleTo.reset(); + actor.scale = ex.vec(1, 1); + expect(scaleTo.isComplete()).toBeFalse(); + }); it('can be scaled at a speed (x,y) overload', () => { expect(actor.scale.x).toBe(1); expect(actor.scale.y).toBe(1); @@ -993,6 +1253,25 @@ describe('Action', () => { expect(actor.scale.y).toBe(2.5); }); + it('(with options) can be scaled at a speed (x,y) overload', () => { + expect(actor.scale.x).toBe(1); + expect(actor.scale.y).toBe(1); + + actor.actions.scaleTo({ scale: ex.vec(2, 4), durationMs: 2000 }); + scene.update(engine, 1000); + + expect(actor.scale.x).toBe(1.5); + expect(actor.scale.y).toBe(2.5); + scene.update(engine, 1000); + + expect(actor.scale.x).toBe(2); + expect(actor.scale.y).toBe(4); + scene.update(engine, 1000); + + expect(actor.scale.x).toBe(2); + expect(actor.scale.y).toBe(4); + }); + it('can be scaled at a speed vector overload', () => { expect(actor.scale.x).toBe(1); expect(actor.scale.y).toBe(1); @@ -1072,6 +1351,16 @@ describe('Action', () => { actor.scale = ex.vec(1, 1); expect(scaleBy.isComplete()).toBeFalse(); }); + it('(with options) can be reset', () => { + const scaleBy = new ex.ScaleByWithOptions(actor, { scaleOffset: ex.vec(1, 1), durationMs: 500 }); + actor.actions.runAction(scaleBy); + scene.update(engine, 1000); + expect(scaleBy.isComplete()).toBeTrue(); + + scaleBy.reset(); + actor.scale = ex.vec(1, 1); + expect(scaleBy.isComplete()).toBeFalse(); + }); it('can be scaled by a certain time (x,y) overload', () => { expect(actor.scale.x).toBe(1); @@ -1088,6 +1377,21 @@ describe('Action', () => { expect(actor.scale.x).toBe(5); expect(actor.scale.y).toBe(5); }); + it('(with options) can be scaled by a certain time (x,y) overload', () => { + expect(actor.scale.x).toBe(1); + expect(actor.scale.y).toBe(1); + + actor.actions.scaleBy({ scaleOffset: ex.vec(4, 4), durationMs: 1000 }); + + scene.update(engine, 500); + expect(actor.scale.x).toBe(3); + expect(actor.scale.y).toBe(3); + + scene.update(engine, 500); + scene.update(engine, 1); + expect(actor.scale.x).toBe(5); + expect(actor.scale.y).toBe(5); + }); it('can be scaled by a certain time vector overload', () => { expect(actor.scale.x).toBe(1); @@ -1137,6 +1441,23 @@ describe('Action', () => { expect(actor.scale.x).toBe(2.5); expect(actor.scale.y).toBe(2.5); }); + + it('(with options) can be stopped', () => { + expect(actor.scale.x).toBe(1); + expect(actor.scale.y).toBe(1); + + actor.actions.scaleBy({ scaleOffset: ex.vec(4, 4), durationMs: 1250 }); + + scene.update(engine, 500); + + actor.actions.clearActions(); + expect(actor.scale.x).toBe(2.6); + expect(actor.scale.y).toBe(2.6); + + scene.update(engine, 500); + expect(actor.scale.x).toBe(2.6); + expect(actor.scale.y).toBe(2.6); + }); }); describe('follow', () => {