// Quintus Game Engine // (c) 2012 Pascal Rettig, Cykod LLC // Quintus may be freely distributed under the MIT license or GPLv2 License. // For all details and documentation: // http://html5quintus.com // // Quintus HTML5 Game Engine // ========================= // // The code in `quintus.js` defines the base `Quintus()` method // which create an instance of the engine. The basic engine doesn't // do a whole lot - it provides an architecture for extension, a // game loop, and a method for creating or binding to an exsiting // canvas context. The engine has dependencies on Underscore.js and jQuery, // although the jQuery dependency will be removed in the future. // // Most of the game-specific functionality is in the // various other modules: // // * `quintus_input.js` - `Input` module, which allows for user input via keyboard and touchscreen // * `quintus_sprites.js` - `Sprites` module, which defines a basic `Q.Sprite` class along with spritesheet support in `Q.SpriteSheet`. // * `quintus_scenes.js` - `Scenes` module. It defines the `Q.Scene` class, which allows creation of reusable scenes, and the `Q.Stage` class, which handles managing a number of sprites at once. // * `quintus_anim.js` - `Anim` module, which adds in support for animations on sprites along with a `viewport` component to follow the player around and a `Q.Repeater` class that can create a repeating, scrolling background. // Engine Bootstrapping // ==================== // Top-level Quintus engine factory wrapper, // creates new instances of the engine by calling: // // var Q = Quintus({ ... }); // // Any initial setup methods also all return the `Q` object, allowing any initial // setup calls to be chained together. // // var Q = Quintus() // .include("Input, Sprites, Scenes") // .setup('quintus', { maximize: true }) // .controls(); // // `Q` is used internally as the object name, and is used in most of the examples, // but multiple instances of the engine on the same page can have different names. // // var Game1 = Quintus(), Game2 = Quintus(); // var Quintus = function Quintus(opts) { // A la jQuery - the returned `Q` object is actually // a method that calls `Q.select`. `Q.select` doesn't do anything // initially, but can be overridden by a module to allow // selection of game objects. The `Scenes` module adds in // the select method which selects from the default stage. // // var Q = Quintus().include("Sprites, Scenes"); // ... Game Code ... // // Set the angry property on all Enemy1 class objects to true // Q("Enemy1").p({ angry: true }); // var Q = function(selector,scope,options) { return Q.select(selector,scope,options); }; Q.select = function() { /* No-op */ }; // Syntax for including other modules into quintus, can accept a comma-separated // list of strings, an array of strings, or an array of actual objects. Example: // // Q.include("Input, Sprites, Scenes") // Q.include = function(mod) { Q._each(Q._normalizeArg(mod),function(name) { var m = Quintus[name] || name; if(!Q._isFunction(m)) { throw "Invalid Module:" + name; } m(Q); }); return Q; }; // Utility Methods // =============== // // Most of these utility methods are a subset of Underscore.js, // Most are pulled directly from underscore and some are // occasionally optimized for speed and memory usage in lieu of flexibility. // Underscore.js is (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. // Underscore is freely distributable under the MIT license. // http://underscorejs.org // An internal utility method (utility methods are prefixed with underscores) // It's used to take a string of comma separated names and turn it into an `Array` // of names. If an array of names is passed in, it's left as is. Example usage: // // Q._normalizeArg("Sprites, Scenes, Physics "); // // returns [ "Sprites", "Scenes", "Physics" ] // // Used by `Q.include` and `Q.Sprite.add` to add modules and components, respectively. Q._normalizeArg = function(arg) { if(Q._isString(arg)) { arg = arg.replace(/\s+/g,'').split(","); } if(!Q._isArray(arg)) { arg = [ arg ]; } return arg; }; // Extends a destination object // with a source object Q._extend = function(dest,source) { if(!source) { return dest; } for (var prop in source) { dest[prop] = source[prop]; } return dest; }; // Return a shallow copy of an object. Sub-objects (and sub-arrays) are not cloned. Q._clone = function(obj) { return Q._extend({},obj); }; // Method that adds default properties onto // an object only if the key is undefined Q._defaults = function(dest,source) { if(!source) { return dest; } for (var prop in source) { if(dest[prop] === void 0) { dest[prop] = source[prop]; } } return dest; }; // Shortcut for hasOwnProperty Q._has = function(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); }; // Check if something is a string // NOTE: this fails for non-primitives Q._isString = function(obj) { return typeof obj === "string"; }; Q._isNumber = function(obj) { return Object.prototype.toString.call(obj) === '[object Number]'; }; // Check if something is a function Q._isFunction = function(obj) { return Object.prototype.toString.call(obj) === '[object Function]'; }; // Check if something is a function Q._isObject = function(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; }; // Check if something is a function Q._isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; // Check if something is undefined Q._isUndefined = function(obj) { return obj === void 0; }; // Removes a property from an object and returns it Q._popProperty = function(obj,property) { var val = obj[property]; delete obj[property]; return val; }; // Basic iteration method. This can often be a performance // handicap when the callback iterator is created inline, // as this leads to lots of functions that need to be GC'd. // Better is to define the iterator as a private method so // it is only created once. Q._each = function(obj,iterator,context) { if (obj == null) { return; } if (obj.forEach) { obj.forEach(iterator,context); } else if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { iterator.call(context, obj[i], i, obj); } } else { for (var key in obj) { iterator.call(context, obj[key], key, obj); } } }; // Invoke the named property on each element of the array Q._invoke = function(arr,property,arg1,arg2) { if (arr == null) { return; } for (var i = 0, l = arr.length; i < l; i++) { arr[i][property](arg1,arg2); } }; // Basic detection method, returns the first instance where the // iterator returns truthy. Q._detect = function(obj,iterator,context,arg1,arg2) { var result; if (obj == null) { return; } if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { result = iterator.call(context, obj[i], i, arg1,arg2); if(result) { return result; } } return false; } else { for (var key in obj) { result = iterator.call(context, obj[key], key, arg1,arg2); if(result) { return result; } } return false; } }; // Returns a new Array with entries set to the return value of the iterator. Q._map = function(obj, iterator, context) { var results = []; if (obj == null) { return results; } if (obj.map) { return obj.map(iterator, context); } Q._each(obj, function(value, index, list) { results[results.length] = iterator.call(context, value, index, list); }); if (obj.length === +obj.length) { results.length = obj.length; } return results; }; // Returns a sorted copy of unique array elements with null remove Q._uniq = function(arr) { arr = arr.slice().sort(); var output = []; var last = null; for(var i=0;i Q.options.frameTimeLimit) { dt = Q.options.frameTimeLimit; } callback.apply(Q,[dt / 1000]); Q.lastGameLoopFrame = now; }; window.requestAnimationFrame(Q.gameLoopCallbackWrapper); return Q; }; // Pause the entire game by canceling the requestAnimationFrame call. If you use setTimeout or // setInterval in your game, those will, of course, keep on rolling... Q.pauseGame = function() { if(Q.loop) { window.cancelAnimationFrame(Q.loop); } Q.loop = null; }; // Unpause the game by restarting the requestAnimationFrame-based loop. Q.unpauseGame = function() { if(!Q.loop) { Q.lastGameLoopFrame = new Date().getTime(); Q.loop = window.requestAnimationFrame(Q.gameLoopCallbackWrapper); } }; // The base Class object // =============== // // Quintus uses the Simple JavaScript inheritance Class object, created by // John Resig and described on his blog: // // [http://ejohn.org/blog/simple-javascript-inheritance/](http://ejohn.org/blog/simple-javascript-inheritance/) // // The class is used wholesale, with the only differences being that instead // of appearing in a top-level namespace, the `Class` object is available as // `Q.Class` and a second argument on the `extend` method allows for adding // class level methods and the class name is passed in a parameter for introspection // purposes. // // Classes can be created by calling `Q.Class.extend(name,{ .. })`, although most of the time // you'll want to use one of the derivitive classes, `Q.Evented` or `Q.GameObject` which // have a little bit of functionality built-in. `Q.Evented` adds event binding and // triggering support and `Q.GameObject` adds support for components and a destroy method. // // The main things Q.Class get you are easy inheritance, a constructor method called `init()`, // dynamic addition of a this._super method when a method is overloaded (be careful with // this as it adds some overhead to method calls.) Calls to `instanceof` also all // work as you'd hope. // // By convention, classes should be added onto to the `Q` object and capitalized, so if // you wanted to create a new class for your game, you'd write: // // Q.Class.extend("MyClass",{ ... }); // // Examples: // // Q.Class.extend("Bird",{ // init: function(name) { this.name = name; }, // speak: function() { console.log(this.name); }, // fly: function() { console.log("Flying"); } // }); // // Q.Bird.extend("Penguin",{ // speak: function() { console.log(this.name + " the penguin"); }, // fly: function() { console.log("Can't fly, sorry..."); } // }); // // var randomBird = new Q.Bird("Frank"), // pengy = new Q.Penguin("Pengy"); // // randomBird.fly(); // Logs "Flying" // pengy.fly(); // Logs "Can't fly,sorry..." // // randomBird.speak(); // Logs "Frank" // pengy.speak(); // Logs "Pengy the penguin" // // console.log(randomBird instanceof Q.Bird); // true // console.log(randomBird instanceof Q.Penguin); // false // console.log(pengy instanceof Q.Bird); // true // console.log(pengy instanceof Q.Penguin); // true /* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed. * * Inspired by base2 and Prototype */ (function(){ var initializing = false, fnTest = /xyz/.test(function(){ var xyz;}) ? /\b_super\b/ : /.*/; /* The base Class implementation (does nothing) */ Q.Class = function(){}; // See if a object is a specific class Q.Class.prototype.isA = function(className) { return this.className === className; }; /* Create a new Class that inherits from this class */ Q.Class.extend = function(className, prop, classMethods) { /* No name, don't add onto Q */ if(!Q._isString(className)) { classMethods = prop; prop = className; className = null; } var _super = this.prototype, ThisClass = this; /* Instantiate a base class (but only create the instance, */ /* don't run the init constructor) */ initializing = true; var prototype = new ThisClass(); initializing = false; function _superFactory(name,fn) { return function() { var tmp = this._super; /* Add a new ._super() method that is the same method */ /* but on the super-class */ this._super = _super[name]; /* The method only need to be bound temporarily, so we */ /* remove it when we're done executing */ var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; } /* Copy the properties over onto the new prototype */ for (var name in prop) { /* Check if we're overwriting an existing function */ prototype[name] = typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name]) ? _superFactory(name,prop[name]) : prop[name]; } /* The dummy class constructor */ function Class() { /* All construction is actually done in the init method */ if ( !initializing && this.init ) { this.init.apply(this, arguments); } } /* Populate our constructed prototype object */ Class.prototype = prototype; /* Enforce the constructor to be what we expect */ Class.prototype.constructor = Class; /* And make this class extendable */ Class.extend = Q.Class.extend; /* If there are class-level Methods, add them to the class */ if(classMethods) { Q._extend(Class,classMethods); } if(className) { /* Save the class onto Q */ Q[className] = Class; /* Let the class know its name */ Class.prototype.className = className; Class.className = className; } return Class; }; }()); // Event Handling // ============== // The `Q.Evented` class adds event handling onto the base `Q.Class` // class. Evented objects can trigger events and other objects can // bind to those events. Q.Class.extend("Evented",{ // Binds a callback to an event on this object. If you provide a // `target` object, that object will add this event to it's list of // binds, allowing it to automatically remove it when it is destroyed. on: function(event,target,callback) { if(Q._isArray(event) || event.indexOf(",") !== -1) { event = Q._normalizeArg(event); for(var i=0;i=0;i--) { if(l[i][0] === target) { if(!callback || callback === l[i][1]) { this.listeners[event].splice(i,1); } } } } } }, // `debind` is called to remove any listeners an object had // on other objects. The most common case is when an object is // destroyed you'll want all the event listeners to be removed // for you. debind: function() { if(this.binds) { for(var i=0,len=this.binds.length;i resampleWidth) || (resampleHeight && h > resampleHeight)) && Q.touchDevice) { Q.el.style.height = h + "px"; Q.el.style.width = w + "px"; Q.el.width = w / 2; Q.el.height = h / 2; } else { Q.el.style.height = h + "px"; Q.el.style.width = w + "px"; Q.el.width = w; Q.el.height = h; } var elParent = Q.el.parentNode; if(elParent) { Q.wrapper = document.createElement("div"); Q.wrapper.id = id + '_container'; Q.wrapper.style.width = w + "px"; Q.wrapper.style.margin = "0 auto"; Q.wrapper.style.position = "relative"; elParent.insertBefore(Q.wrapper,Q.el); Q.wrapper.appendChild(Q.el); } Q.el.style.position = 'relative'; Q.ctx = Q.el.getContext && Q.el.getContext("2d"); Q.width = parseInt(Q.el.width,10); Q.height = parseInt(Q.el.height,10); Q.cssWidth = w; Q.cssHeight = h; window.addEventListener('orientationchange',function() { setTimeout(function() { window.scrollTo(0,1); }, 0); }); return Q; }; // Clear the canvas completely. Q.clear = function() { if(Q.clearColor) { Q.ctx.globalAlpha = 1; Q.ctx.fillStyle = Q.clearColor; Q.ctx.fillRect(0,0,Q.width,Q.height); } else { Q.ctx.clearRect(0,0,Q.width,Q.height); } }; // Return canvas image data given an Image object. Q.imageData = function(img) { var canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; var ctx = canvas.getContext("2d"); ctx.drawImage(img,0,0); return ctx.getImageData(0,0,img.width,img.height); }; // Asset Loading Support // ===================== // // The engine supports loading assets of different types using // `load` or `preload`. Assets are stored by their name so the // same asset won't be loaded twice if it already exists. // Augmentable list of asset types, loads a specific asset // type if the file type matches, otherwise defaults to a Ajax // load of the data. // // You can new types of assets based on file extension by // adding to `assetTypes` and adding a method called // loadAssetTYPENAME where TYPENAME is the name of the // type you added in. Q.assetTypes = { png: 'Image', jpg: 'Image', gif: 'Image', jpeg: 'Image', ogg: 'Audio', wav: 'Audio', m4a: 'Audio', mp3: 'Audio' }; // Determine the type of asset based on the lookup table above Q.assetType = function(asset) { /* Determine the lowercase extension of the file */ var fileParts = asset.split("."), fileExt = fileParts[fileParts.length-1].toLowerCase(); // Use the web audio loader instead of the regular loader // if it's supported. var fileType = Q.assetTypes[fileExt]; if(fileType === 'Audio' && Q.audio && Q.audio.type === "WebAudio") { fileType = 'WebAudio'; } /* Lookup the asset in the assetTypes hash, or return other */ return fileType || 'Other'; }; // Either return an absolute URL, // or add a base to a relative URL Q.assetUrl = function(base,url) { var timestamp = ""; if(Q.options.development) { timestamp = (/\?/.test(url) ? "&" : "?") + "_t=" +new Date().getTime(); } if(/^https?:\/\//.test(url) || url[0] === "/") { return url + timestamp; } else { return base + url + timestamp; } }; // Loader for Images, creates a new `Image` object and uses the // load callback to determine the image has been loaded Q.loadAssetImage = function(key,src,callback,errorCallback) { var img = new Image(); img.onload = function() { callback(key,img); }; img.onerror = errorCallback; img.src = Q.assetUrl(Q.options.imagePath,src); }; // List of mime types given an audio file extension, used to // determine what sound types the browser can play using the // built-in `Sound.canPlayType` Q.audioMimeTypes = { mp3: 'audio/mpeg', ogg: 'audio/ogg; codecs="vorbis"', m4a: 'audio/m4a', wav: 'audio/wav' }; Q._audioAssetExtension = function() { if(Q._audioAssetPreferredExtension) { return Q._audioAssetPreferredExtension; } var snd = new Audio(); /* Find a supported type */ return Q._audioAssetPreferredExtension = Q._detect(Q.options.audioSupported, function(extension) { return snd.canPlayType(Q.audioMimeTypes[extension]) ? extension : null; }); }; // Loader for Audio assets. By default chops off the extension and // will automatically determine which of the supported types is // playable by the browser and load that type. // // Which types are available are determined by the file extensions // listed in the Quintus `options.audioSupported` Q.loadAssetAudio = function(key,src,callback,errorCallback) { if(!document.createElement("audio").play || !Q.options.sound) { callback(key,null); return; } var baseName = Q._removeExtension(src), extension = Q._audioAssetExtension(), filename = null, snd = new Audio(); /* No supported audio = trigger ok callback anyway */ if(!extension) { callback(key,null); return; } snd.addEventListener("error",errorCallback); // Don't wait for canplaythrough on mobile if(!Q.touchDevice) { snd.addEventListener('canplaythrough',function() { callback(key,snd); }); } snd.src = Q.assetUrl(Q.options.audioPath,baseName + "." + extension); snd.load(); if(Q.touchDevice) { callback(key,snd); } }; Q.loadAssetWebAudio = function(key,src,callback,errorCallback) { var request = new XMLHttpRequest(), baseName = Q._removeExtension(src), extension = Q._audioAssetExtension(); request.open("GET", Q.assetUrl(Q.options.audioPath,baseName + "." + extension), true); request.responseType = "arraybuffer"; // Our asynchronous callback request.onload = function() { var audioData = request.response; Q.audioContext.decodeAudioData(request.response, function(buffer) { callback(key,buffer); }, errorCallback); }; request.send(); }; // Loader for other file types, just store the data // returned from an Ajax call. Q.loadAssetOther = function(key,src,callback,errorCallback) { var request = new XMLHttpRequest(); var fileParts = src.split("."), fileExt = fileParts[fileParts.length-1].toLowerCase(); request.onreadystatechange = function() { if(request.readyState === 4) { if(request.status === 200) { if(fileExt === 'json') { callback(key,JSON.parse(request.responseText)); } else { callback(key,request.responseText); } } else { errorCallback(); } } }; request.open("GET", Q.assetUrl(Q.options.dataPath,src), true); request.send(null); }; // Helper method to return a name without an extension Q._removeExtension = function(filename) { return filename.replace(/\.(\w{3,4})$/,""); }; // Asset hash storing any loaded assets Q.assets = {}; // Getter method to return an asset by its name. // // Asset names default to their filenames, but can be overridden // by passing a hash to `load` to set different names. Q.asset = function(name) { return Q.assets[name]; }; // Load assets, and call our callback when done. // // Also optionally takes a `progressCallback` which will be called // with the number of assets loaded and the total number of assets // to allow showing of a progress. // // Assets can be passed in as an array of file names, and Quintus // will use the file names as the name for reference, or as a hash of // `{ name: filename }`. // // Example usage: // Q.load(['sprites.png','sprites.,json'],function() { // Q.stageScene("level1"); // or something to start the game. // }); Q.load = function(assets,callback,options) { var assetObj = {}; /* Make sure we have an options hash to work with */ if(!options) { options = {}; } /* Get our progressCallback if we have one */ var progressCallback = options.progressCallback; var errors = false, errorCallback = function(itm) { errors = true; (options.errorCallback || function(itm) { throw("Error Loading: " + itm ); })(itm); }; /* Convert to an array if it's a string */ if(Q._isString(assets)) { assets = Q._normalizeArg(assets); } /* If the user passed in an array, convert it */ /* to a hash with lookups by filename */ if(Q._isArray(assets)) { Q._each(assets,function(itm) { if(Q._isObject(itm)) { Q._extend(assetObj,itm); } else { assetObj[itm] = itm; } }); } else { /* Otherwise just use the assets as is */ assetObj = assets; } /* Find the # of assets we're loading */ var assetsTotal = Q._keys(assetObj).length, assetsRemaining = assetsTotal; /* Closure'd per-asset callback gets called */ /* each time an asset is successfully loadded */ var loadedCallback = function(key,obj,force) { if(errors) { return; } // Prevent double callbacks (I'm looking at you Firefox, canplaythrough if(!Q.assets[key]||force) { /* Add the object to our asset list */ Q.assets[key] = obj; /* We've got one less asset to load */ assetsRemaining--; /* Update our progress if we have it */ if(progressCallback) { progressCallback(assetsTotal - assetsRemaining,assetsTotal); } } /* If we're out of assets, call our full callback */ /* if there is one */ if(assetsRemaining === 0 && callback) { /* if we haven't set up our canvas element yet, */ /* assume we're using a canvas with id 'quintus' */ callback.apply(Q); } }; /* Now actually load each asset */ Q._each(assetObj,function(itm,key) { /* Determine the type of the asset */ var assetType = Q.assetType(itm); /* If we already have the asset loaded, */ /* don't load it again */ if(Q.assets[key]) { loadedCallback(key,Q.assets[key],true); } else { /* Call the appropriate loader function */ /* passing in our per-asset callback */ /* Dropping our asset by name into Q.assets */ Q["loadAsset" + assetType](key,itm, loadedCallback, function() { errorCallback(itm); }); } }); }; // Array to store any assets that need to be // preloaded Q.preloads = []; // Let us gather assets to load at a later time, // and then preload them all at the same time with // a single callback. Options are passed through to the // Q.load method if used. // // Example usage: // Q.preload("sprites.png"); // ... // Q.preload("sprites.json"); // ... // // Q.preload(function() { // Q.stageScene("level1"); // or something to start the game // }); Q.preload = function(arg,options) { if(Q._isFunction(arg)) { Q.load(Q._uniq(Q.preloads),arg,options); Q.preloads = []; } else { Q.preloads = Q.preloads.concat(arg); } }; // Math Methods // ============== // // Math methods, for rotating and scaling points // A list of matrices available Q.matrices2d = []; Q.matrix2d = function() { return Q.matrices2d.length > 0 ? Q.matrices2d.pop().identity() : new Q.Matrix2D(); }; // A 2D matrix class, optimized for 2D points, // where the last row of the matrix will always be 0,0,1 // Good Docs where: // https://github.com/heygrady/transform/wiki/calculating-2d-matrices Q.Matrix2D = Q.Class.extend({ init: function(source) { if(source) { this.m = []; this.clone(source); } else { this.m = [1,0,0,0,1,0]; } }, // Turn this matrix into the identity identity: function() { var m = this.m; m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 1; m[5] = 0; return this; }, // Clone another matrix into this one clone: function(matrix) { var d = this.m, s = matrix.m; d[0]=s[0]; d[1]=s[1]; d[2] = s[2]; d[3]=s[3]; d[4]=s[4]; d[5] = s[5]; return this; }, // a * b = // [ [ a11*b11 + a12*b21 ], [ a11*b12 + a12*b22 ], [ a11*b31 + a12*b32 + a13 ] , // [ a21*b11 + a22*b21 ], [ a21*b12 + a22*b22 ], [ a21*b31 + a22*b32 + a23 ] ] multiply: function(matrix) { var a = this.m, b = matrix.m; var m11 = a[0]*b[0] + a[1]*b[3]; var m12 = a[0]*b[1] + a[1]*b[4]; var m13 = a[0]*b[2] + a[1]*b[5] + a[2]; var m21 = a[3]*b[0] + a[4]*b[3]; var m22 = a[3]*b[1] + a[4]*b[4]; var m23 = a[3]*b[2] + a[4]*b[5] + a[5]; a[0]=m11; a[1]=m12; a[2] = m13; a[3]=m21; a[4]=m22; a[5] = m23; return this; }, // Multiply this matrix by a rotation matrix rotated radians radians rotate: function(radians) { if(radians === 0) { return this; } var cos = Math.cos(radians), sin = Math.sin(radians), m = this.m; var m11 = m[0]*cos + m[1]*sin; var m12 = m[0]*-sin + m[1]*cos; var m21 = m[3]*cos + m[4]*sin; var m22 = m[3]*-sin + m[4]*cos; m[0] = m11; m[1] = m12; // m[2] == m[2] m[3] = m21; m[4] = m22; // m[5] == m[5] return this; }, // Helper method to rotate by a set number of degrees rotateDeg: function(degrees) { if(degrees === 0) { return this; } return this.rotate(Math.PI * degrees / 180); }, // Multiply this matrix by a scaling matrix scaling sx and sy scale: function(sx,sy) { var m = this.m; if(sy === void 0) { sy = sx; } m[0] *= sx; m[1] *= sy; m[3] *= sx; m[4] *= sy; return this; }, // Multiply this matrix by a translation matrix translate by tx and ty translate: function(tx,ty) { var m = this.m; m[2] += m[0]*tx + m[1]*ty; m[5] += m[3]*tx + m[4]*ty; return this; }, // Memory Hoggy version transform: function(x,y) { return [ x * this.m[0] + y * this.m[1] + this.m[2], x * this.m[3] + y * this.m[4] + this.m[5] ]; }, // Transform an object with an x and y property by this Matrix transformPt: function(obj) { var x = obj.x, y = obj.y; obj.x = x * this.m[0] + y * this.m[1] + this.m[2]; obj.y = x * this.m[3] + y * this.m[4] + this.m[5]; return obj; }, // Transform an array with an x and y property by this Matrix transformArr: function(inArr,outArr) { var x = inArr[0], y = inArr[1]; outArr[0] = x * this.m[0] + y * this.m[1] + this.m[2]; outArr[1] = x * this.m[3] + y * this.m[4] + this.m[5]; return outArr; }, // Return just the x component by this Matrix transformX: function(x,y) { return x * this.m[0] + y * this.m[1] + this.m[2]; }, // Return just the y component by this Matrix transformY: function(x,y) { return x * this.m[3] + y * this.m[4] + this.m[5]; }, // Release this Matrix to be reused release: function() { Q.matrices2d.push(this); return null; }, setContextTransform: function(ctx) { var m = this.m; // source: // m[0] m[1] m[2] // m[3] m[4] m[5] // 0 0 1 // // destination: // m11 m21 dx // m12 m22 dy // 0 0 1 // setTransform(m11, m12, m21, m22, dx, dy) ctx.transform(m[0],m[3],m[1],m[4],m[2],m[5]); //ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]); } }); // And that's it.. // =============== // // Return the `Q` object from the `Quintus()` factory method. Create awesome games. Repeat. return Q; }; // Lastly, add in the `requestAnimationFrame` shim, if necessary. Does nothing // if `requestAnimationFrame` is already on the `window` object. (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) { window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } }()); /*global Quintus:false */ Quintus["2D"] = function(Q) { Q.component('viewport',{ added: function() { this.entity.on('prerender',this,'prerender'); this.entity.on('render',this,'postrender'); this.x = 0; this.y = 0; this.offsetX = 0; this.offsetY = 0; this.centerX = Q.width/2; this.centerY = Q.height/2; this.scale = 1; }, extend: { follow: function(sprite,directions) { this.off('poststep',this.viewport,'follow'); this.viewport.directions = directions || { x: true, y: true }; this.viewport.following = sprite; this.on('poststep',this.viewport,'follow'); this.viewport.follow(true); }, unfollow: function() { this.off('poststep',this.viewport,'follow'); }, centerOn: function(x,y) { this.viewport.centerOn(x,y); }, moveTo: function(x,y) { return this.viewport.moveTo(x,y); } }, follow: function(first) { var followX = Q._isFunction(this.directions.x) ? this.directions.x(this.following) : this.directions.x; var followY = Q._isFunction(this.directions.y) ? this.directions.y(this.following) : this.directions.y; this[first === true ? 'centerOn' : 'softCenterOn']( followX ? this.following.p.x + this.following.p.w/2 - this.offsetX : undefined, followY ? this.following.p.y + this.following.p.h/2 - this.offsetY : undefined ); }, offset: function(x,y) { this.offsetX = x; this.offsetY = y; }, softCenterOn: function(x,y) { if(x !== void 0) { this.x += (x - Q.width / 2 / this.scale - this.x)/3; } if(y !== void 0) { this.y += (y - Q.height / 2 / this.scale - this.y)/3; } }, centerOn: function(x,y) { if(x !== void 0) { this.x = x - Q.width / 2 / this.scale; } if(y !== void 0) { this.y = y - Q.height / 2 / this.scale; } }, moveTo: function(x,y) { if(x !== void 0) { this.x = x; } if(y !== void 0) { this.y = y; } return this.entity; }, prerender: function() { this.centerX = this.x + Q.width / 2 /this.scale; this.centerY = this.y + Q.height / 2 /this.scale; Q.ctx.save(); Q.ctx.translate(Math.floor(Q.width/2),Math.floor(Q.height/2)); Q.ctx.scale(this.scale,this.scale); Q.ctx.translate(-Math.floor(this.centerX), -Math.floor(this.centerY)); }, postrender: function() { Q.ctx.restore(); } }); Q.TileLayer = Q.Sprite.extend({ init: function(props) { this._super(props,{ tileW: 32, tileH: 32, blockTileW: 10, blockTileH: 10, type: 1, layerIndex: 0 }); if(this.p.dataAsset) { this.load(this.p.dataAsset); } this.blocks = []; this.p.blockW = this.p.tileW * this.p.blockTileW; this.p.blockH = this.p.tileH * this.p.blockTileH; this.colBounds = {}; this.directions = [ 'top','left','right','bottom']; this.collisionObject = { p: { w: this.p.tileW, h: this.p.tileH, cx: this.p.tileW/2, cy: this.p.tileH/2 } }; this.collisionNormal = { separate: []}; }, load: function(dataAsset) { var fileParts = dataAsset.split("."), fileExt = fileParts[fileParts.length-1].toLowerCase(); if (fileExt == "json") { var data = Q._isString(dataAsset) ? Q.asset(dataAsset) : dataAsset; } else if (fileExt == "tmx" || fileExt == "xml") { var parser = new DOMParser(), doc = parser.parseFromString(Q.asset(dataAsset), "application/xml"); var layer = doc.getElementsByTagName("layer")[this.p.layerIndex], width = parseInt(layer.getAttribute("width")), height = parseInt(layer.getAttribute("height")); var data = [], tiles = layer.getElementsByTagName("tile"), idx = 0; for(var y = 0;y < height;y++) { data[y] = []; for(var x = 0;x < width;x++) { var tile = tiles[idx]; data[y].push(parseInt(tile.getAttribute("gid")-1)); idx++; } } } else { throw "file type not supported"; } this.p.tiles = data; this.p.rows = data.length; this.p.cols = data[0].length; this.p.w = this.p.cols * this.p.tileW; this.p.h = this.p.rows * this.p.tileH; }, getTile: function(tileX,tileY) { return this.p.tiles[tileY] && this.p.tiles[tileY][tileX]; }, setTile: function(x,y,tile) { var p = this.p, blockX = Math.floor(x/p.blockTileW), blockY = Math.floor(y/p.blockTileH); if(blockX >= 0 && blockY >= 0 && blockX < this.p.cols && blockY < this.p.cols) { this.p.tiles[y][x] = tile; if(this.blocks[blockY]) { this.blocks[blockY][blockX] = null; } } }, tilePresent: function(tileX,tileY) { return this.p.tiles[tileY] && this.collidableTile(this.p.tiles[tileY][tileX]); }, // Overload this method to draw tiles at frame 0 or not draw // tiles at higher number frames drawableTile: function(tileNum) { return tileNum > 0; }, // Overload this method to control which tiles trigger a collision // (defaults to all tiles > number 0) collidableTile: function(tileNum) { return tileNum > 0; }, collide: function(obj) { var p = this.p, tileStartX = Math.floor((obj.p.x - obj.p.cx - p.x) / p.tileW), tileStartY = Math.floor((obj.p.y - obj.p.cy - p.y) / p.tileH), tileEndX = Math.ceil((obj.p.x - obj.p.cx + obj.p.w - p.x) / p.tileW), tileEndY = Math.ceil((obj.p.y - obj.p.cy + obj.p.h - p.y) / p.tileH), colObj = this.collisionObject, normal = this.collisionNormal, col; normal.collided = false; for(var tileY = tileStartY; tileY<=tileEndY; tileY++) { for(var tileX = tileStartX; tileX<=tileEndX; tileX++) { if(this.tilePresent(tileX,tileY)) { colObj.p.x = tileX * p.tileW + p.x + p.tileW/2; colObj.p.y = tileY * p.tileH + p.y + p.tileH/2; col = Q.collision(obj,colObj); if(col && col.magnitude > 0 && (!normal.collided || normal.magnitude < col.magnitude )) { normal.collided = true; normal.separate[0] = col.separate[0]; normal.separate[1] = col.separate[1]; normal.magnitude = col.magnitude; normal.distance = col.distance; normal.normalX = col.normalX; normal.normalY = col.normalY; normal.tileX = tileX; normal.tileY = tileY; normal.tile = this.getTile(tileX,tileY); } } } } return normal.collided ? normal : false; }, prerenderBlock: function(blockX,blockY) { var p = this.p, tiles = p.tiles, sheet = this.sheet(), blockOffsetX = blockX*p.blockTileW, blockOffsetY = blockY*p.blockTileH; if(blockOffsetX < 0 || blockOffsetX >= this.p.cols || blockOffsetY < 0 || blockOffsetY >= this.p.rows) { return; } var canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'); canvas.width = p.blockW; canvas.height= p.blockH; this.blocks[blockY] = this.blocks[blockY] || {}; this.blocks[blockY][blockX] = canvas; for(var y=0;y 0) { p.vy = 0; } col.impact = impactY; entity.trigger("bump.bottom",col); } if(col.normalY > 0.3) { if(p.vy < 0) { p.vy = 0; } col.impact = impactY; entity.trigger("bump.top",col); } if(col.normalX < -0.3) { if(p.vx > 0) { p.vx = 0; } col.impact = impactX; entity.trigger("bump.right",col); } if(col.normalX > 0.3) { if(p.vx < 0) { p.vx = 0; } col.impact = impactX; entity.trigger("bump.left",col); } }, step: function(dt) { var p = this.entity.p, dtStep = dt; // TODO: check the entity's magnitude of vx and vy, // reduce the max dtStep if necessary to prevent // skipping through objects. while(dtStep > 0) { dt = Math.min(1/30,dtStep); // Updated based on the velocity and acceleration p.vx += p.ax * dt + (p.gravityX == void 0 ? Q.gravityX : p.gravityX) * dt * p.gravity; p.vy += p.ay * dt + (p.gravityY == void 0 ? Q.gravityY : p.gravityY) * dt * p.gravity; p.x += p.vx * dt; p.y += p.vy * dt; this.entity.stage.collide(this.entity); dtStep -= dt; } } }); Q.component('aiBounce', { added: function() { this.entity.on("bump.right",this,"goLeft"); this.entity.on("bump.left",this,"goRight"); }, goLeft: function(col) { this.entity.p.vx = -col.impact; }, goRight: function(col) { this.entity.p.vx = col.impact; } }); }; /*global Quintus:false */ Quintus.Anim = function(Q) { Q._animations = {}; Q.animations = function(sprite,animations) { if(!Q._animations[sprite]) { Q._animations[sprite] = {}; } Q._extend(Q._animations[sprite],animations); }; Q.animation = function(sprite,name) { return Q._animations[sprite] && Q._animations[sprite][name]; }; Q.component('animation',{ added: function() { var p = this.entity.p; p.animation = null; p.animationPriority = -1; p.animationFrame = 0; p.animationTime = 0; this.entity.on("step",this,"step"); }, extend: { play: function(name,priority) { this.animation.play(name,priority); } }, step: function(dt) { var entity = this.entity, p = entity.p; if(p.animation) { var anim = Q.animation(p.sprite,p.animation), rate = anim.rate || p.rate, stepped = 0; p.animationTime += dt; if(p.animationChanged) { p.animationChanged = false; } else { p.animationTime += dt; if(p.animationTime > rate) { stepped = Math.floor(p.animationTime / rate); p.animationTime -= stepped * rate; p.animationFrame += stepped; } } if(stepped > 0) { if(p.animationFrame >= anim.frames.length) { if(anim.loop === false || anim.next) { p.animationFrame = anim.frames.length - 1; entity.trigger('animEnd'); entity.trigger('animEnd.' + p.animation); p.animation = null; p.animationPriority = -1; if(anim.trigger) { entity.trigger(anim.trigger,anim.triggerData); } if(anim.next) { this.play(anim.next,anim.nextPriority); } return; } else { entity.trigger('animLoop'); entity.trigger('animLoop.' + p.animation); p.animationFrame = p.animationFrame % anim.frames.length; } } entity.trigger("animFrame"); } p.sheet = anim.sheet || p.sheet; p.frame = anim.frames[p.animationFrame]; } }, play: function(name,priority) { var entity = this.entity, p = entity.p; priority = priority || 0; if(name !== p.animation && priority >= p.animationPriority) { p.animation = name; p.animationChanged = true; p.animationTime = 0; p.animationFrame = 0; p.animationPriority = priority; entity.trigger('anim'); entity.trigger('anim.' + p.animation); } } }); Q.Sprite.extend("Repeater",{ init: function(props) { this._super(Q._defaults(props,{ speedX: 1, speedY: 1, repeatY: true, repeatX: true, type: 0 })); this.p.repeatW = this.p.repeatW || this.p.w; this.p.repeatH = this.p.repeatH || this.p.h; }, draw: function(ctx) { var p = this.p, asset = this.asset(), sheet = this.sheet(), scale = this.stage.viewport ? this.stage.viewport.scale : 1, viewX = this.stage.viewport ? this.stage.viewport.x : 0, viewY = this.stage.viewport ? this.stage.viewport.y : 0, offsetX = p.x + viewX * this.p.speedX, offsetY = p.y + viewY * this.p.speedY, curX, curY, startX; if(p.repeatX) { curX = Math.floor(-offsetX % p.repeatW); if(curX > 0) { curX -= p.repeatW; } } else { curX = p.x - viewX; } if(p.repeatY) { curY = Math.floor(-offsetY % p.repeatH); if(curY > 0) { curY -= p.repeatH; } } else { curY = p.y - viewY; } startX = curX; while(curY < Q.height / scale) { curX = startX; while(curX < Q.width / scale) { if(sheet) { sheet.draw(ctx,Math.floor(curX + viewX), Math.floor(curY + viewY),p.frame); } else { ctx.drawImage(asset,Math.floor(curX + viewX),Math.floor(curY + viewY)); } curX += p.repeatW; if(!p.repeatX) { break; } } curY += p.repeatH; if(!p.repeatY) { break; } } } }); Q.Tween = Q.Class.extend({ init: function(entity,properties,duration,easing,options) { if(Q._isObject(easing)) { options = easing; easing = Q.Easing.Linear; } if(Q._isObject(duration)) { options = duration; duration = 1; } this.entity = entity; //this.p = (entity instanceof Q.Stage) ? entity.viewport : entity.p; this.duration = duration || 1; this.time = 0; this.options = options || {}; this.delay = this.options.delay || 0; this.easing = easing || this.options.easing || Q.Easing.Linear; this.startFrame = Q._loopFrame + 1; this.properties = properties; this.start = {}; this.diff = {}; }, step: function(dt) { var property; if(this.startFrame > Q._loopFrame) { return true; } if(this.delay >= dt) { this.delay -= dt; return true; } if(this.delay > 0) { dt -= this.delay; this.delay = 0; } if(this.time === 0) { // first time running? Initialize the properties to chaining correctly. var entity = this.entity, properties = this.properties; this.p = (entity instanceof Q.Stage) ? entity.viewport : entity.p; for(property in properties) { this.start[property] = this.p[property]; if(!Q._isUndefined(this.start[property])) { this.diff[property] = properties[property] - this.start[property]; } } } this.time += dt; var progress = Math.min(1,this.time / this.duration), location = this.easing(progress); for(property in this.start) { if(!Q._isUndefined(this.p[property])) { this.p[property] = this.start[property] + this.diff[property] * location; } } if(progress >= 1) { if(this.options.callback) { this.options.callback.apply(this.entity); } } return progress < 1; } }); // Code ripped directly from Tween.js // https://github.com/sole/tween.js/blob/master/src/Tween.js Q.Easing = { Linear: function (k) { return k; }, Quadratic: { In: function ( k ) { return k * k; }, Out: function ( k ) {return k * ( 2 - k ); }, InOut: function ( k ) { if ((k *= 2 ) < 1) { return 0.5 * k * k; } return -0.5 * (--k * (k - 2) - 1); } } }; Q.component('tween',{ added: function() { this._tweens = []; this.entity.on("step",this,"step"); }, extend: { animate: function(properties,duration,easing,options) { this.tween._tweens.push(new Q.Tween(this,properties,duration,easing,options)); return this; }, chain: function(properties,duration,easing,options) { if(Q._isObject(easing)) { options = easing; easing = Q.Easing.Linear; } // Chain an animation to the end var tweenCnt = this.tween._tweens.length; if(tweenCnt > 0) { var lastTween = this.tween._tweens[tweenCnt - 1]; options = options || {}; options['delay'] = lastTween.duration - lastTween.time + lastTween.delay; } this.animate(properties,duration,easing,options); return this; }, stop: function() { this.tween._tweens.length = 0; return this; } }, step: function(dt) { for(var i=0; i < this._tweens.length; i++) { if(!this._tweens[i].step(dt)) { this._tweens.splice(i,1); i--; } } } }); }; /*global Quintus:false, AudioContext:false, window:false */ Quintus.Audio = function(Q) { Q.audio = { channels: [], channelMax: Q.options.channelMax || 10, active: {}, play: function() {} }; Q.hasWebAudio = (typeof AudioContext !== "undefined") || (typeof webkitAudioContext !== "undefined"); if(Q.hasWebAudio) { if(typeof AudioContext !== "undefined") { Q.audioContext = new AudioContext(); } else { Q.audioContext = new window.webkitAudioContext(); } } Q.enableSound = function() { var hasTouch = !!('ontouchstart' in window); if(Q.hasWebAudio) { Q.audio.enableWebAudioSound(); } else { Q.audio.enableHTML5Sound(); } return Q; }; Q.audio.enableWebAudioSound = function() { Q.audio.type = "WebAudio"; Q.audio.soundID = 0; Q.audio.playingSounds = {}; Q.audio.removeSound = function(soundID) { delete Q.audio.playingSounds[soundID]; }; // Play a single sound, optionally debounced // to prevent repeated plays in a short time Q.audio.play = function(s,options) { var now = new Date().getTime(); // See if this audio file is currently being debounced, if // it is, don't do anything and just return if(Q.audio.active[s] && Q.audio.active[s] > now) { return; } // If any options were passed in, check for a debounce, // which is the number of milliseconds to debounce this sound if(options && options['debounce']) { Q.audio.active[s] = now + options['debounce']; } else { delete Q.audio.active[s]; } var soundID = Q.audio.soundID++; var source = Q.audioContext.createBufferSource(); source.buffer = Q.asset(s); source.connect(Q.audioContext.destination); if(options && options['loop']) { source.loop = true; } else { setTimeout(function() { Q.audio.removeSound(soundID); },source.buffer.duration * 1000); } source.assetName = s; if(source.start) { source.start(0); } else { source.noteOn(0); } Q.audio.playingSounds[soundID] = source; }; Q.audio.stop = function(s) { for(var key in Q.audio.playingSounds) { var snd = Q.audio.playingSounds[key]; if(!s || s === snd.assetName) { if(snd.stop) { snd.stop(0); } else { snd.noteOff(0); } } } }; }; Q.audio.enableHTML5Sound = function() { Q.audio.type = "HTML5"; for (var i=0;i now) { return; } // If any options were passed in, check for a debounce, // which is the number of milliseconds to debounce this sound if(options && options['debounce']) { Q.audio.active[s] = now + options['debounce']; } else { delete Q.audio.active[s]; } // Find a free audio channel and play the sound for (var i=0;i= tm)) { Q.audio.channels[i]['channel'].pause(); Q.audio.channels[i]['loop'] = false; } } }; }; }; /*global Quintus:false */ Quintus.Input = function(Q) { var KEY_NAMES = { LEFT: 37, RIGHT: 39, SPACE: 32, UP: 38, DOWN: 40, Z: 90, X: 88 }; var DEFAULT_KEYS = { LEFT: 'left', RIGHT: 'right', UP: 'up', DOWN: 'down', SPACE: 'fire', Z: 'fire', X: 'action' }; var DEFAULT_TOUCH_CONTROLS = [ ['left','<' ], ['right','>' ], [], ['action','b'], ['fire', 'a' ]]; // Clockwise from midnight (a la CSS) var DEFAULT_JOYPAD_INPUTS = [ 'up','right','down','left']; Q.inputs = {}; Q.joypad = {}; var hasTouch = !!('ontouchstart' in window); // Convert a canvas point to a stage point, x dimension Q.canvasToStageX = function(x,stage) { x = x / Q.cssWidth * Q.width; if(stage.viewport) { x /= stage.viewport.scale; x += stage.viewport.x; } return x; }; Q.canvasToStageY = function(y,stage) { y = y / Q.cssWidth * Q.width; if(stage.viewport) { y /= stage.viewport.scale; y += stage.viewport.y; } return y; }; Q.InputSystem = Q.Evented.extend({ keys: {}, keypad: {}, keyboardEnabled: false, touchEnabled: false, joypadEnabled: false, bindKey: function(key,name) { Q.input.keys[KEY_NAMES[key] || key] = name; }, keyboardControls: function(keys) { keys = keys || DEFAULT_KEYS; Q._each(keys,function(name,key) { this.bindKey(key,name); },Q.input); this.enableKeyboard(); }, enableKeyboard: function() { if(this.keyboardEnabled) { return false; } // Make selectable and remove an :focus outline Q.el.tabIndex = 0; Q.el.style.outline = 0; Q.el.addEventListener("keydown",function(e) { if(Q.input.keys[e.keyCode]) { var actionName = Q.input.keys[e.keyCode]; Q.inputs[actionName] = true; Q.input.trigger(actionName); Q.input.trigger('keydown',e.keyCode); } e.preventDefault(); },false); Q.el.addEventListener("keyup",function(e) { if(Q.input.keys[e.keyCode]) { var actionName = Q.input.keys[e.keyCode]; Q.inputs[actionName] = false; Q.input.trigger(actionName + "Up"); Q.input.trigger('keyup',e.keyCode); } e.preventDefault(); },false); Q.el.focus(); this.keyboardEnabled = true; }, _containerOffset: function() { Q.input.offsetX = 0; Q.input.offsetY = 0; var el = Q.el; do { Q.input.offsetX += el.offsetLeft; Q.input.offsetY += el.offsetTop; } while(el = el.offsetParent); }, touchLocation: function(touch) { var el = Q.el, posX = touch.offsetX, posY = touch.offsetY, touchX, touchY; if(Q._isUndefined(posX) || Q._isUndefined(posY)) { posX = touch.layerX; posY = touch.layerY; } if(Q._isUndefined(posX) || Q._isUndefined(posY)) { if(Q.input.offsetX === void 0) { Q.input._containerOffset(); } posX = touch.pageX - Q.input.offsetX; posY = touch.pageY - Q.input.offsetY; } touchX = Q.width * posX / Q.cssWidth; touchY = Q.height * posY / Q.cssHeight; return { x: touchX, y: touchY }; }, touchControls: function(opts) { if(this.touchEnabled) { return false; } if(!hasTouch) { return false; } Q.input.keypad = opts = Q._extend({ left: 0, gutter:10, controls: DEFAULT_TOUCH_CONTROLS, width: Q.width, bottom: Q.height },opts); opts.unit = (opts.width / opts.controls.length); opts.size = opts.unit - 2 * opts.gutter; function getKey(touch) { var pos = Q.input.touchLocation(touch); for(var i=0,len=opts.controls.length;i 1) { dx /= overage; dy /= overage; dist /= overage; } var triggers = [ dy < -joypad.trigger, dx > joypad.trigger, dy > joypad.trigger, dx < -joypad.trigger ]; for(var k=0;k 0 && (Q.inputs['up'] || Q.inputs['action'])) { p.vy = p.jumpSpeed; p.landed = -dt; } p.landed -= dt; } }); Q.component("stepControls", { added: function() { var p = this.entity.p; if(!p.stepDistance) { p.stepDistance = 32; } if(!p.stepDelay) { p.stepDelay = 0.2; } p.stepWait = 0; this.entity.on("step",this,"step"); this.entity.on("hit", this,"collision"); }, collision: function(col) { var p = this.entity.p; if(p.stepping) { p.stepping = false; p.x = p.origX; p.y = p.origY; } }, step: function(dt) { var p = this.entity.p, moved = false; p.stepWait -= dt; if(p.stepping) { p.x += p.diffX * dt / p.stepDelay; p.y += p.diffY * dt / p.stepDelay; } if(p.stepWait > 0) { return; } if(p.stepping) { p.x = p.destX; p.y = p.destY; } p.stepping = false; p.diffX = 0; p.diffY = 0; if(Q.inputs['left']) { p.diffX = -p.stepDistance; } else if(Q.inputs['right']) { p.diffX = p.stepDistance; } if(Q.inputs['up']) { p.diffY = -p.stepDistance; } else if(Q.inputs['down']) { p.diffY = p.stepDistance; } if(p.diffY || p.diffX ) { p.stepping = true; p.origX = p.x; p.origY = p.y; p.destX = p.x + p.diffX; p.destY = p.y + p.diffY; p.stepWait = p.stepDelay; } } }); }; /*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/ // TODO: handle angles on objects 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.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(stage.options.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; } if(!item.p.type && !skipAdd) { 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); } } }, 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