import StateMap from '../StateMap.js';
import {arrayCache} from '../utils/array.js';
import createComponentClass from '../factory.js';
export default (function () {
var createTest = function (testStates, animation) {
if (testStates === 'default') {
return defaultTest.bind(null, animation);
} else {
//TODO: Better clean-up: Create a lot of these without removing them later... DDD 2/5/2016
return stateTest.bind(null, animation, StateMap.setUp(testStates));
}
},
defaultTest = function (animation) {
return animation;
},
methodPlay = function (animation, loop, restart) {
this.component.playAnimation(animation, loop, restart);
},
methodStop = function (animation) {
this.component.stopAnimation(animation);
},
stateTest = function (animation, states, ownerState) {
if (ownerState.includes(states)) {
return animation;
}
return false;
},
triggerPlay = function (animation, loop, restart) {
/**
* On entering a new animation-mapped state, this component triggers this event to play an animation.
*
* @event platypus.Entity#play-animation
* @param animation {String} Describes the animation to play.
* @param loop {Boolean} Whether to loop a playing animation.
* @param restart {Boolean} Whether to restart a playing animation.
*/
this.owner.triggerEvent('play-animation', animation, loop, restart);
},
triggerStop = function (animation) {
/**
* On attaining an animation-mapped state, this component triggers this event to stop a previous animation.
*
* @event platypus.Entity#stop-animation
* @param animation {String} Describes the animation to stop.
*/
this.owner.triggerEvent('stop-animation', animation);
};
return createComponentClass(/** @lends platypus.components.RenderAnimator.prototype */{
id: 'RenderAnimator',
properties: {
/**
* An object containg key-value pairs that define a mapping from entity states to the animation that should play. The list is processed from top to bottom, so the most important actions should be listed first (for example, a jumping animation might take precedence over an idle animation). If not specified, an 1-to-1 animation map is created from the list of animations in the sprite sheet definition using the animation names as the keys.
*
* "animationStates":{
* "standing": "default-animation" // On receiving a "standing" event, or when this.owner.state.standing === true, the "default" animation will begin playing.
* "ground,moving": "walking", // Comma separated values have a special meaning when evaluating "state-changed" messages. The above example will cause the "walking" animation to play ONLY if the entity's state includes both "moving" and "ground" equal to true.
* "ground,striking": "swing!", // Putting an exclamation after an animation name causes this animation to complete before going to the next animation. This is useful for animations that would look poorly if interrupted.
* "default": "default-animation" // Optional. "default" is a special property that matches all states. If none of the above states are valid for the entity, it will use the default animation listed here.
* }
*
* @property animationStates
* @type Object
* @default null
*/
animationStates: null,
/**
* An object containg key-value pairs that define a mapping from triggered events to the animation that should play.
*
* "animationEvents":{
* "move": "walk-animation",
* "jump": "jumping-animation"
* }
*
* The above will create two event listeners on the entity, "move" and "jump", that will play their corresponding animations when the events are triggered.
*
* @property animationEvents
* @type Object
* @default null
*/
animationEvents: null,
/**
* Sets a component that this component should be connected to.
*
* @property component
* @type Component
* @default null
*/
component: null,
/**
* Optional. Forces animations to complete before starting a new animation. Defaults to `false`.
*
* @property forcePlayThrough
* @type Boolean
* @default false
*/
forcePlayThrough: false,
/**
* Whether to restart a playing animation on event.
*
* @property restart
* @type Boolean
* @default true
*/
restart: true,
/**
* Whether to loop a playing animation on event.
*
* @property loop
* @type Boolean
* @default false
*/
loop: false
},
/**
* This component is typically added to an entity automatically by a render component. It handles mapping entity states and events to playable animations.
*
* @memberof platypus.components
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#animation-ended
* @listens platypus.Entity#state-changed
* @listens platypus.Entity#update-animation
* @fires platypus.Entity#play-animation
* @fires platypus.Entity#stop-animation
*/
initialize: (function () {
const
trigger = function (animation, loop, restart) {
this.override = animation;
this.owner.triggerEvent('play-animation', animation, loop, restart);
},
method = function (animation, loop, restart) {
this.override = animation;
this.playAnimation(animation, loop, restart);
};
return function () {
const
events = this.animationEvents,
states = this.animationStates;
//Handle Events:
this.override = false;
if (events) {
for (const animation in events) {
if (events.hasOwnProperty(animation)) {
if (this.component) {
this.addEventListener(animation, method.bind(this.component, events[animation], this.loop, this.restart));
} else {
this.addEventListener(animation, trigger.bind(this, events[animation], this.loop, this.restart));
}
}
}
}
//Handle States:
this.followThroughs = {};
this.checkStates = arrayCache.setUp();
this.state = this.owner.state;
this.stateChange = true; //Check state against entity's prior state to update animation if necessary on instantiation.
this.lastState = -1;
if (states) {
for (const anim in states) {
if (states.hasOwnProperty(anim)) {
const animation = states[anim];
//TODO: Should probably find a cleaner way to accomplish this. Maybe in the animationMap definition? - DDD
if (animation[animation.length - 1] === '!') {
animation = animation.substring(0, animation.length - 1);
this.followThroughs[animation] = true;
} else {
this.followThroughs[animation] = false;
}
this.checkStates.push(createTest(anim, animation));
}
}
}
this.waitingAnimation = false;
this.waitingState = 0;
this.playWaiting = false;
this.animationFinished = false;
if (this.component) {
this.playAnimation = methodPlay;
this.stopAnimation = methodStop;
} else {
this.playAnimation = triggerPlay;
this.stopAnimation = triggerStop;
}
};
} ()),
events: {
"state-changed": function () {
this.stateChange = true;
},
"animation-ended": function (animation) {
if (animation === this.currentAnimation) {
if (this.override && (animation === this.override)) {
this.stateChange = true;
this.override = false;
}
if (this.waitingAnimation) {
this.currentAnimation = this.waitingAnimation;
this.waitingAnimation = false;
this.lastState = this.waitingState;
this.animationFinished = false;
this.playAnimation(this.currentAnimation);
} else {
this.animationFinished = true;
}
}
},
"update-animation": function (playing) {
var i = 0,
testCase = false;
if (this.stateChange && !this.override) {
if (this.state.has('visible')) {
this.visible = this.state.get('visible');
}
for (i = 0; i < this.checkStates.length; i++) {
testCase = this.checkStates[i](this.state);
if (testCase) {
if (this.currentAnimation !== testCase) {
if (!this.followThroughs[this.currentAnimation] && (!this.forcePlaythrough || (this.animationFinished || (this.lastState >= +i)))) {
this.currentAnimation = testCase;
this.lastState = +i;
this.animationFinished = false;
if (playing) {
this.playAnimation(this.currentAnimation);
} else {
this.stopAnimation(this.currentAnimation);
}
} else {
this.waitingAnimation = testCase;
this.waitingState = +i;
}
} else if (this.waitingAnimation && !this.followThroughs[this.currentAnimation]) {// keep animating this animation since this animation has already overlapped the waiting animation.
this.waitingAnimation = false;
}
break;
}
}
this.stateChange = false;
}
}
},
methods: {
toJSON: function () { // This component is added by another component, so it shouldn't be returned for reconstruction.
return null;
},
destroy: function () {
arrayCache.recycle(this.checkStates);
this.checkStates = null;
this.followThroughs = null;
this.state = null;
}
}
});
}());