import {compose} from '@avocado/core'; import {AnimationView} from '@avocado/graphics'; import {Rectangle, Vector} from '@avocado/math'; import {Animation} from '@avocado/timing'; import {StateProperty, Trait} from '../trait'; const decorate = compose( StateProperty('currentAnimation', { track: true, }), StateProperty('currentFrame', { track: true, }), ); class AnimatedBase extends Trait { static defaultParams() { return { animations: {}, }; } static defaultState() { return { currentAnimation: 'idle', currentFrame: 0, }; } initialize() { this.animations = {}; this.animationListeners = {}; 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(); } } for (const key in this.animations) { const animation = this.animations[key]; animation.off('indexChanged', this.animationListeners[key]); animation.destroy(); } } hideAnimation(key) { if (!this.animationViews) { return; } const animationView = this.animationViews[key]; if (!animationView) { return; } this.entity.container.removeChild(animationView); } loadAnimations() { if (this.animationsPromise) { return; } const animations = this.params.get('animations'); const animationPromises = []; // Load all animations. for (const key in animations) { const {uri} = animations[key]; const promise = Animation.load(uri).then((animation) => { // Zip with key to make populating animations and views trivial. return {animation, key}; }); animationPromises.push(promise); } this.animationsPromise = Promise.all(animationPromises); // Store keyed animations. this.animations = {}; this.animationsPromise.then((animations) => { animations.forEach(({animation, key}) => { this.animations[key] = animation; // Set direction upfront. animation.direction = this.entity.direction; // Listen for index changes. this.animationListeners[key] = () => { if (this.entity.currentAnimation === key) { this.entity.currentFrame = animation.index; } }; animation.on('indexChanged', this.animationListeners[key]); }); // Bounding box update. this.entity.emit('boundingBoxesUpdated'); }); } loadAnimationImagesIfPossible() { if (!this.entity.container) { return; } 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); // Calculate any offset. const animationView = this.animationViews[key]; animationView.position = this.viewPositionFor(key); // Ensure animation is made visible upfront. const isCurrentAnimation = key === this.entity.currentAnimation; if (isCurrentAnimation) { this.showAnimation(key); } }); }); } offsetFor(key) { const animations = this.params.get('animations'); if (!animations[key] || !animations[key].offset) { return [0, 0]; } return animations[key].offset; } showAnimation(key) { if (!this.animationViews) { return; } const animationView = this.animationViews[key]; if (!animationView) { return; } this.entity.container.addChild(animationView); } viewPositionFor(key) { const animation = this.animations[key]; if (!animation) { return [0, 0]; } const size = animation.frameSize; const halfway = Vector.scale(size, -0.5); const offset = this.offsetFor(key); return Vector.add(halfway, offset); } hooks() { return { boundingBoxes: () => { const key = this.entity.currentAnimation; const animation = this.animations[key]; if (!animation) { return [0, 0, 0, 0]; } const viewPosition = this.viewPositionFor(key); const position = Vector.add(this.entity.position, viewPosition); return Rectangle.compose(position, animation.frameSize); }, } } listeners() { return { currentAnimationChanged: (oldKey) => { // Reset old animation. const oldAnimation = this.animations[oldKey]; if (oldAnimation) { oldAnimation.reset(); } // Bounding box update. this.entity.emit('boundingBoxesUpdated'); // Only client/graphics. if (!this.animationViews) { return; } // Swap the animation. this.hideAnimation(oldKey); this.showAnimation(this.entity.currentAnimation); }, currentFrameChanged: () => { // Animation index from current entity frame. const animation = this.animations[this.entity.currentAnimation]; if (!animation) { return; } animation.index = this.entity.currentFrame; }, directionChanged: () => { // All animations track direction. for (const key in this.animations) { const animation = this.animations[key]; animation.direction = this.entity.direction; } }, tick: (elapsed) => { // Only tick current animation. const animation = this.animations[this.entity.currentAnimation]; if (!animation) { return; } animation.tick(elapsed); }, traitAdded: (type) => { if (-1 === [ 'animated', 'graphical', ].indexOf(type)) { return; } this.loadAnimations(); this.loadAnimationImagesIfPossible(); }, }; } } export class Animated extends decorate(AnimatedBase) {}