File:State.js

/**
 * @module States
 * @namespace springroll
 * @requires Core
 */
(function(undefined)
{
	// Imports
	var Debug,
		Application,
		EventDispatcher = include('springroll.EventDispatcher');

	/**
	 * Defines the base functionality for a state used by the state manager
	 *
	 * @class State
	 * @constructor
	 * @param {createjs.Container|PIXI.DisplayObjectContainer} panel The panel to associate with
	 * 	this state.
	 * @param {Object} [options] The list of options
	 * @param {String|Function} [options.next=null] The next state alias or function to call when going to the next state.
	 * @param {String|Function} [options.previous=null] The previous state alias to call when going to the previous state.
	 * @param {int} [options.delayLoad=0] The number of frames to delay the loading for cases where
	 *  heavy object instaniation slow the game dramatically.
	 * @param {Array} [options.preload=[]] The assets to preload before the state loads
	 * @param {Object|String} [options.scaling=null] The scaling items to use with the ScaleManager.
	 *       If options.scaling is `"panel"` then the entire panel will be scaled as a title-safe
	 *       item. See `ScaleManager.addItems` for more information about the
	 *       format of the scaling objects. (UI Module only)
	 */
	var State = function(panel, options)
	{
		EventDispatcher.call(this);

		if (!Application)
		{
			Application = include('springroll.Application');
			Debug = include('springroll.Debug', false);
		}

		if (DEBUG && Debug && !panel)
		{
			Debug.error("State requires a panel display object as the first constructor argument");
		}

		// Construct the options
		options = Object.merge(
		{
			next: null,
			previous: null,
			delayLoad: 0,
			preload: [],
			scaling: null
		}, options ||
		{});

		/**
		 * Reference to the main app
		 * @property {Application} app
		 * @readOnly
		 */
		var app = this.app = Application.instance;

		/**
		 * The instance of the VOPlayer, Sound module required
		 * @property {springroll.VOPlayer} voPlayer
		 * @readOnly
		 */
		this.voPlayer = app.voPlayer || null;

		/**
		 * The instance of the Sound, Sound module required
		 * @property {springroll.Sound} sound
		 * @readOnly
		 */
		this.sound = app.sound || null;

		/**
		 * Reference to the main config object
		 * @property {Object} config
		 * @readOnly
		 */
		this.config = app.config || null;

		/**
		 * Reference to the scaling object, UI module required
		 * @property {springroll.ScaleManager} scaling
		 * @readOnly
		 */
		this.scaling = app.scaling || null;

		/**
		 * The items to scale on the panel, see `ScaleManager.addItems` for
		 * more information. If no options are set in the State's constructor
		 * then it will try to find an object on the app config on `scaling` property
		 * matching the same state alias. For instance `config.scaling.title` if
		 * `title` is the state alias. If no scalingItems are set, will scale
		 * and position the panal itself.
		 * @property {Object} scalingItems
		 * @readOnly
		 * @default null
		 */
		this.scalingItems = options.scaling || null;

		/**
		 * The id reference
		 * @property {String} stateId
		 */
		this.stateId = null;

		/**
		 * A reference to the state manager
		 * @property {springroll.StateManager} manager
		 */
		this.manager = null;

		/**
		 * The panel for the state.
		 * @property {createjs.Container|PIXI.DisplayObjectContainer} panel
		 */
		this.panel = panel;

		/**
		 * The assets to load each time
		 * @property {Array} preload
		 */
		this.preload = options.preload;

		/**
		 * Check to see if the assets have finished loading
		 * @property {Boolean} preloaded
		 * @protected
		 * @readOnly
		 */
		this.preloaded = false;

		/**
		 * The collection of assets loaded
		 * @property {Array|Object} assets
		 * @protected
		 */
		this.assets = null;

		/**
		 * If the state has been destroyed.
		 * @property {Boolean} _destroyed
		 * @private
		 */
		this._destroyed = false;

		/**
		 * If the manager considers this the active panel
		 * @property {Boolean} _active
		 * @private
		 */
		this._active = false;

		/**
		 * If we are pre-loading the state
		 * @property {Boolean} _isLoading
		 * @private
		 */
		this._isLoading = false;

		/**
		 * If we canceled entering the state
		 * @property {Boolean} _canceled
		 * @private
		 */
		this._canceled = false;

		/**
		 * When we're finishing loading
		 * @property {Function} _onEnterProceed
		 * @private
		 */
		this._onEnterProceed = null;

		/**
		 * If we start doing a load in enter, assign the onEnterComplete here
		 * @property {Function} _onLoadingComplete
		 * @private
		 */
		this._onLoadingComplete = null;

		/**
		 * If the state is enabled, meaning that it is click ready
		 * @property {Boolean} _enabled
		 * @private
		 */
		this._enabled = false;

		/**
		 * Either the alias of the next state or a function
		 * to call when going to the next state.
		 * @property {String|Function} _nextState
		 * @private
		 */
		this._nextState = options.next;

		/**
		 * Either the alias of the previous state or a function
		 * to call when going to the previous state.
		 * @property {String|Function} _prevState
		 * @private
		 */
		this._prevState = options.previous;

		/**
		 * The number of frames to delay the transition in after loading, to allow the framerate
		 * to stablize after heavy art instantiation.
		 * @property {int} delayLoad
		 * @protected
		 */
		this.delayLoad = options.delayLoad;

		// Hide the panel by default
		this.panel.visible = false;
	};

	// Reference to the prototype
	var s = EventDispatcher.prototype;
	var p = EventDispatcher.extend(State);

	/**
	 * Event when the state finishes exiting. Nothing is showing at this point.
	 * @event exit
	 */

	/**
	 * Event when the state is being destroyed.
	 * @event destroy
	 */

	/**
	 * Event when the transition is finished the state is fully entered.
	 * @event enterDone
	 */

	/**
	 * Event when the loading of a state was canceled.
	 * @event cancel
	 */

	/**
	 * Event when the state starts exiting, everything is showing at this point.
	 * @event exitStart
	 */

	/**
	 * Event when the preload of assets is finished. If no assets are loaded, the `assets` parameter is null.
	 * @event loaded
	 * @param {Object|Array|null} asset The collection of assets loaded
	 */

	/**
	 * When there has been a change in how much has been preloaded
	 * @event progress
	 * @param {Number} percentage The amount preloaded from zero to 1
	 */

	/**
	 * Event when the assets are starting to load.
	 * @event loading
	 * @param {Array} asset An empty array that additional assets can be added to, if needed. Any dynamic
	 *                      assets that are added need to be manually unloaded when the state exits.
	 */

	/**
	 * Event when the state is enabled status changes. Enable is when the state is mouse enabled or not.
	 * @event enabled
	 * @param {Boolean} enable The enabled status of the state
	 */

	// create empty function to avoid a lot of if checks
	var empty = function() {};

	/**
	 * When the state is exited. Override this to provide state cleanup.
	 * @property {function} exit
	 * @default null
	 */
	p.exit = empty;

	/**
	 * When the state has requested to be exit, pre-transition. Override this to ensure
	 * that animation/audio is stopped when leaving the state.
	 * @property {function} exitStart
	 * @default null
	 */
	p.exitStart = empty;

	/**
	 * Cancel the load, implementation-specific.
	 * This is where any async actions should be removed.
	 * @property {function} cancel
	 * @default null
	 */
	p.cancel = empty;

	/**
	 * When the state is entered. Override this to start loading assets - call loadingStart()
	 * to tell the StateManager that that is going on.
	 * @property {function} enter
	 * @default null
	 */
	p.enter = empty;

	/**
	 * When the state is visually entered fully - after the transition is done.
	 * Override this to begin your state's activities.
	 * @property {function} enterDone
	 * @default null
	 */
	p.enterDone = empty;

	/**
	 * Goto the next state
	 * @method nextState
	 * @final
	 */
	p.nextState = function()
	{
		var type = typeof this._nextState;

		if (!this._nextState)
		{
			if (DEBUG && Debug)
			{
				Debug.info("'next' is undefined in current state, ignoring");
			}
			return;
		}
		else if (type === "function")
		{
			this._nextState();
		}
		else if (type === "string")
		{
			this.manager.state = this._nextState;
		}
	};

	/**
	 * Goto the previous state
	 * @method previousState
	 * @final
	 */
	p.previousState = function()
	{
		var type = typeof this._prevState;

		if (!this._prevState)
		{
			if (DEBUG && Debug)
			{
				Debug.info("'prevState' is undefined in current state, ignoring");
			}
			return;
		}
		else if (type === "function")
		{
			this._prevState();
		}
		else if (type === "string")
		{
			this.manager.state = this._prevState;
		}
	};

	/**
	 * Manual call to signal the start of preloading
	 * @method loadingStart
	 * @final
	 */
	p.loadingStart = function()
	{
		if (this._isLoading)
		{
			if (DEBUG && Debug) Debug.warn("loadingStart() was called while we're already loading");
			return;
		}

		this._isLoading = true;
		this.manager.loadingStart();

		// Starting a load is optional and
		// need to be called from the enter function
		// We'll override the existing behavior
		// of internalEnter, by passing
		// the complete function to onLoadingComplete
		this._onLoadingComplete = this._onEnterProceed;
		this._onEnterProceed = null;
	};

	/**
	 * Manual call to signal the end of preloading
	 * @method loadingDone
	 * @final
	 * @param {int} [delay] Frames to delay the load completion to allow the framerate to
	 *   stabilize. If not delay is set, defaults to the `delayLoad` property.
	 */
	p.loadingDone = function(delay)
	{
		if (delay === undefined)
		{
			delay = this.delayLoad;
		}

		if (!this._isLoading)
		{
			if (DEBUG && Debug) Debug.warn("loadingDone() was called without a load started, call loadingStart() first");
			return;
		}

		if (delay && typeof delay == "number")
		{
			//allow the renderer to figure out that any images on stage need decoding during the
			//delay, not during the transition in
			this.panel.visible = true;
			this.app.setTimeout(this.loadingDone.bind(this, 0), delay, true);
			return;
		}

		this._isLoading = false;
		this.manager.loadingDone();

		if (this._onLoadingComplete)
		{
			this._onLoadingComplete();
			this._onLoadingComplete = null;
		}
	};

	/**
	 * Status of whether the panel load was canceled
	 * @property {Boolean} canceled
	 * @readOnly
	 */
	Object.defineProperty(p, 'canceled',
	{
		get: function()
		{
			return this._canceled;
		}
	});

	/**
	 * Get if this is the active state
	 * @property {Boolean} active
	 * @readOnly
	 */
	Object.defineProperty(p, 'active',
	{
		get: function()
		{
			return this._active;
		}
	});

	/**
	 * If the state is enabled, meaning that it is click ready
	 * @property {Boolean} enabled
	 */
	Object.defineProperty(p, 'enabled',
	{
		get: function()
		{
			return this._enabled;
		},
		set: function(value)
		{
			var oldEnabled = this._enabled;
			this._enabled = value;
			if (oldEnabled != value)
			{
				this.trigger('enabled', value);
			}
		}
	});

	/**
	 * If the state has been destroyed.
	 * @property {Boolean} destroyed
	 * @readOnly
	 */
	Object.defineProperty(p, 'destroyed',
	{
		get: function()
		{
			return this._destroyed;
		}
	});

	/**
	 * This is called by the State Manager to exit the state
	 * @method _internalExit
	 * @protected
	 */
	p._internalExit = function()
	{
		this.preloaded = false;

		// local variables
		var panel = this.panel;
		var items = this.scalingItems;
		var scaling = this.scaling;

		//remove scaling objects that we added
		if (scaling && items)
		{
			if (items == "panel")
			{
				scaling.removeItem(panel);
			}
			else
			{
				scaling.removeItems(panel, items);
			}
		}

		// Clean any assets loaded by the manifest
		if (this.preload.length)
		{
			this.app.unload(this.preload);
		}

		if (this._isTransitioning)
		{
			this._isTransitioning = false;
			if (this.manager.animator)
			{
				this.manager.animator.stop(panel);
			}
		}
		this._enabled = false;
		panel.visible = false;
		this._active = false;
		this.exit();

		this.trigger('exit');
	};

	/**
	 * When the state is entering
	 * @method _internalEntering
	 * @param {Function} proceed The function to call after enter has been called
	 * @protected
	 */
	p._internalEntering = function()
	{
		this.enter();

		this.trigger('enter');

		// Start prealoading assets
		this.loadingStart();

		// Boolean to see if we've preloaded assests
		this.preloaded = false;

		var assets = [];

		this.trigger('loading', assets);

		if (this.preload.length)
		{
			assets = this.preload.concat(assets);
		}

		// Start loading assets if we have some
		if (assets.length)
		{
			this.app.load(assets,
			{
				complete: this._onLoaded.bind(this),
				progress: onProgress.bind(this),
				cacheAll: true
			});
		}
		// No files to load, just continue
		else
		{
			this._onLoaded(null);
		}
	};

	/**
	 * Handle the load progress and pass to the manager
	 * @method onProgress
	 * @private
	 * @param {Number} progress The amount preloaded from zero to 1
	 */
	var onProgress = function(progress)
	{
		this.trigger('progress', progress);
		this.manager.trigger('progress', progress);
	};

	/**
	 * The internal call for on assets loaded
	 * @method _onLoaded
	 * @private
	 * @param {Object|null} assets The assets result of the load
	 */
	p._onLoaded = function(assets)
	{
		this.assets = assets;
		this.preloaded = true;

		this.trigger('loaded', assets);

		if (this.scaling)
		{
			var items = this.scalingItems;

			if (items)
			{
				if (items == "panel")
				{
					// Reset the panel scale & position, to ensure
					// that the panel is scaled properly
					// upon state re-entry
					this.panel.x = this.panel.y = 0;
					this.panel.scaleX = this.panel.scaleY = 1;

					this.scaling.addItem(this.panel,
					{
						align: "top-left",
						titleSafe: true
					});
				}
				else
				{
					this.scaling.addItems(this.panel, items);
				}
			}
		}
		this.loadingDone();
	};

	/**
	 * Exit the state start, called by the State Manager
	 * @method _internalExitStart
	 * @protected
	 */
	p._internalExitStart = function()
	{
		this.exitStart();
		this.trigger('exitStart');
	};

	/**
	 * Exit the state start, called by the State Manager
	 * @method _internalEnter
	 * @param {Function} proceed The function to call after enter has been called
	 * @protected
	 */
	p._internalEnter = function(proceed)
	{
		if (this._isTransitioning)
		{
			this._isTransitioning = false;
			if (this.manager.animator)
			{
				this.manager.animator.stop(this.panel);
			}
		}
		this._enabled = false;
		this._active = true;
		this._canceled = false;

		this._onEnterProceed = proceed;
		this._internalEntering();

		if (this._onEnterProceed)
		{
			this._onEnterProceed();
			this._onEnterProceed = null;
		}
	};

	/**
	 * Cancel the loading of this state
	 * @method _internalCancel
	 * @protected
	 */
	p._internalCancel = function()
	{
		this._active = false;
		this._canceled = true;
		this._isLoading = false;

		this._internalExit();
		this.cancel();
		this.trigger('cancel');
	};

	/**
	 * Exit the state start, called by the State Manager
	 * @method _internalEnterDone
	 * @private
	 */
	p._internalEnterDone = function()
	{
		if (this._canceled) return;

		this.enabled = true;
		this.enterDone();
		this.trigger('enterDone');
	};

	/**
	 * Don't use the state object after this
	 * @method destroy
	 */
	p.destroy = function()
	{
		// Only destroy once!
		if (this._destroyed) return;

		this.trigger('destroy');

		this.app = null;
		this.scaling = null;
		this.sound = null;
		this.voPlayer = null;
		this.config = null;
		this.scalingItems = null;
		this.assets = null;
		this.preload = null;
		this.panel = null;
		this.manager = null;
		this._destroyed = true;
		this._onEnterProceed = null;
		this._onLoadingComplete = null;

		s.destroy.call(this);
	};

	// Add to the namespace
	namespace('springroll').State = State;

}());