File:StateManager.js

/**
 * @module States
 * @namespace springroll
 * @requires Core
 */
(function(undefined)
{

	// Imports
	var Debug = include('springroll.Debug', false),
		EventDispatcher = include('springroll.EventDispatcher'),
		State = include('springroll.State'),
		StateEvent = include('springroll.StateEvent');

	/**
	 * The State Manager used for managing the different states of a game or site
	 *
	 * @class StateManager
	 * @extends springroll.EventDispatcher
	 * @constructor
	 * @param {Object} [transitionSounds] Data object with aliases and start times (seconds) for
	 *     transition in, loop and out sounds. Example: `{in:{alias:"myAlias", start:0.2}}`.
	 *     These objects are in the format for Animator from EaselJSDisplay or PixiDisplay,
	 *     so they can be just the sound alias instead of an object.
	 * @param {Object|String} [transitionSounds.in] The sound to play for transition in
	 * @param {Object|String} [transitionSounds.out] The sound to play for transition out
	 * @param {Object|String} [transitionSounds.loading] The sound to play for loading
	 */
	var StateManager = function(transitionSounds)
	{
		EventDispatcher.call(this);

		/**
		 * The animator playback.
		 *
		 * @property {springroll.Animator} animator
		 * @private
		 */
		this.animator = null;

		/**
		 * The click to play in between transitioning states
		 *
		 * @property {createjs.MovieClip|springroll.easeljs.BitmapMovieClip|PIXI.Spine} transition
		 */
		this.transition = null;

		/**
		 * Wait to fire the onTransitionIn event until the onTransitionLoading
		 * loop reaches it’s final frame.
		 * @property {boolean} waitForLoadingComplete
		 */
		this.waitForLoadingComplete = false;

		/**
		 * The sounds for the transition
		 *
		 * @property {Object} _transitionSounds
		 * @private
		 */
		this._transitionSounds = transitionSounds || null;

		/**
		 * The collection of states map
		 *
		 * @property {Object} _states
		 * @private
		 */
		this._states = {};

		/**
		 * The currently selected state
		 *
		 * @property {springroll.State} _state
		 * @private
		 */
		this._state = null;

		/**
		 * The currently selected state id
		 *
		 * @property {String} _stateID
		 * @private
		 */
		this._stateId = null;

		/**
		 * The old state
		 *
		 * @property {springroll.State} _oldState
		 * @private
		 */
		this._oldState = null;

		/**
		 * If the manager is loading a state
		 *
		 * @property {Boolean} name description
		 * @private
		 */
		this._isLoading = false;

		/**
		 * If the state or manager is current transitioning
		 *
		 * @property {Boolean} _isTransitioning
		 * @private
		 */
		this._isTransitioning = false;

		/**
		 * If the current object is destroyed
		 *
		 * @property {Boolean} _destroyed
		 * @private
		 */
		this._destroyed = false;

		// Hide the blocker
		this.enabled = true;

		// Binding
		this._onTransitionLoading = this._onTransitionLoading.bind(this);
		this._onTransitionOut = this._onTransitionOut.bind(this);
		this._onStateLoaded = this._onStateLoaded.bind(this);
		this._onTransitionIn = this._onTransitionIn.bind(this);
	};

	var p = EventDispatcher.extend(StateManager);

	/**
	 * The amount of progress while state is being preloaded from zero to 1
	 * @event progress
	 * @param {Number} percentage The amount loaded
	 */

	/**
	 * The name of the Animator label and event for transitioning into a state.
	 *
	 * @event onTransitionIn
	 */
	var TRANSITION_IN = StateManager.TRANSITION_IN = "onTransitionIn";

	/**
	 * The name of the Animator label and event for loading between state change.
	 * this event is only dispatched if there is a loading sequence to show in the
	 * transition. Recommended to use 'loadingStart' instead for checking.
	 *
	 * @event onTransitionLoading
	 */
	var TRANSITION_LOADING = StateManager.TRANSITION_LOADING = "onTransitionLoading";

	/**
	 * The name of the event for completing transitioning into a state.
	 *
	 * @event onTransitionInDone
	 */
	var TRANSITION_IN_DONE = StateManager.TRANSITION_IN_DONE = "onTransitionInDone";

	/**
	 * The name of the Animator label and event for transitioning out of a state.
	 *
	 * @event onTransitionOut
	 */
	var TRANSITION_OUT = StateManager.TRANSITION_OUT = "onTransitionOut";

	/**
	 * The name of the event for completing transitioning out of a state.
	 *
	 * @event onTransitionOutDone
	 */
	var TRANSITION_OUT_DONE = StateManager.TRANSITION_OUT_DONE = "onTransitionOutDone";

	/**
	 * The name of the event for initialization complete - the first state is then being entered.
	 *
	 * @event onInitDone
	 */
	var TRANSITION_INIT_DONE = StateManager.TRANSITION_INIT_DONE = "onInitDone";

	/**
	 * Event when the state begins loading assets when it is entered.
	 *
	 * @event onLoadingStart
	 */
	var LOADING_START = StateManager.LOADING_START = "onLoadingStart";

	/**
	 * Event when the state finishes loading assets when it is entered.
	 *
	 * @event onLoadingDone
	 */
	var LOADING_DONE = StateManager.LOADING_DONE = "onLoadingDone";

	/**
	 * Register a state with the state manager, done initially
	 *
	 * @method addState
	 * @param {String} id The string alias for a state
	 * @param {springroll.State} state State object reference
	 */
	p.addState = function(id, state)
	{
		if (DEBUG && Debug)
		{
			Debug.assert(state instanceof State, "State (" + id + ") needs to subclass springroll.State");
		}

		// Add to the collection of states
		this._states[id] = state;

		// Give the state a reference to the id
		state.stateId = id;

		// Give the state a reference to the manager
		state.manager = this;
	};

	/**
	 * Get the current selected state (state object)
	 * @property {springroll.State} currentState
	 * @readOnly
	 */
	Object.defineProperty(p, 'currentState',
	{
		get: function()
		{
			return this._state;
		}
	});

	/**
	 * Access a certain state by the ID
	 *
	 * @method getStateById
	 * @param {String} id State alias
	 * @return {springroll.State} The base State object
	 */
	p.getStateById = function(id)
	{
		if (DEBUG && Debug) Debug.assert(this._states[id] !== undefined, "No alias matching " + id);
		return this._states[id];
	};

	/**
	 * If the StateManager is busy because it is currently loading or transitioning.
	 *
	 * @method isBusy
	 * @return {Boolean} If StateManager is busy
	 */
	p.isBusy = function()
	{
		return this._isLoading || this._isTransitioning;
	};

	/**
	 * If the state needs to do some asyncronous tasks,
	 * The state can tell the manager to stop the animation
	 *
	 * @method loadingStart
	 */
	p.loadingStart = function()
	{
		if (this._destroyed) return;

		this.trigger(LOADING_START);

		this._onTransitionLoading();
	};

	/**
	 * If the state has finished it's asyncronous task loading
	 * Lets enter the state
	 *
	 * @method loadingDone
	 */
	p.loadingDone = function()
	{
		if (this._destroyed) return;

		this.trigger(LOADING_DONE);
	};

	/**
	 * Internal setter for the enabled status
	 * @private
	 * @property {Boolean} enabled
	 */
	Object.defineProperty(p, 'enabled',
	{
		set: function(enabled)
		{
			/**
			 * If the state manager is enabled, used internally
			 * @event enabled
			 * @param {Boolean} enabled
			 */
			this.trigger('enabled', enabled);
		}
	});

	/**
	 * This transitions out of the current state and
	 * enters it again. Can be useful for clearing a state
	 *
	 * @method refresh
	 */
	p.refresh = function()
	{
		if (DEBUG && Debug) Debug.assert(!!this._state, "No current state to refresh!");
		this.state = this._stateId;
	};

	/**
	 * Get or change the current state, using the state id.
	 * @property {String} state
	 */
	Object.defineProperty(p, "state",
	{
		set: function(id)
		{
			if (DEBUG && Debug) Debug.assert(this._states[id] !== undefined, "No current state mattching id '" + id + "'");

			// If we try to transition while the transition or state
			// is transition, then we queue the state and proceed
			// after an animation has played out, to avoid abrupt changes
			if (this._isTransitioning)
			{
				return;
			}

			this._stateId = id;
			this.enabled = false;
			this._oldState = this._state;
			this._state = this._states[id];

			if (!this._oldState)
			{
				// There is not current state
				// this is only possible if this is the first
				// state we're loading
				this._isTransitioning = true;
				if (this.transition)
					this.transition.visible = true;
				this._onTransitionLoading();
				this.trigger(TRANSITION_INIT_DONE);
				this._isLoading = true;
				this._state._internalEnter(this._onStateLoaded);
			}
			else
			{
				// Check to see if the state is currently in a load
				// if so cancel the state
				if (this._isLoading)
				{
					this._oldState._internalCancel();
					this._isLoading = false;
					this._state._internalEnter(this._onStateLoaded);
				}
				else
				{
					this._isTransitioning = true;
					this._oldState._internalExitStart();
					this.enabled = false;

					this.trigger(TRANSITION_OUT);

					this._transitioning(TRANSITION_OUT, this._onTransitionOut);
				}
			}
		},
		get: function()
		{
			return this._stateId;
		}
	});

	/**
	 * When the transition out of a state has finished playing during a state change.
	 * @method _onTransitionOut
	 * @private
	 */
	p._onTransitionOut = function()
	{
		this.trigger(TRANSITION_OUT_DONE);

		this._isTransitioning = false;

		if (this.has(StateEvent.HIDDEN))
		{
			this.trigger(
				StateEvent.HIDDEN,
				new StateEvent(StateEvent.HIDDEN, this._state, this._oldState));
		}
		this._oldState.panel.visible = false;
		this._oldState._internalExit();
		this._oldState = null;

		this._onTransitionLoading(); //play the transition loop animation

		this._isLoading = true;
		this._state._internalEnter(this._onStateLoaded);
	};

	/**
	 * When the state has completed its loading sequence.
	 * This should be treated as an asynchronous process.
	 *
	 * @method _onStateLoaded
	 * @private
	 */
	p._onStateLoaded = function()
	{
		this._isLoading = false;
		this._isTransitioning = true;

		if (this.has(StateEvent.VISIBLE))
			this.trigger(StateEvent.VISIBLE, new StateEvent(StateEvent.VISIBLE, this._state));
		this._state.panel.visible = true;

		if (this.waitForLoadingComplete && this.animator.hasAnimation(this.transition, TRANSITION_LOADING))
		{
			var timeline = this.animator.getTimeline(this.transition);
			timeline.onComplete = function()
			{
				this.trigger(TRANSITION_IN);
				this._transitioning(TRANSITION_IN, this._onTransitionIn);
			}.bind(this);
			timeline.isLooping = false;
		}
		else
		{
			this.trigger(TRANSITION_IN);
			this._transitioning(TRANSITION_IN, this._onTransitionIn);
		}
	};

	/**
	 * When the transition into a state has finished playing during a state change.
	 * @method _onTransitionIn
	 * @private
	 */
	p._onTransitionIn = function()
	{
		if (this.transition)
		{
			this.transition.visible = false;
		}
		this.trigger(TRANSITION_IN_DONE);
		this._isTransitioning = false;
		this.enabled = true;

		this._state._internalEnterDone();
	};

	/**
	 * Plays the animation "onTransitionLoading" on the transition. Also serves as the animation callback.
	 * Manually looping the animation allows the animation to be synced to the audio while looping.
	 *
	 * @method _onTransitionLoading
	 * @private
	 */
	p._onTransitionLoading = function()
	{
		// Ignore if no transition
		if (!this.transition) return;

		var audio;
		var sounds = this._transitionSounds;
		if (sounds)
		{
			// @deprecate the use of 'loop' sound property in favor of 'loading'
			audio = sounds.loading || sounds.loop;
		}
		var animator = this.animator;
		if (animator.hasAnimation(this.transition, TRANSITION_LOADING))
		{
			this.trigger(TRANSITION_LOADING);
			animator.play(
				this.transition,
				{
					anim: TRANSITION_LOADING,
					audio: audio
				}
			);
		}
		// @deprecate the use of 'transitionLoop' in favor of 'onTransitionLoading'
		else if (animator.hasAnimation(this.transition, 'transitionLoop'))
		{
			this.trigger(TRANSITION_LOADING);
			animator.play(
				this.transition,
				{
					anim: 'transitionLoop',
					audio: audio
				}
			);
		}
	};

	/**
	 * Displays the transition out animation, without changing states. Upon completion, the
	 * transition looping animation automatically starts playing.
	 *
	 * @method showTransitionOut
	 * @param {function} callback The function to call when the animation is complete.
	 */
	p.showTransitionOut = function(callback)
	{
		this.enabled = false;
		this._transitioning(TRANSITION_OUT, function()
			{
				this._onTransitionLoading();
				if (callback) callback();
			}
			.bind(this));
	};

	/**
	 * Displays the transition in animation, without changing states.
	 *
	 * @method showTransitionIn
	 * @param {function} callback The function to call when the animation is complete.
	 */
	p.showTransitionIn = function(callback)
	{
		this._transitioning(TRANSITION_IN, function()
			{
				this.enabled = true;
				this.transition.visible = false;
				if (callback) callback();
			}
			.bind(this));
	};

	/**
	 * Generalized function for transitioning with the manager
	 *
	 * @method _transitioning
	 * @param {String} The animator event to play
	 * @param {Function} The callback function after transition is done
	 * @private
	 */
	p._transitioning = function(event, callback)
	{
		var transition = this.transition;
		var sounds = this._transitionSounds;

		// Ignore with no transition
		if (!transition)
		{
			return callback();
		}

		transition.visible = true;

		var audio;
		if (sounds)
		{
			audio = (event == TRANSITION_IN) ? sounds.in : sounds.out;
		}
		this.animator.play(
			transition,
			{
				anim: event,
				audio: audio
			},
			callback
		);
	};

	/**
	 * Remove the state manager
	 * @method destroy
	 */
	p.destroy = function()
	{
		this._destroyed = true;

		this.off();

		if (this.transition)
		{
			this.animator.stop(this.transition);
		}

		if (this._state)
		{
			this._state._internalExit();
		}

		if (this._states)
		{
			for (var id in this._states)
			{
				this._states[id].destroy();
				delete this._states[id];
			}
		}

		this.transition = null;
		this._state = null;
		this._oldState = null;
		this._states = null;
	};

	// Add to the name space
	namespace('springroll').StateManager = StateManager;
})();