UNPKG

lettuce

Version:

Lettuce JS, Mini Mobile Framework for Romantic with DSL.

732 lines (657 loc) 25.2 kB
/* * MelonJS Game Engine * Copyright (C) 2011 - 2013, Olivier Biot, Jason Oster * http://www.melonjs.org * */ (function () { /** * Private function to re-use for object removal in a defer * @ignore */ var deferredRemove = function (child, keepalive) { if (child.ancestor) { child.ancestor.removeChildNow(child, keepalive); } }; /** * A global "translation context" for nested Containers * @ignore */ var globalTranslation = new me.Rect(0, 0, 0, 0); /** * A global "floating children" reference counter for nested ObjectContainers * @ignore */ var globalFloatingCounter = 0; /** * me.Container represents a collection of child objects * @class * @extends me.Renderable * @memberOf me * @constructor * @param {Number} [x=0] position of the container * @param {Number} [y=0] position of the container * @param {Number} [w=me.game.viewport.width] width of the container * @param {number} [h=me.game.viewport.height] height of the container */ me.Container = me.Renderable.extend( /** @scope me.Container.prototype */ { /** * constructor * @ignore */ init : function (x, y, width, height) { /** * keep track of pending sort * @ignore */ this.pendingSort = null; // TODO; container do not have a physic body // ADD child container child one by one to the quadtree? /** * the container default transformation matrix * @public * @type me.Matrix2d * @name transform * @memberOf me.Container */ this.transform = new me.Matrix2d(); // call the _super constructor this._super(me.Renderable, "init", [x, y, width || Infinity, height || Infinity] ); // init the bounds to an empty rect /** * Container bounds * @ignore */ this.bounds = undefined; /** * The array of children of this container. * @ignore */ this.children = []; // by default reuse the global me.game.setting /** * The property of the child object that should be used to sort on <br> * value : "x", "y", "z" (default: me.game.sortOn) * @public * @type String * @name sortOn * @memberOf me.Container */ this.sortOn = me.game.sortOn; /** * Specify if the children list should be automatically sorted when adding a new child * @public * @type Boolean * @name autoSort * @memberOf me.Container */ this.autoSort = true; this.transform.identity(); /** * Used by the debug panel plugin * @ignore */ this.drawCount = 0; }, /** * Add a child to the container <br> * if auto-sort is disable, the object will be appended at the bottom of the list * @name addChild * @memberOf me.Container * @function * @param {me.Renderable} child * @param {number} [zIndex] forces the z index of the child to the specified value. * @return {me.Renderable} the added child */ addChild : function (child, zIndex) { if (typeof(child.ancestor) !== "undefined") { child.ancestor.removeChildNow(child); } else { // only allocate a GUID if the object has no previous ancestor // (e.g. move one child from one container to another) if (child.isRenderable) { // allocated a GUID value child.GUID = me.utils.createGUID(); } } // change the child z-index if one is specified if (typeof(zIndex) === "number") { child.z = zIndex; } // specify a z property to infinity if not defined if ((typeof child.z === "undefined") || (child.z !== child.z)) { child.z = this.children.length; } child.ancestor = this; this.children.push(child); if (this.autoSort === true) { this.sort(); } if (typeof child.onActivateEvent === "function") { child.onActivateEvent(); } return child; }, /** * Add a child to the container at the specified index<br> * (the list won't be sorted after insertion) * @name addChildAt * @memberOf me.Container * @function * @param {me.Renderable} child * @param {Number} index * @return {me.Renderable} the added child */ addChildAt : function (child, index) { if (index >= 0 && index < this.children.length) { if (typeof(child.ancestor) !== "undefined") { child.ancestor.removeChildNow(child); } else { // only allocate a GUID if the object has no previous ancestor // (e.g. move one child from one container to another) if (child.isRenderable) { // allocated a GUID value child.GUID = me.utils.createGUID(); } } child.ancestor = this; this.children.splice(index, 0, child); if (typeof child.onActivateEvent === "function") { child.onActivateEvent(); } return child; } else { throw new me.Container.Error("Index (" + index + ") Out Of Bounds for addChildAt()"); } }, /** * Swaps the position (z depth) of 2 childs * @name swapChildren * @memberOf me.Container * @function * @param {me.Renderable} child * @param {me.Renderable} child */ swapChildren : function (child, child2) { var index = this.getChildIndex(child); var index2 = this.getChildIndex(child2); if ((index !== -1) && (index2 !== -1)) { // swap z index var _z = child.z; child.z = child2.z; child2.z = _z; // swap the positions.. this.children[index] = child2; this.children[index2] = child; } else { throw new me.Container.Error(child + " Both the supplied childs must be a child of the caller " + this); } }, /** * Returns the Child at the specified index * @name getChildAt * @memberOf me.Container * @function * @param {Number} index */ getChildAt : function (index) { if (index >= 0 && index < this.children.length) { return this.children[index]; } else { throw new me.Container.Error("Index (" + index + ") Out Of Bounds for getChildAt()"); } }, /** * Returns the index of the Child * @name getChildAt * @memberOf me.Container * @function * @param {me.Renderable} child */ getChildIndex : function (child) { return this.children.indexOf(child); }, /** * Returns true if contains the specified Child * @name hasChild * @memberOf me.Container * @function * @param {me.Renderable} child * @return {Boolean} */ hasChild : function (child) { return this === child.ancestor; }, /** * return the child corresponding to the given property and value.<br> * note : avoid calling this function every frame since * it parses the whole object tree each time * @name getChildByProp * @memberOf me.Container * @public * @function * @param {String} prop Property name * @param {String|RegExp|Number} value Value of the property * @return {me.Renderable[]} Array of childs * @example * // get the first child object called "mainPlayer" in a specific container : * var ent = myContainer.getChildByProp("name", "mainPlayer"); * * // or query the whole world : * var ent = me.game.world.getChildByProp("name", "mainPlayer"); * * // partial property matches are also allowed by using a RegExp. * // the following matches "redCOIN", "bluecoin", "bagOfCoins", etc : * var allCoins = me.game.world.getChildByProp("name", /coin/i); * * // searching for numbers or other data types : * var zIndex10 = me.game.world.getChildByProp("z", 10); * var inViewport = me.game.world.getChildByProp("inViewport", true); */ getChildByProp : function (prop, value) { var objList = []; function compare(obj, prop) { var v = obj[prop]; if (value instanceof RegExp && typeof(v) === "string") { if (value.test(v)) { objList.push(obj); } } else if (v === value) { objList.push(obj); } } for (var i = this.children.length - 1; i >= 0; i--) { var obj = this.children[i]; compare(obj, prop); if (obj instanceof me.Container) { objList = objList.concat(obj.getChildByProp(prop, value)); } } return objList; }, /** * returns the list of childs with the specified name<br> * as defined in Tiled (Name field of the Object Properties)<br> * note : avoid calling this function every frame since * it parses the whole object list each time * @name getChildByName * @memberOf me.Container * @public * @function * @param {String|RegExp|Number} name entity name * @return {me.Renderable[]} Array of childs */ getChildByName : function (name) { return this.getChildByProp("name", name); }, /** * return the child corresponding to the specified GUID<br> * note : avoid calling this function every frame since * it parses the whole object list each time * @name getChildByGUID * @memberOf me.Container * @public * @function * @param {String|RegExp|Number} GUID entity GUID * @return {me.Renderable} corresponding child or null */ getChildByGUID : function (guid) { var obj = this.getChildByProp("GUID", guid); return (obj.length > 0) ? obj[0] : null; }, /** * returns the bounding box for this container, the smallest rectangle object completely containing all childrens * @name getBounds * @memberOf me.Container * @function * @param {me.Rect} [rect] an optional rectangle object to use when returning the bounding rect(else returns a new object) * @return {me.Rect} new rectangle */ getBounds : function () { if (!this.bounds) { this.bounds = new me.Rect(Infinity, Infinity, -Infinity, -Infinity); } else { // reset the rect with default values this.bounds.pos.set(Infinity, Infinity); this.bounds.resize(-Infinity, -Infinity); } var childBounds; for (var i = this.children.length, child; i--, (child = this.children[i]);) { if (child.isRenderable) { childBounds = child.getBounds(); // TODO : returns an "empty" rect instead of null (e.g. EntityObject) // TODO : getBounds should always return something anyway if (childBounds !== null) { this.bounds.union(childBounds); } } } // TODO : cache the value until any childs are modified? (next frame?) return this.bounds; }, /** * Invokes the removeChildNow in a defer, to ensure the child is removed safely after the update & draw stack has completed * @name removeChild * @memberOf me.Container * @public * @function * @param {me.Renderable} child * @param {Boolean} [keepalive=False] True to prevent calling child.destroy() */ removeChild : function (child, keepalive) { if (child.ancestor) { deferredRemove.defer(this, child, keepalive); } }, /** * Removes (and optionally destroys) a child from the container.<br> * (removal is immediate and unconditional)<br> * Never use keepalive=true with objects from {@link me.pool}. Doing so will create a memory leak. * @name removeChildNow * @memberOf me.Container * @function * @param {me.Renderable} child * @param {Boolean} [keepalive=False] True to prevent calling child.destroy() */ removeChildNow : function (child, keepalive) { if (this.hasChild(child)) { child.ancestor = undefined; if (typeof child.onDeactivateEvent === "function") { child.onDeactivateEvent(); } if (!keepalive) { if (typeof (child.destroy) === "function") { child.destroy(); } me.pool.push(child); } this.children.splice(this.getChildIndex(child), 1); } else { throw new me.Container.Error(child + " The supplied child must be a child of the caller " + this); } }, /** * Automatically set the specified property of all childs to the given value * @name setChildsProperty * @memberOf me.Container * @function * @param {String} property property name * @param {Object} value property value * @param {Boolean} [recursive=false] recursively apply the value to child containers if true */ setChildsProperty : function (prop, val, recursive) { for (var i = this.children.length; i >= 0; i--) { var obj = this.children[i]; if ((recursive === true) && (obj instanceof me.Container)) { obj.setChildsProperty(prop, val, recursive); } obj[prop] = val; } }, /** * Move the child in the group one step forward (z depth). * @name moveUp * @memberOf me.Container * @function * @param {me.Renderable} child */ moveUp : function (child) { var childIndex = this.getChildIndex(child); if (childIndex - 1 >= 0) { // note : we use an inverted loop this.swapChildren(child, this.getChildAt(childIndex - 1)); } }, /** * Move the child in the group one step backward (z depth). * @name moveDown * @memberOf me.Container * @function * @param {me.Renderable} child */ moveDown : function (child) { var childIndex = this.getChildIndex(child); if (childIndex + 1 < this.children.length) { // note : we use an inverted loop this.swapChildren(child, this.getChildAt(childIndex + 1)); } }, /** * Move the specified child to the top(z depth). * @name moveToTop * @memberOf me.Container * @function * @param {me.Renderable} child */ moveToTop : function (child) { var childIndex = this.getChildIndex(child); if (childIndex > 0) { // note : we use an inverted loop this.children.splice(0, 0, this.children.splice(childIndex, 1)[0]); // increment our child z value based on the previous child depth child.z = this.children[1].z + 1; } }, /** * Move the specified child the bottom (z depth). * @name moveToBottom * @memberOf me.Container * @function * @param {me.Renderable} child */ moveToBottom : function (child) { var childIndex = this.getChildIndex(child); if (childIndex < (this.children.length - 1)) { // note : we use an inverted loop this.children.splice((this.children.length - 1), 0, this.children.splice(childIndex, 1)[0]); // increment our child z value based on the next child depth child.z = this.children[(this.children.length - 2)].z - 1; } }, /** * Manually trigger the sort of all the childs in the container</p> * @name sort * @memberOf me.Container * @public * @function * @param {Boolean} [recursive=false] recursively sort all containers if true */ sort : function (recursive) { // do nothing if there is already a pending sort if (this.pendingSort === null) { if (recursive === true) { // trigger other child container sort function (if any) for (var i = this.children.length, obj; i--, (obj = this.children[i]);) { if (obj instanceof me.Container) { // note : this will generate one defered sorting function // for each existing containe obj.sort(recursive); } } } /** @ignore */ this.pendingSort = function (self) { // sort everything in this container self.children.sort(self["_sort" + self.sortOn.toUpperCase()]); // clear the defer id self.pendingSort = null; // make sure we redraw everything me.game.repaint(); }.defer(this, this); } }, /** * Z Sorting function * @ignore */ _sortZ : function (a, b) { return (b.z) - (a.z); }, /** * X Sorting function * @ignore */ _sortX : function (a, b) { /* ? */ var result = (b.z - a.z); return (result ? result : ((b.pos && b.pos.x) - (a.pos && a.pos.x)) || 0); }, /** * Y Sorting function * @ignore */ _sortY : function (a, b) { var result = (b.z - a.z); return (result ? result : ((b.pos && b.pos.y) - (a.pos && a.pos.y)) || 0); }, /** * Destroy function<br> * @ignore */ destroy : function () { // cancel any sort operation if (this.pendingSort) { clearTimeout(this.pendingSort); this.pendingSort = null; } // delete all children for (var i = this.children.length, obj; i--, (obj = this.children[i]);) { // don't remove it if a persistent object if (!obj.isPersistent) { this.removeChildNow(obj); } } // reset the transformation matrix this.transform.identity(); }, /** * @ignore */ update : function (dt) { var isDirty = false; var isFloating = false; var isPaused = me.state.isPaused(); var isTranslated = false; var isStacked = false; var stack = []; var x = 0; var y = 0; var z = 0; var viewport = me.game.viewport; for (var i = this.children.length, obj; i--, (obj = this.children[i]);) { if (isPaused && (!obj.updateWhenPaused)) { // skip this object continue; } if (obj.isRenderable) { isFloating = (globalFloatingCounter > 0 || obj.floating); if (isFloating) { globalFloatingCounter++; } // Translate global context isTranslated = !isFloating; if (isTranslated) { x = obj.pos.x; y = obj.pos.y; z = Math.abs(x + y + obj.width + obj.height); if (z !== z || z === Infinity) { isStacked = true; stack.push(globalTranslation.clone()); } globalTranslation.translateV(obj.pos); globalTranslation.resize(obj.width, obj.height); } // check if object is visible obj.inViewport = isFloating || viewport.isVisible(globalTranslation); // update our object isDirty = ((obj.inViewport || obj.alwaysUpdate) && obj.update(dt)) || isDirty; // Undo global context translation if (isTranslated) { if (isStacked) { isStacked = false; globalTranslation.copy(stack.pop()); } else { globalTranslation.translate(-x, -y); } } if (globalFloatingCounter > 0) { globalFloatingCounter--; } } else { // just directly call update() for non renderable object isDirty = obj.update(dt) || isDirty; } } return isDirty; }, /** * @ignore */ draw : function (renderer, rect) { var viewport = me.game.viewport; var isFloating = false; this.drawCount = 0; renderer.save(); // apply the container current transform renderer.transform.apply(renderer, this.transform.val); // apply the group opacity renderer.setGlobalAlpha(renderer.globalAlpha() * this.getOpacity()); // translate to the container position renderer.translate(this.pos.x, this.pos.y); for (var i = this.children.length, obj; i--, (obj = this.children[i]);) { isFloating = obj.floating; if ((obj.inViewport || isFloating) && obj.isRenderable) { if (isFloating === true) { // translate to object renderer.translate( viewport.screenX - this.pos.x, viewport.screenY - this.pos.y ); } // draw the object obj.draw(renderer, rect); if (isFloating === true) { // translate back to viewport renderer.translate( this.pos.x - viewport.screenX, this.pos.y - viewport.screenY ); } this.drawCount++; } } renderer.restore(); } }); /** * Base class for ObjectContainer exception handling. * @name Error * @class * @memberOf me.Container * @constructor * @param {String} msg Error message. */ me.Container.Error = me.Renderable.Error.extend({ init : function (msg) { this._super(me.Renderable.Error, "init", [ msg ]); this.name = "me.Container.Error"; } }); })();