avocado-old/packages/timing/traits/animated.trait.js
2020-06-21 23:06:05 -05:00

305 lines
7.6 KiB
JavaScript

import {compose} from '@avocado/core';
import {StateProperty, Trait} from '@avocado/entity';
import {Rectangle, Vector} from '@avocado/math';
import {Animation} from '../animation';
import {AnimationView} from '../animation-view';
import TraitUpdateAnimatedPacket from '../packets/trait-update-animated.packet';
const decorate = compose(
StateProperty('currentAnimation', {
track: true,
}),
StateProperty('isAnimating', {
track: true,
}),
);
export default class Animated extends decorate(Trait) {
static defaultParams() {
return {
animations: {},
};
}
static defaultState() {
return {
currentAnimation: 'idle',
isAnimating: true,
};
}
static describeParams() {
return {
animations: {
type: 'object',
label: 'Animations',
},
};
}
static describeState() {
return {
isAnimating: {
type: 'bool',
label: 'Is animating',
},
currentAnimation: {
type: 'string',
label: 'Current animation',
},
}
}
static type() {
return 'animated';
}
constructor(entity, params, state) {
super(entity, params, state);
this._animations = this.params.animations;
this.animations = {};
this.animationViews = undefined;
this.animationsPromise = undefined;
this._cachedAabbs = {};
this._currentAnimation = this.state.currentAnimation;
}
destroy() {
if (this.animationViews) {
for (const key in this.animationViews) {
this.hideAnimation(key);
const animationView = this.animationViews[key];
animationView.destroy();
}
this.animationViews = undefined;
}
for (const key in this.animations) {
const animation = this.animations[key];
animation.destroy();
}
this.animations = {};
this.animationsPromise = undefined;
this._cachedAabbs = {};
}
acceptPacket(packet) {
if (packet instanceof TraitUpdateAnimatedPacket) {
this.entity.currentAnimation = packet.data.currentAnimation;
this.entity.isAnimating = packet.data.isAnimating;
}
}
hideAnimation(key) {
if (!this.animationViews) {
return;
}
const animationView = this.animationViews[key];
if (!animationView) {
return;
}
this.entity.container.removeChild(animationView);
}
hydrate() {
return Promise.all([
this.loadAnimations(),
this.loadAnimationImagesIfPossible(),
]);
}
jitterFor(key) {
if (!(key in this._animations) || !this._animations[key].jitter) {
return 0;
}
return this._animations[key].jitter;
}
loadAnimations() {
if (this.animationsPromise) {
return;
}
if (!this.animationsPromise) {
const animationPromises = [];
// Load all animations.
for (const key in this._animations) {
const {uri} = this._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 = {};
return this.animationsPromise.then((animations) => {
animations.forEach(({animation, key}) => {
this.animations[key] = animation;
// Set direction upfront.
animation.direction = this.entity.direction;
});
// Bounding box update.
this.entity.updateVisibleBoundingBox();
});
}
loadAnimationImagesIfPossible() {
if (!this.entity.container) {
return;
}
if (!this.animationsPromise) {
return;
}
if (this.animationViews) {
return;
}
// Store keyed animation views.
this.animationViews = {};
return 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.offsetFor(key);
// Ensure animation is made visible upfront.
const isCurrentAnimation = key === this._currentAnimation;
if (isCurrentAnimation) {
this.showAnimation(key);
}
});
this.setSpriteScale();
});
}
offsetFor(key) {
if (!(key in this._animations) || !this._animations[key].offset) {
return [0, 0];
}
return this._animations[key].offset;
}
packets(informed) {
const {currentAnimation, isAnimating} = this.stateDifferences();
if (currentAnimation || isAnimating) {
return new TraitUpdateAnimatedPacket({
currentAnimation: this.state.currentAnimation,
isAnimating: this.state.isAnimating,
});
}
}
setSpriteScale() {
if (!this.animationViews) {
return;
}
for (const key in this.animationViews) {
const animationView = this.animationViews[key];
animationView.scale = this.entity.rawVisibleScale;
}
}
showAnimation(key) {
if (!this.animationViews) {
return;
}
if (!(key in this.animationViews)) {
return;
}
const animationView = this.animationViews[key];
this.entity.container.addChild(animationView);
}
hooks() {
return {
visibleAabbs: () => {
const key = this._currentAnimation;
if (key in this._cachedAabbs) {
return this._cachedAabbs[key];
}
if (!(key in this.animations)) {
return [0, 0, 0, 0];
}
const animation = this.animations[key];
const size = animation.frameSize;
const scaledSize = Vector.mul(size, this.entity.rawVisibleScale);
const viewPosition = Vector.sub(
this.offsetFor(key),
Vector.scale(scaledSize, 0.5),
);
const rectangle = Rectangle.compose(viewPosition, size);
const expanded = Rectangle.expand(
rectangle,
Vector.sub(scaledSize, size),
);
return this._cachedAabbs[key] = expanded;
},
}
}
listeners() {
return {
currentAnimationChanged: (oldKey, currentAnimation) => {
this._currentAnimation = currentAnimation;
// Reset old animation.
if (oldKey in this.animations) {
const oldAnimation = this.animations[oldKey];
oldAnimation.reset();
}
// Bounding box update.
this.entity.updateVisibleBoundingBox();
// Only client/graphics.
if (!this.animationViews) {
return;
}
// Swap the animation.
this.hideAnimation(oldKey);
this.showAnimation(this._currentAnimation);
},
directionChanged: () => {
// All animations track direction.
for (const key in this.animations) {
const animation = this.animations[key];
animation.direction = this.entity.direction;
}
},
isDyingChanged: (_, isDying) => {
this.isAnimating = !isDying;
},
visibleScaleChanged: () => {
this._cachedAabbs = {};
this.setSpriteScale();
},
};
}
tick(elapsed) {
if (!this.isAnimating) {
return;
}
// Only tick current animation.
const currentAnimation = this._currentAnimation;
if (!(currentAnimation in this.animations)) {
return;
}
const animation = this.animations[currentAnimation];
const jitterAmount = this.jitterFor(currentAnimation);
if (jitterAmount > 0) {
const jitter = Math.random() * jitterAmount;
const halfJitter = jitter / 2;
elapsed += jitter - halfJitter;
}
animation.tick(elapsed);
}
}