components/Timeline.js

/* global platypus */
import {arrayCache, greenSplice} from '../utils/array.js';
import Data from '../Data.js';
import createComponentClass from '../factory.js';

export default (function () {
    var pause = function () {
            this.active--;
        },
        play = function () {
            this.active++;
        },
        timelineTrigger = function (timelineId) {
            this.timelineInstances.push(this.createTimeStampedTimeline(this.timelines[timelineId]));
        },
        updateLogic = function (tick) {
            var delta = tick.delta,
                instance = null,
                instances = this.timelineInstances,
                i = instances.length;
            
            while (i--) {
                instance = instances[i];
                if (instance.remove) {
                    greenSplice(instances, i);
                    arrayCache.recycle(instance.timeline);
                    instance.recycle();
                } else if (instance.active) {
                    if (instance.timeline.length === 0) {
                        greenSplice(instances, i);
                        arrayCache.recycle(instance.timeline);
                        instance.recycle();
                    } else {
                        this.progressTimeline(instance, delta);
                    }
                }
            }
        };
    
    return createComponentClass(/** @lends platypus.components.Timeline.prototype */{
        
        id: 'Timeline',
        
        properties: {
            /**
             * Defines the set of timelines. Triggering the key for one of the events will run the timeline. A timeline can contain three different types integers >= 0, strings, and objects. Integers are interpreted as waits and define
             * pauses between events. Strings are intepreted as event calls. Objects can contain several parameters: entity, event, message. The entity is the id of the entity that
             * the event will be fired on. The event can be a string or array. If a string, it will call that event on the entity or owner. If an array, the value will be passed
             * to the event handling system.
             *
             *  "timelines": {
             *      "sample-timeline-1": [
             *          500,
             *          "sample-event",
             *          {"event": "sample-event", "message": "sample-message"},
             *          {"entity": "entity-id-to-trigger-event-on", "event": "sample-event"},
             *          {"event": ["sample-event", "sample-event-2", {"event": "sample-event-3", "message": "sample-message"}]},
             *      ],
             *      "sample-timeline-2": [
             *          200,
             *          "sample-event"
             *      ]
             * }
             *
             * @property timelines
             * @type Object
             * @default {}
             */
            "timelines": {}
        },
        
        /**
         * Timeline enables the scheduling of events based on a linear timeline
         *
         * @memberof platypus.components
         * @uses platypus.Component
         * @constructs
         * @listens platypus.Entity#handle-logic
         * @listens platypus.Entity#stop-active-timelines
         */
        initialize: function () {
            var x = 0;
            
            this.timelineInstances = arrayCache.setUp();
            for (x in this.timelines) {
                if (this.timelines.hasOwnProperty(x)) {
                    this.addEventListener(x, timelineTrigger.bind(this, x));
                }
            }
        },

        events: {
            "handle-logic": updateLogic,

            /**
             * Stops all timelines.
             *
             * @event platypus.Entity#stop-active-timelines
             */
            "stop-active-timelines": function () {
                var instances = this.timelineInstances,
                    i = instances.length;

                while (i--) {
                    instances[i].remove = true;
                }
            }
        },
        
        methods: {
            createTimeStampedTimeline: function (timeline) {
                var timeStampedTimeline = arrayCache.setUp(),
                    x = 0,
                    timeOffset = 0,
                    entry = null;
                
                for (x = 0; x < timeline.length; x++) {
                    entry = timeline[x];
                    if (typeof entry === 'number') {
                        timeOffset += entry;
                    } else {
                        timeStampedTimeline.push(Data.setUp(
                            "time", timeOffset,
                            "value", entry
                        ));
                    }
                }
                timeStampedTimeline.reverse();
                return Data.setUp(
                    "timeline", timeStampedTimeline,
                    "time", 0,
                    "active", 1,
                    "pause", pause,
                    "play", play,
                    "remove", false
                );
            },
            progressTimeline: function (instance, delta) {
                var timeline = instance.timeline,
                    i = timeline.length,
                    entry = null,
                    value = null,
                    triggerOn = this.owner;
                
                instance.time += delta;
                
                //Go through the timeline playing events if the time has progressed far enough to trigger them.
                while (i--) {
                    entry = timeline[i];
                    if (entry.time <= instance.time) {
                        value = entry.value;
                        if (typeof value === 'string') {
                            this.owner.triggerEvent(value);
                        } else {
                            if (value.entity) {
                                if (this.owner.getEntityById) {
                                    triggerOn = this.owner.getEntityById(value.entity);
                                } else {
                                    triggerOn = this.owner.parent.getEntityById(value.entity);
                                }
                                
                                if (!triggerOn) {
                                    platypus.debug.warn('No entity of that id');
                                    triggerOn = this.owner;
                                }
                            }
                            
                            if (value.message) {
                                triggerOn.triggerEvent(value.event, value.message);
                            } else {
                                triggerOn.trigger(value.event);
                            }
                        }
                        
                        entry.recycle();
                        timeline.pop(); //Remove the entry.
                        if (!instance.active) {
                            return; //We bail until the callback.
                        }
                    } else {
                        return;
                    }
                    
                    entry = null;
                    value = null;
                    triggerOn = this.owner;
                }
            },
            destroy: function () {
                var instance = null,
                    instances = this.timelineInstances,
                    i = instances.length;
                
                while (i--) {
                    instance = instances[i];
                    arrayCache.recycle(instance.timeline);
                    instance.recycle();
                }
                arrayCache.recycle(instances);
                this.timelineInstances = null;
            }
        }
    });
}());