commit 4ce8e0671960e214b8bdd8bc2a61d2b6c8fa506b Author: neingeist Date: Sun Nov 24 20:50:08 2013 +0100 initial commit - quintus example platformer diff --git a/data/level.json b/data/level.json new file mode 100644 index 0000000..60a7181 --- /dev/null +++ b/data/level.json @@ -0,0 +1,16 @@ +[ +[ 1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0], +[ 1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0], +[ 1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0], +[ 1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,1,1,1], +[ 1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,2,1,1], +[ 1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,2,1,1], +[ 1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,1,1], +[ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], +[ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], +[ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], +[ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], +[ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], +[ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], +[ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +] diff --git a/data/sprites.json b/data/sprites.json new file mode 100644 index 0000000..c3d2ae4 --- /dev/null +++ b/data/sprites.json @@ -0,0 +1 @@ +{"player":{"sx":0,"sy":0,"cols":1,"tilew":30,"tileh":30,"frames":1},"enemy":{"sx":0,"sy":30,"cols":1,"tilew":30,"tileh":24,"frames":1},"tower":{"sx":0,"sy":54,"cols":1,"tilew":30,"tileh":30,"frames":1}} diff --git a/images/background-wall.png b/images/background-wall.png new file mode 100644 index 0000000..4f62569 Binary files /dev/null and b/images/background-wall.png differ diff --git a/images/sprites.png b/images/sprites.png new file mode 100644 index 0000000..6c85f3d Binary files /dev/null and b/images/sprites.png differ diff --git a/images/tiles.png b/images/tiles.png new file mode 100644 index 0000000..9d1831d Binary files /dev/null and b/images/tiles.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cd07a0b --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + +Platformer Example + + + + + + + + + + + + + + + diff --git a/lib/quintus.js b/lib/quintus.js new file mode 100644 index 0000000..b487d0a --- /dev/null +++ b/lib/quintus.js @@ -0,0 +1,1599 @@ +// 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. + +@module Quintus +*/ + +/** + 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(); + +@class 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 }); + + @method Q + @for Quintus + */ + var Q = function(selector,scope,options) { + return Q.select(selector,scope,options); + }; + + /** + Default no-op select method. Replaced with the Quintus.Scene class + + @method Q.select + @for Quintus + */ + 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 + @class Q.Class + @for Quintus + */ + (function(){ + var initializing = false, + fnTest = /xyz/.test(function(){ var xyz;}) ? /\b_super\b/ : /.*/; + /** The base Class implementation (does nothing) + * + * @constructor + * @for Q.Class + */ + Q.Class = function(){}; + + /** + * See if a object is a specific class + * + * @method isA + */ + Q.Class.prototype.isA = function(className) { + return this.className === className; + }; + + /** + * Create a new Class that inherits from this class + * + * @method extend + */ + 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. Q.Evented objects can trigger events and other objects can + bind to those events. + + @class Q.Evented + @for Quintus + */ + 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. + + @method on + @for Q.Evented + */ + 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); + }; + } +}()); + + diff --git a/lib/quintus_2d.js b/lib/quintus_2d.js new file mode 100644 index 0000000..be6b64a --- /dev/null +++ b/lib/quintus_2d.js @@ -0,0 +1,457 @@ +/*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,boundingBox) { + this.off('poststep',this.viewport,'follow'); + this.viewport.directions = directions || { x: true, y: true }; + this.viewport.following = sprite; + this.viewport.boundingBox = boundingBox; + 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) { + var dx = (x - Q.width / 2 / this.scale - this.x)/3; + if(this.boundingBox) { + if(this.x + dx < this.boundingBox.minX) { + this.x = this.boundingBox.minX / this.scale; + } + else if(this.x + dx > (this.boundingBox.maxX - Q.width) / this.scale) { + this.x = (this.boundingBox.maxX - Q.width) / this.scale; + } + else { + this.x += dx; + } + } + else { + this.x += dx; + } + } + if(y !== void 0) { + var dy = (y - Q.height / 2 / this.scale - this.y)/3; + if(this.boundingBox) { + if(this.y + dy < this.boundingBox.minY) { + this.y = this.boundingBox.minY / this.scale; + } + else if(this.y + dy > (this.boundingBox.maxY - Q.height) / this.scale) { + this.y = (this.boundingBox.maxY - Q.height) / this.scale; + } + else { + this.y += dy; + } + } + else { + this.y += dy; + } + } + + }, + 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(), + data; + + if (fileExt === "json") { + 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"),10), + height = parseInt(layer.getAttribute("height"),10); + + var tiles = layer.getElementsByTagName("tile"), + idx = 0; + + data = []; + + 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,10)); + 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.rows) { + 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; + if(this.entity.p.defaultDirection === 'right') { + this.entity.p.flip = 'x'; + } + else { + this.entity.p.flip = false; + } + }, + + goRight: function(col) { + this.entity.p.vx = col.impact; + if(this.entity.p.defaultDirection === 'left') { + this.entity.p.flip = 'x'; + } + else { + this.entity.p.flip = false; + } + } + }); + +}; + diff --git a/lib/quintus_anim.js b/lib/quintus_anim.js new file mode 100644 index 0000000..f6d44ab --- /dev/null +++ b/lib/quintus_anim.js @@ -0,0 +1,267 @@ +/*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 = Math.floor(this.stage.viewport ? this.stage.viewport.x : 0), + viewY = Math.floor(this.stage.viewport ? this.stage.viewport.y : 0), + offsetX = Math.floor(p.x + viewX * this.p.speedX), + offsetY = Math.floor(p.y + viewY * this.p.speedY), + curX, curY, startX; + if(p.repeatX) { + curX = -offsetX % p.repeatW; + if(curX > 0) { curX -= p.repeatW; } + } else { + curX = p.x - viewX; + } + if(p.repeatY) { + curY = -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,curX + viewX,curY + viewY,p.frame); + } else { + ctx.drawImage(asset,curX + viewX,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--; + } + } + } + }); + + +}; + diff --git a/lib/quintus_audio.js b/lib/quintus_audio.js new file mode 100644 index 0000000..66e1e58 --- /dev/null +++ b/lib/quintus_audio.js @@ -0,0 +1,157 @@ +/*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; + } + } + }; + + }; + +}; + diff --git a/lib/quintus_input.js b/lib/quintus_input.js new file mode 100644 index 0000000..c8edf37 --- /dev/null +++ b/lib/quintus_input.js @@ -0,0 +1,664 @@ +/*global Quintus:false */ +/** +Quintus HTML5 Game Engine - Input Module + +The code in `quintus_input.js` defines the `Quintus.Input` module, which +concerns itself with game-type (pretty anything besides touchscreen input) + +@module Quintus.Input +*/ + +/** + * Quintus Input class + * + * @class Quintus.Input + */ +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 + * + * @method Q.canvasToStageX + */ + Q.canvasToStageX = function(x,stage) { + x = x / Q.cssWidth * Q.width; + if(stage.viewport) { + x /= stage.viewport.scale; + x += stage.viewport.x; + } + + return x; + }; + + /** + * + * Convert a canvas point to a stage point, y dimension + * + * @method Q.canvasToStageY + */ + Q.canvasToStageY = function(y,stage) { + y = y / Q.cssWidth * Q.width; + if(stage.viewport) { + y /= stage.viewport.scale; + y += stage.viewport.y; + } + + return y; + }; + + + + /** + * + * Button and mouse input subsystem for Quintus. + * An instance of this class is auto-created as {{#crossLink "Q.input"}}{{/crossLink}} + * + * @class InputSystem + * @extends Evented + * @for Quintus.Input + */ + 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; + } + + } + + }); +}; + diff --git a/lib/quintus_scenes.js b/lib/quintus_scenes.js new file mode 100644 index 0000000..8ae2f3e --- /dev/null +++ b/lib/quintus_scenes.js @@ -0,0 +1,909 @@ +/*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