2019-03-18 22:20:03 -05:00
|
|
|
import {compose} from '@avocado/core';
|
2019-04-14 20:21:52 -05:00
|
|
|
import {StateProperty, Trait} from '@avocado/entity';
|
2019-03-19 20:58:19 -05:00
|
|
|
import {Rectangle, Vector} from '@avocado/math';
|
2019-04-14 20:33:52 -05:00
|
|
|
|
|
|
|
import {Animation} from '../animation';
|
|
|
|
import {AnimationView} from '../animation-view';
|
2019-03-18 22:20:03 -05:00
|
|
|
|
|
|
|
const decorate = compose(
|
2019-03-23 23:24:18 -05:00
|
|
|
StateProperty('currentAnimation', {
|
2019-03-19 18:06:08 -05:00
|
|
|
track: true,
|
|
|
|
}),
|
2019-04-21 03:43:00 -05:00
|
|
|
StateProperty('isAnimating', {
|
|
|
|
track: true,
|
|
|
|
}),
|
2019-03-18 22:20:03 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
class AnimatedBase extends Trait {
|
|
|
|
|
|
|
|
static defaultParams() {
|
|
|
|
return {
|
2019-03-19 18:06:08 -05:00
|
|
|
animations: {},
|
2019-03-18 22:20:03 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static defaultState() {
|
|
|
|
return {
|
2019-03-19 18:06:08 -05:00
|
|
|
currentAnimation: 'idle',
|
2019-04-21 03:43:00 -05:00
|
|
|
isAnimating: true,
|
2019-03-18 22:20:03 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-03-20 23:23:34 -05:00
|
|
|
initialize() {
|
2019-04-05 11:54:25 -05:00
|
|
|
this._animations = this.params.get('animations').toJS();
|
2019-03-20 23:23:34 -05:00
|
|
|
this.animations = {};
|
|
|
|
this.animationViews = undefined;
|
|
|
|
this.animationsPromise = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
if (this.animationViews) {
|
|
|
|
for (const key in this.animationViews) {
|
|
|
|
this.hideAnimation(key);
|
|
|
|
const animationView = this.animationViews[key];
|
|
|
|
animationView.destroy();
|
|
|
|
}
|
|
|
|
}
|
2019-03-21 00:09:17 -05:00
|
|
|
for (const key in this.animations) {
|
|
|
|
const animation = this.animations[key];
|
|
|
|
animation.destroy();
|
|
|
|
}
|
2019-03-20 23:23:34 -05:00
|
|
|
}
|
|
|
|
|
2019-03-19 20:58:19 -05:00
|
|
|
hideAnimation(key) {
|
|
|
|
if (!this.animationViews) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const animationView = this.animationViews[key];
|
|
|
|
if (!animationView) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.entity.container.removeChild(animationView);
|
|
|
|
}
|
|
|
|
|
2019-04-21 21:54:41 -05:00
|
|
|
jitterFor(key) {
|
|
|
|
if (!this._animations[key] || !this._animations[key].jitter) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return this._animations[key].jitter;
|
|
|
|
}
|
|
|
|
|
2019-03-19 18:06:08 -05:00
|
|
|
loadAnimations() {
|
|
|
|
if (this.animationsPromise) {
|
2019-03-19 10:40:59 -05:00
|
|
|
return;
|
|
|
|
}
|
2019-03-19 18:06:08 -05:00
|
|
|
const animationPromises = [];
|
|
|
|
// Load all animations.
|
2019-04-05 11:54:25 -05:00
|
|
|
for (const key in this._animations) {
|
|
|
|
const {uri} = this._animations[key];
|
2019-03-19 18:06:08 -05:00
|
|
|
const promise = Animation.load(uri).then((animation) => {
|
|
|
|
// Zip with key to make populating animations and views trivial.
|
|
|
|
return {animation, key};
|
|
|
|
});
|
|
|
|
animationPromises.push(promise);
|
2019-03-19 10:46:20 -05:00
|
|
|
}
|
2019-03-19 18:06:08 -05:00
|
|
|
this.animationsPromise = Promise.all(animationPromises);
|
|
|
|
// Store keyed animations.
|
|
|
|
this.animations = {};
|
|
|
|
this.animationsPromise.then((animations) => {
|
|
|
|
animations.forEach(({animation, key}) => {
|
|
|
|
this.animations[key] = animation;
|
2019-03-20 19:23:32 -05:00
|
|
|
// Set direction upfront.
|
|
|
|
animation.direction = this.entity.direction;
|
2019-03-19 18:06:08 -05:00
|
|
|
});
|
2019-03-21 20:07:32 -05:00
|
|
|
// Bounding box update.
|
2019-04-16 13:30:15 -05:00
|
|
|
this.entity.updateVisibleBoundingBox();
|
2019-03-19 10:40:59 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-03-19 18:06:08 -05:00
|
|
|
loadAnimationImagesIfPossible() {
|
|
|
|
if (!this.entity.container) {
|
2019-03-19 11:29:07 -05:00
|
|
|
return;
|
2019-03-19 10:22:35 -05:00
|
|
|
}
|
2019-03-19 18:06:08 -05:00
|
|
|
if (!this.animationsPromise) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.animationViews) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Store keyed animation views.
|
|
|
|
this.animationViews = {};
|
|
|
|
this.animationsPromise.then((animations) => {
|
|
|
|
animations.forEach(({animation, key}) => {
|
|
|
|
this.animationViews[key] = new AnimationView(animation);
|
2019-03-20 21:58:14 -05:00
|
|
|
// Calculate any offset.
|
|
|
|
const animationView = this.animationViews[key];
|
2019-04-19 16:32:17 -05:00
|
|
|
animationView.position = this.offsetFor(key);
|
2019-03-19 18:06:08 -05:00
|
|
|
// Ensure animation is made visible upfront.
|
|
|
|
const isCurrentAnimation = key === this.entity.currentAnimation;
|
|
|
|
if (isCurrentAnimation) {
|
2019-03-19 20:58:19 -05:00
|
|
|
this.showAnimation(key);
|
2019-03-19 18:06:08 -05:00
|
|
|
}
|
|
|
|
});
|
2019-04-19 16:49:41 -05:00
|
|
|
this.setSpriteScale();
|
2019-03-19 18:06:08 -05:00
|
|
|
});
|
2019-03-19 10:22:35 -05:00
|
|
|
}
|
|
|
|
|
2019-03-21 20:07:32 -05:00
|
|
|
offsetFor(key) {
|
2019-04-05 11:54:25 -05:00
|
|
|
if (!this._animations[key] || !this._animations[key].offset) {
|
2019-03-21 20:07:32 -05:00
|
|
|
return [0, 0];
|
|
|
|
}
|
2019-04-05 11:54:25 -05:00
|
|
|
return this._animations[key].offset;
|
2019-03-21 20:07:32 -05:00
|
|
|
}
|
|
|
|
|
2019-04-19 16:49:41 -05:00
|
|
|
setSpriteScale() {
|
|
|
|
if (!this.animationViews) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const rawScale = this.entity.rawVisibleScale;
|
|
|
|
for (const key in this.animationViews) {
|
|
|
|
const animationView = this.animationViews[key];
|
|
|
|
animationView.scale = rawScale;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-19 20:58:19 -05:00
|
|
|
showAnimation(key) {
|
|
|
|
if (!this.animationViews) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const animationView = this.animationViews[key];
|
|
|
|
if (!animationView) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.entity.container.addChild(animationView);
|
|
|
|
}
|
|
|
|
|
2019-03-21 20:07:32 -05:00
|
|
|
hooks() {
|
|
|
|
return {
|
|
|
|
|
2019-04-14 18:42:13 -05:00
|
|
|
visibleBoundingBoxes: () => {
|
2019-03-21 20:07:32 -05:00
|
|
|
const key = this.entity.currentAnimation;
|
|
|
|
const animation = this.animations[key];
|
|
|
|
if (!animation) {
|
|
|
|
return [0, 0, 0, 0];
|
|
|
|
}
|
2019-04-19 16:32:17 -05:00
|
|
|
const viewPosition = this.offsetFor(key);
|
2019-04-11 12:20:24 -05:00
|
|
|
const position = Vector.add(this.entity.position, viewPosition);
|
2019-04-19 16:49:41 -05:00
|
|
|
const size = animation.frameSize;
|
|
|
|
const rectangle = Rectangle.compose(position, size);
|
|
|
|
const expanded = Rectangle.expand(
|
|
|
|
rectangle,
|
|
|
|
Vector.sub(Vector.mul(size, this.entity.rawVisibleScale), size),
|
|
|
|
);
|
|
|
|
return expanded;
|
2019-03-21 20:07:32 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-18 22:20:03 -05:00
|
|
|
listeners() {
|
|
|
|
return {
|
2019-04-19 15:51:57 -05:00
|
|
|
|
2019-03-19 18:06:08 -05:00
|
|
|
currentAnimationChanged: (oldKey) => {
|
|
|
|
// Reset old animation.
|
|
|
|
const oldAnimation = this.animations[oldKey];
|
|
|
|
if (oldAnimation) {
|
|
|
|
oldAnimation.reset();
|
|
|
|
}
|
2019-03-21 20:07:32 -05:00
|
|
|
// Bounding box update.
|
2019-04-16 13:30:15 -05:00
|
|
|
this.entity.updateVisibleBoundingBox();
|
2019-03-19 18:06:08 -05:00
|
|
|
// Only client/graphics.
|
|
|
|
if (!this.animationViews) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Swap the animation.
|
2019-03-19 20:58:19 -05:00
|
|
|
this.hideAnimation(oldKey);
|
|
|
|
this.showAnimation(this.entity.currentAnimation);
|
2019-03-19 18:06:08 -05:00
|
|
|
},
|
2019-04-19 15:51:57 -05:00
|
|
|
|
2019-03-18 22:20:03 -05:00
|
|
|
directionChanged: () => {
|
2019-03-19 18:06:08 -05:00
|
|
|
// All animations track direction.
|
|
|
|
for (const key in this.animations) {
|
|
|
|
const animation = this.animations[key];
|
|
|
|
animation.direction = this.entity.direction;
|
|
|
|
}
|
2019-03-18 22:20:03 -05:00
|
|
|
},
|
2019-04-19 15:51:57 -05:00
|
|
|
|
|
|
|
dying: () => {
|
2019-04-21 03:43:00 -05:00
|
|
|
this.isAnimating = false;
|
2019-04-19 15:51:57 -05:00
|
|
|
},
|
|
|
|
|
2019-04-19 17:09:48 -05:00
|
|
|
visibleScaleChanged: () => {
|
2019-04-19 16:49:41 -05:00
|
|
|
this.setSpriteScale();
|
|
|
|
},
|
|
|
|
|
2019-03-21 00:15:15 -05:00
|
|
|
traitAdded: (type) => {
|
2019-03-19 18:06:08 -05:00
|
|
|
if (-1 === [
|
|
|
|
'animated',
|
2019-04-14 18:42:13 -05:00
|
|
|
'visible',
|
2019-03-21 00:15:15 -05:00
|
|
|
].indexOf(type)) {
|
2019-03-18 22:20:03 -05:00
|
|
|
return;
|
|
|
|
}
|
2019-03-19 18:06:08 -05:00
|
|
|
this.loadAnimations();
|
|
|
|
this.loadAnimationImagesIfPossible();
|
2019-03-18 22:20:03 -05:00
|
|
|
},
|
2019-04-19 16:49:41 -05:00
|
|
|
|
2019-03-18 22:20:03 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-04-12 13:05:30 -05:00
|
|
|
tick(elapsed) {
|
2019-04-21 03:43:00 -05:00
|
|
|
if (!this.isAnimating) {
|
2019-04-19 15:51:57 -05:00
|
|
|
return;
|
|
|
|
}
|
2019-04-12 13:05:30 -05:00
|
|
|
// Only tick current animation.
|
2019-04-21 21:54:41 -05:00
|
|
|
const currentAnimation = this.entity.currentAnimation;
|
|
|
|
const animation = this.animations[currentAnimation];
|
2019-04-12 13:05:30 -05:00
|
|
|
if (!animation) {
|
|
|
|
return;
|
|
|
|
}
|
2019-04-21 21:54:41 -05:00
|
|
|
const jitter = Math.random() * this.jitterFor(currentAnimation);
|
|
|
|
const halfJitter = jitter / 2;
|
|
|
|
animation.tick(elapsed + (jitter - halfJitter));
|
2019-04-12 13:05:30 -05:00
|
|
|
}
|
|
|
|
|
2019-03-18 22:20:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export class Animated extends decorate(AnimatedBase) {}
|