/*global Quintus:false */ Quintus.Scenes = function(Q) { Q.scenes = {}; Q.stages = []; Q.Scene = Q.Class.extend({ init: function(sceneFunc,opts) { this.opts = opts || {}; this.sceneFunc = sceneFunc; } }); // Set up or return a new scene Q.scene = function(name,sceneObj,opts) { if(sceneObj === void 0) { return Q.scenes[name]; } else { if(Q._isFunction(sceneObj)) { sceneObj = new Q.Scene(sceneObj,opts); } Q.scenes[name] = sceneObj; return sceneObj; } }; Q._nullContainer = { c: { x: 0, y: 0, /* cx: 0, cy: 0, */ angle: 0, scale: 1 }, matrix: Q.matrix2d() }; // Default to SAT collision between two objects // Thanks to doc's at: http://www.sevenson.com.au/actionscript/sat/ Q.collision = (function() { var normalX, normalY, offset = [ 0,0 ], result1 = { separate: [] }, result2 = { separate: [] }; function calculateNormal(points,idx) { var pt1 = points[idx], pt2 = points[idx+1] || points[0]; normalX = -(pt2[1] - pt1[1]); normalY = pt2[0] - pt1[0]; var dist = Math.sqrt(normalX*normalX + normalY*normalY); if(dist > 0) { normalX /= dist; normalY /= dist; } } function dotProductAgainstNormal(point) { return (normalX * point[0]) + (normalY * point[1]); } function collide(o1,o2,flip) { var min1,max1, min2,max2, d1, d2, offsetLength, tmp, i, j, minDist, minDistAbs, shortestDist = Number.POSITIVE_INFINITY, collided = false, p1, p2; var result = flip ? result2 : result1; offset[0] = 0; //o1.x + o1.cx - o2.x - o2.cx; offset[1] = 0; //o1.y + o1.cy - o2.y - o2.cy; // If we have a position matrix, just use those points, if(o1.c) { p1 = o1.c.points; } else { p1 = o1.p.points; offset[0] += o1.p.x; offset[1] += o1.p.y; } if(o2.c) { p2 = o2.c.points; } else { p2 = o2.p.points; offset[0] += -o2.p.x; offset[1] += -o2.p.y; } o1 = o1.p; o2 = o2.p; for(i = 0;i max1) { max1 = tmp; } } min2 = dotProductAgainstNormal(p2[0]); max2 = min2; for(j = 1;j max2) { max2 = tmp; } } offsetLength = dotProductAgainstNormal(offset); min1 += offsetLength; max1 += offsetLength; d1 = min1 - max2; d2 = min2 - max1; if(d1 > 0 || d2 > 0) { return null; } minDist = (max2 - min1) * -1; if(flip) { minDist *= -1; } minDistAbs = Math.abs(minDist); if(minDistAbs < shortestDist) { result.distance = minDist; result.magnitude = minDistAbs; result.normalX = normalX; result.normalY = normalY; if(result.distance > 0) { result.distance *= -1; result.normalX *= -1; result.normalY *= -1; } collided = true; shortestDist = minDistAbs; } } // Do return the actual collision return collided ? result : null; } function satCollision(o1,o2) { var result1, result2, result; // Don't compare a square to a square for no reason // if(!o1.p.points && !o2.p.points) return true; if(!o1.p.points) { Q._generatePoints(o1); } if(!o2.p.points) { Q._generatePoints(o2); } Q._generateCollisionPoints(o1); Q._generateCollisionPoints(o2); result1 = collide(o1,o2); if(!result1) { return false; } result2 = collide(o2,o1,true); if(!result2) { return false; } result = (result2.magnitude < result1.magnitude) ? result2 : result1; if(result.magnitude === 0) { return false; } result.separate[0] = result.distance * result.normalX; result.separate[1] = result.distance * result.normalY; return result; } return satCollision; }()); Q.overlap = function(o1,o2) { var c1 = o1.c || o1.p; var c2 = o2.c || o2.p; var o1x = c1.x - c1.cx, o1y = c1.y - c1.cy; var o2x = c2.x - c2.cx, o2y = c2.y - c2.cy; return !((o1y+c1.ho2y+c2.h) || (o1x+c1.wo2x+c2.w)); }; Q.Stage = Q.GameObject.extend({ // Should know whether or not the stage is paused defaults: { sort: false, gridW: 400, gridH: 400 }, init: function(scene,opts) { this.scene = scene; this.items = []; this.lists = {}; this.index = {}; this.removeList = []; this.grid = {}; this.time = 0; this.options = Q._extend({},this.defaults); if(this.scene) { Q._extend(this.options,scene.opts); } if(opts) { Q._extend(this.options,opts); } if(this.options.sort && !Q._isFunction(this.options.sort)) { this.options.sort = function(a,b) { return ((a.p && a.p.z) || -1) - ((b.p && b.p.z) || -1); }; } }, destroyed: function() { this.invoke("debind"); this.trigger("destroyed"); }, // Needs to be separated out so the current stage can be set loadScene: function() { if(this.scene) { this.scene.sceneFunc(this); } }, // Load an array of assets of the form: // [ [ "Player", { x: 15, y: 54 } ], // [ "Enemy", { x: 54, y: 42 } ] ] // Either pass in the array or a string of asset name loadAssets: function(asset) { var assetArray = Q._isArray(asset) ? asset : Q.asset(asset); for(var i=0;i= 0; i--) { if(func.call(this.items[i],arguments[1],arguments[2],arguments[3])) { return this.items[i]; } } return false; }, identify: function(func) { var result; for(var i = this.items.length-1;i >= 0; i--) { if(result = func.call(this.items[i],arguments[1],arguments[2],arguments[3])) { return result; } } return false; }, addToLists: function(lists,object) { for(var i=0;i 0 && (col = this._collisionLayer.collide(obj))) { col.obj = this._collisionLayer; if(!skipEvents) { obj.trigger('hit',col); obj.trigger('hit.collision',col); } this.regrid(obj); curCol--; } } curCol = maxCol; while(curCol > 0 && (col2 = this.gridTest(obj,collisionMask,this._collisionLayer))) { obj.trigger('hit',col2); obj.trigger('hit.sprite',col2); // Do the recipricol collision // TODO: extract if(!skipEvents) { var obj2 = col2.obj; col2.obj = obj; col2.normalX *= -1; col2.normalY *= -1; col2.distance = 0; col2.magnitude = 0; col2.separate[0] = 0; col2.separate[1] = 0; obj2.trigger('hit',col2); obj2.trigger('hit.sprite',col2); } this.regrid(obj); curCol--; } return col2 || col; }, delGrid: function(item) { var grid = item.grid; for(var y = grid.Y1;y <= grid.Y2;y++) { if(this.grid[y]) { for(var x = grid.X1;x <= grid.X2;x++) { if(this.grid[y][x]) { delete this.grid[y][x][item.p.id]; } } } } }, addGrid: function(item) { var grid = item.grid; for(var y = grid.Y1;y <= grid.Y2;y++) { if(!this.grid[y]) { this.grid[y] = {}; } for(var x = grid.X1;x <= grid.X2;x++) { if(!this.grid[y][x]) { this.grid[y][x] = {}; } this.grid[y][x][item.p.id] = item.p.type; } } }, // Add an item into the collision detection grid, // Ignore the collision layer or objects without a type regrid: function(item,skipAdd) { if(this._collisionLayer && item === this._collisionLayer) { return; } var c = item.c || item.p; var gridX1 = Math.floor((c.x - c.cx) / this.options.gridW), gridY1 = Math.floor((c.y - c.cy) / this.options.gridH), gridX2 = Math.floor((c.x - c.cx + c.w) / this.options.gridW), gridY2 = Math.floor((c.y - c.cy + c.h) / this.options.gridH), grid = item.grid; if(grid.X1 !== gridX1 || grid.X2 !== gridX2 || grid.Y1 !== gridY1 || grid.Y2 !== gridY2) { if(grid.X1 !== void 0) { this.delGrid(item); } grid.X1 = gridX1; grid.X2 = gridX2; grid.Y1 = gridY1; grid.Y2 = gridY2; if(!skipAdd) { this.addGrid(item); } } }, markSprites: function(items,time) { var viewport = this.viewport, scale = viewport ? viewport.scale : 1, x = viewport ? viewport.x : 0, y = viewport ? viewport.y : 0, viewW = Q.width / scale, viewH = Q.height / scale, gridX1 = Math.floor(x / this.options.gridW), gridY1 = Math.floor(y / this.options.gridH), gridX2 = Math.floor((x + viewW) / this.options.gridW), gridY2 = Math.floor((y + viewH) / this.options.gridH), gridRow, gridBlock; for(var iy=gridY1; iy<=gridY2; iy++) { if((gridRow = this.grid[iy])) { for(var ix=gridX1; ix<=gridX2; ix++) { if((gridBlock = gridRow[ix])) { for(var id in gridBlock) { this.index[id].mark = time; if(this.index[id].container) { this.index[id].container.mark = time; } } } } } } if(this._collisionLayer) { this._collisionLayer.mark = time; } }, updateSprites: function(items,dt,isContainer) { var item; for(var i=0,len=items.length;i 0) { for(var i=0,len=this.removeList.length;i= this.time) { item.render(ctx); } } this.trigger("render",ctx); this.trigger("postrender",ctx); } }); Q.activeStage = 0; Q.StageSelector = Q.Class.extend({ emptyList: [], init: function(stage,selector) { this.stage = stage; this.selector = selector; // Generate an object list from the selector // TODO: handle array selectors this.items = this.stage.lists[this.selector] || this.emptyList; this.length = this.items.length; }, each: function(callback) { for(var i=0,len=this.items.length;i