From 2d0be9c287e2eac1f4c426047cf53d1a3e87132e Mon Sep 17 00:00:00 2001 From: Dmitry Gammel Date: Tue, 28 Jul 2026 12:05:01 +0500 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=B3=D1=80=D0=BE=D0=B2=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20PvP:=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80=20(C++)=20+=20=D0=BA=D0=BB=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20(Quintus.js)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сервер: Game — authoritative-тик 20 Гц через uSockets timer, движение/коллизии танков и пуль, разрушаемые кирпичные стены, respawn, победа/поражение/ничья, технический проигрыш при дисконнекте. game.ready запускает матч, когда оба игрока в pvp-комнате готовы. Клиент: заведён Quintus.js (MIT, vendored) — Tank/Bullet/Wall спрайты, рендер по снапшотам game.state, keyboard-инпут (WASD/стрелки/space), экран результата матча. Найден и исправлен use-after-free: колбэк onFinished_ синхронно удалял Game изнутри его же метода — перенесено на uWS::Loop::defer. --- frontend/css/game.css | 31 + frontend/game.html | 9 +- frontend/js/game/battle.js | 195 ++ frontend/js/game/lobby.js | 20 +- frontend/vendor/quintus/.gitkeep | 0 frontend/vendor/quintus/LICENSE.txt | 20 + frontend/vendor/quintus/quintus.js | 2292 ++++++++++++++++++++ frontend/vendor/quintus/quintus_2d.js | 550 +++++ frontend/vendor/quintus/quintus_input.js | 986 +++++++++ frontend/vendor/quintus/quintus_scenes.js | 1172 ++++++++++ frontend/vendor/quintus/quintus_sprites.js | 770 +++++++ gameserver/CMakeLists.txt | 1 + gameserver/Dockerfile | 2 +- gameserver/src/Game.cpp | 402 ++++ gameserver/src/Game.hpp | 77 + gameserver/src/Lobby.cpp | 98 + gameserver/src/Lobby.hpp | 12 + 17 files changed, 6632 insertions(+), 5 deletions(-) create mode 100644 frontend/js/game/battle.js delete mode 100644 frontend/vendor/quintus/.gitkeep create mode 100644 frontend/vendor/quintus/LICENSE.txt create mode 100644 frontend/vendor/quintus/quintus.js create mode 100644 frontend/vendor/quintus/quintus_2d.js create mode 100644 frontend/vendor/quintus/quintus_input.js create mode 100644 frontend/vendor/quintus/quintus_scenes.js create mode 100644 frontend/vendor/quintus/quintus_sprites.js create mode 100644 gameserver/src/Game.cpp create mode 100644 gameserver/src/Game.hpp diff --git a/frontend/css/game.css b/frontend/css/game.css index b58023e..a11c1d8 100644 --- a/frontend/css/game.css +++ b/frontend/css/game.css @@ -88,3 +88,34 @@ content: '- '; color: var(--color-accent-dim); } + +#battlecity-canvas-container { + max-width: 640px; + margin: 1.5rem 0; +} + +#battlecity-canvas-container canvas { + border: var(--border); + background: var(--color-bg); + display: block; +} + +#battle-result { + color: var(--color-accent); + font-size: 1.1rem; + margin: 0.75rem 0; +} + +#battle-back { + background: transparent; + border: 1px solid var(--color-accent-dim); + color: var(--color-accent); + font-family: var(--font-mono); + padding: 0.35rem 0.75rem; + cursor: pointer; +} + +#battle-back:hover { + border-color: var(--color-accent); + box-shadow: 0 0 6px var(--color-accent-dim); +} diff --git a/frontend/game.html b/frontend/game.html index 5c55e48..5bef8ce 100644 --- a/frontend/game.html +++ b/frontend/game.html @@ -25,13 +25,18 @@
- +

./battlecity

-
+
+ + + + + diff --git a/frontend/js/game/battle.js b/frontend/js/game/battle.js new file mode 100644 index 0000000..21af651 --- /dev/null +++ b/frontend/js/game/battle.js @@ -0,0 +1,195 @@ +import { ClientMessage, ServerMessage } from './protocol.js'; + +const TILE_PX = 32; +// Quintus транслирует и вращает контекст сам (Sprite.render -> matrix.setContextTransform) +// на основе p.x/p.y/p.angle — draw() получает уже трансформированный контекст +// и должен рисовать спрайт центрированным в (0,0), без своих translate/rotate. +const DIR_DEGREES = { up: 0, right: 90, down: 180, left: 270 }; + +let Q = null; +let net = null; +let myPlayerId = null; +let stage = null; +let tankSprites = new Map(); +let bulletSprites = new Map(); +let lastSentInput = null; +let seq = 0; +let polling = false; + +function setupEntities() { + Q.Sprite.extend('Tank', { + init(p) { + this._super(p, { w: 26, h: 26, renderAlways: true }); + }, + draw(ctx) { + const p = this.p; + ctx.fillStyle = p.isMe ? '#00ff41' : '#9fa3a0'; + ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); + ctx.fillStyle = '#0a0e0c'; + ctx.fillRect(-3, -p.h / 2 - 8, 6, 12); + }, + }); + + Q.Sprite.extend('Bullet', { + init(p) { + this._super(p, { w: 8, h: 8, renderAlways: true }); + }, + draw(ctx) { + ctx.fillStyle = '#c7c7c7'; + ctx.beginPath(); + ctx.arc(0, 0, this.p.w / 2, 0, Math.PI * 2); + ctx.fill(); + }, + }); + + Q.Sprite.extend('Wall', { + init(p) { + this._super(p, { w: TILE_PX, h: TILE_PX }); + }, + draw(ctx) { + const p = this.p; + if (p.tile === 2) { + ctx.fillStyle = '#9fa3a0'; + ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); + } else { + ctx.fillStyle = '#0a8f2c'; + ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); + ctx.strokeStyle = '#0a0e0c'; + ctx.strokeRect(-p.w / 2 + 1, -p.h / 2 + 1, p.w - 2, p.h - 2); + } + }, + }); +} + +function ensureQuintus(cols, rows) { + if (Q) { + return; + } + Q = window.Quintus().include('Sprites, Scenes, 2D, Input').setup({ + width: cols * TILE_PX, + height: rows * TILE_PX, + }); + Q.input.keyboardControls({ + LEFT: 'left', RIGHT: 'right', UP: 'up', DOWN: 'down', + A: 'left', D: 'right', W: 'up', S: 'down', + SPACE: 'fire', + }); + setupEntities(); + Q.scene('battle', (stageRef) => { + stage = stageRef; + }); + + document.getElementById('battlecity-canvas-container').appendChild(Q.el); +} + +function buildMap(map) { + for (let y = 0; y < map.length; y++) { + for (let x = 0; x < map[y].length; x++) { + const tile = map[y][x]; + if (tile === 0) { + continue; + } + stage.insert( + new Q.Wall({ + x: x * TILE_PX + TILE_PX / 2, + y: y * TILE_PX + TILE_PX / 2, + tile, + }) + ); + } + } +} + +function pollInput() { + if (!polling) { + return; + } + const input = { + seq: seq++, + up: !!Q.inputs.up, + down: !!Q.inputs.down, + left: !!Q.inputs.left, + right: !!Q.inputs.right, + fire: !!Q.inputs.fire, + }; + const key = `${input.up}${input.down}${input.left}${input.right}${input.fire}`; + if (key !== lastSentInput) { + lastSentInput = key; + net.send(ClientMessage.GAME_INPUT, input); + } + requestAnimationFrame(pollInput); +} + +function onState(payload) { + for (const t of payload.tanks) { + let sprite = tankSprites.get(t.id); + if (!sprite) { + sprite = new Q.Tank({ x: t.x * TILE_PX, y: t.y * TILE_PX, isMe: t.id === myPlayerId }); + stage.insert(sprite); + tankSprites.set(t.id, sprite); + } + sprite.p.x = t.x * TILE_PX; + sprite.p.y = t.y * TILE_PX; + sprite.p.angle = DIR_DEGREES[t.direction] || 0; + sprite.p.hidden = !t.alive; + } + + const seenBullets = new Set(); + for (const b of payload.bullets) { + seenBullets.add(b.id); + let sprite = bulletSprites.get(b.id); + if (!sprite) { + sprite = new Q.Bullet({ x: b.x * TILE_PX, y: b.y * TILE_PX }); + stage.insert(sprite); + bulletSprites.set(b.id, sprite); + } + sprite.p.x = b.x * TILE_PX; + sprite.p.y = b.y * TILE_PX; + } + for (const [id, sprite] of bulletSprites) { + if (!seenBullets.has(id)) { + sprite.destroy(); + bulletSprites.delete(id); + } + } +} + +function onOver(payload) { + polling = false; + const resultText = { win: 'Победа', lose: 'Поражение', draw: 'Ничья' }[payload.result] || payload.result; + const resultEl = document.getElementById('battle-result'); + resultEl.textContent = `${resultText} — ${payload.reason}`; + document.getElementById('battle-back').style.display = 'inline-block'; +} + +export function startBattle(networkInstance, payload, ownPlayerId) { + net = networkInstance; + myPlayerId = ownPlayerId; + + const rows = payload.map.length; + const cols = payload.map[0].length; + + document.getElementById('lobby').style.display = 'none'; + const container = document.getElementById('battlecity-canvas-container'); + container.style.display = 'block'; + container.innerHTML = '

'; + + ensureQuintus(cols, rows); + Q.stageScene('battle', 0); + tankSprites = new Map(); + bulletSprites = new Map(); + + buildMap(payload.map); + + net.on(ServerMessage.GAME_STATE, onState); + net.on(ServerMessage.GAME_OVER, onOver); + + lastSentInput = null; + seq = 0; + polling = true; + requestAnimationFrame(pollInput); + + document.getElementById('battle-back').addEventListener('click', () => { + location.reload(); + }); +} diff --git a/frontend/js/game/lobby.js b/frontend/js/game/lobby.js index 043d364..4ec0a90 100644 --- a/frontend/js/game/lobby.js +++ b/frontend/js/game/lobby.js @@ -1,8 +1,11 @@ import { Network, ServerMessage } from './network.js'; +import { ClientMessage } from './protocol.js'; +import { startBattle } from './battle.js'; const container = document.getElementById('lobby'); let net; +let myPlayerId = null; let rooms = []; let currentRoom = null; let errorMessage = ''; @@ -47,14 +50,19 @@ function renderLobbyView() { } function renderRoomView() { + const readyBtn = + currentRoom.mode === 'pvp' + ? '' + : '

coop ещё не реализован (этап 5)

'; container.innerHTML = `
${errorBanner()}

room ${currentRoom.name}

режим: ${currentRoom.mode}

+ ${readyBtn}
`; } @@ -78,6 +86,9 @@ container?.addEventListener('click', (e) => { case 'join': net.joinRoom(btn.dataset.roomId); break; + case 'ready': + net.send(ClientMessage.GAME_READY); + break; case 'leave': currentRoom = null; net.leaveRoom(); @@ -99,8 +110,9 @@ export function initLobby() { net = new Network(); net.on('_open', () => net.hello(nickname)); - net.on(ServerMessage.HELLO_ACK, () => { + net.on(ServerMessage.HELLO_ACK, (payload) => { connected = true; + myPlayerId = payload.player_id; net.listRooms(); }); net.on('_close', () => { @@ -125,6 +137,10 @@ export function initLobby() { } }); + net.on(ServerMessage.GAME_START, (payload) => { + startBattle(net, payload, myPlayerId); + }); + net.on(ServerMessage.ERROR, (payload) => { console.error('[gameserver] error:', payload); errorMessage = payload.message; diff --git a/frontend/vendor/quintus/.gitkeep b/frontend/vendor/quintus/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/vendor/quintus/LICENSE.txt b/frontend/vendor/quintus/LICENSE.txt new file mode 100644 index 0000000..72997a4 --- /dev/null +++ b/frontend/vendor/quintus/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2011 Cykod LLC, http://coderdeck.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/frontend/vendor/quintus/quintus.js b/frontend/vendor/quintus/quintus.js new file mode 100644 index 0000000..fb82652 --- /dev/null +++ b/frontend/vendor/quintus/quintus.js @@ -0,0 +1,2292 @@ +/*global module:false */ + + +// 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 +*/ + +var quintusCore = function(exportTarget,key) { + "use strict"; + +/** + 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 = exportTarget[key] = function(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 */ }; + + /** + Default no-op select method. Replaced with the Quintus.Scene class + + + 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") + + @method Q.include + @param {String} mod - A comma separated list of module names + @return {Quintus} returns Quintus instance for chaining. + @for Quintus + */ + 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; + }; + + /** + 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. + + 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 + + @method Q._normalizeArg + @param {String or Array} arg - Either a comma separated string or an array + @return {Array} array of normalized names + @for Quintus + */ + 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 (modifies destination object) + + @method Q._extend + @param {Object} dest - destination object + @param {Object} source - source object + @return {Object} returns the dest object + @for Quintus + */ + 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. (uses extend internally) + + @method Q._clone + @param {Object} obj - object to clone + @return {Object} cloned object + @for Quintus + */ + Q._clone = function(obj) { + return Q._extend({},obj); + }; + + /** + Method that adds default properties onto an object only if the key on dest is undefined + + @method Q._defaults + @param {Object} dest - destination object + @param {Object} source - source object + @return {Object} returns the dest object + @for Quintus + */ + 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 + + @method Q._defaults + @param {Object} object - destination object + @param {String} key - key to check for + @return {Boolean} + @for Quintus + */ + Q._has = function(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + }; + + /** + Check if something is a string + + NOTE: this fails for non-primitives + + @method Q._isString + @param {Var} obj - object to check + @return {Boolean} + @for Quintus + */ + Q._isString = function(obj) { + return typeof obj === "string"; + }; + + /** + Check if something is a number + + @method Q._isNumber + @param {Var} obj - object to check + @return {Boolean} + @for Quintus + */ + Q._isNumber = function(obj) { + return Object.prototype.toString.call(obj) === '[object Number]'; + }; + + /** + Check if something is a function + + @method Q._isFunction + @param {Var} obj - object to check + @return {Boolean} + @for Quintus + */ + Q._isFunction = function(obj) { + return Object.prototype.toString.call(obj) === '[object Function]'; + }; + + /** + Check if something is an Object + + @method Q._isObject + @param {Var} obj - object to check + @return {Boolean} + @for Quintus + */ + Q._isObject = function(obj) { + return Object.prototype.toString.call(obj) === '[object Object]'; + }; + + /** + Check if something is an Array + + @method Q._isArray + @param {Var} obj - object to check + @return {Boolean} + @for Quintus + */ + Q._isArray = function(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + + /** + Check if something is undefined + + @method Q._isUndefined + @param {Var} obj - object to check + @return {Boolean} + @for Quintus + */ + Q._isUndefined = function(obj) { + return obj === void 0; + }; + + /** + Removes a property from an object and returns it if it exists + + @method Q._popProperty + @param {Object} obj + @param {String} property - property to pop off the object + @return {Var} popped property + @for Quintus + */ + 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. + Uses the built in `forEach` method + + @method Q._each + @param {Array or Object} obj + @param {Function iterator function, `this` is used for each object + @for Quintus + */ + 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 + + @method Q._invoke + @param {Array} arr + @param {String} property - property to invoke + @param {Var} [arg1] + @param {Var} [arg2] + @for Quintus + */ + 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. + + @method Q._detect + @param {Array or Object} obj + @param {Function} iterator + @param {Object} context + @param {Var} [arg1] + @param {Var} [arg2] + @returns {Var} first truthy value + @for Quintus + */ + 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. + + @method Q._detect + @param {Array or Object} obj + @param {Function} iterator + @param {Object} context + @returns {Array} + @for Quintus + */ + 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 removed + + @method Q._uniq + @param {Array} arr + @returns {Array} uniq'd sorted copy of array + @for Quintus + */ + 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; + }; + + Q.scheduleFrame(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... + + @method Q.pauseGame + @for Quintus + */ + Q.pauseGame = function() { + if(Q.loop) { + Q.cancelFrame(Q.loop); + } + Q.loop = null; + }; + + /** + Unpause the game by restarting the requestAnimationFrame-based loop. + 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... + + @method Q.pauseGame + @for Quintus + */ + Q.unpauseGame = function() { + if(!Q.loop) { + Q.lastGameLoopFrame = new Date().getTime(); + Q.loop = Q.scheduleFrame(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 + * @param {String} className - class to check against + */ + Q.Class.prototype.isA = function(className) { + return this.className === className; + }; + + /** + * Create a new Class that inherits from this class + * + * @method extend + * @param {String} className + * @param {Object} properties - hash of properties (init will be the constructor) + * @param {Object} [classMethods] - optional class methods to add to the 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. Q.Evented objects can trigger events and other objects can + bind to those events. + + @class Q.Evented + @extends Q.Class + @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 + @param {String} event - name or comma separated list of events + @param {Object} [target] - optional context for callback, defaults to the Evented + @param {Function} [callback] - callback (optional - defaults to name of event on context + */ + 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. + + @method debind + @for Q.Evented + */ + 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) { + Q.wrapper = document.createElement("div"); + Q.wrapper.id = Q.el.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; + + //scale to fit + if(options.scaleToFit) { + var factor = 1; + + var winW = window.innerWidth*factor; + var winH = window.innerHeight*factor; + var winRatio = winW/winH; + var gameRatio = Q.el.width/Q.el.height; + var scaleRatio = gameRatio < winRatio ? winH/Q.el.height : winW/Q.el.width; + var scaledW = Q.el.width * scaleRatio; + var scaledH = Q.el.height * scaleRatio; + + Q.el.style.width = scaledW + "px"; + Q.el.style.height = scaledH + "px"; + + if(Q.el.parentNode) { + Q.el.parentNode.style.width = scaledW + "px"; + Q.el.parentNode.style.height = scaledH + "px"; + } + + Q.cssWidth = parseInt(scaledW,10); + Q.cssHeight = parseInt(scaledH,10); + + //center vertically when adjusting to width + if(gameRatio > winRatio) { + var topPos = (winH - scaledH)/2; + Q.el.style.top = topPos+'px'; + } + } + + window.addEventListener('orientationchange',function() { + setTimeout(function() { window.scrollTo(0,1); }, 0); + }); + + return Q; + }; + + + /** + Clear the canvas completely. + + If you want it cleared to a specific color - set `Q.clearColor` to that color + + @method Q.clear + @for Quintus + */ + 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); + } + }; + + Q.setImageSmoothing = function(enabled) { + Q.ctx.mozImageSmoothingEnabled = enabled; + Q.ctx.webkitImageSmoothingEnabled = enabled; + Q.ctx.msImageSmoothingEnabled = enabled; + Q.ctx.imageSmoothingEnabled = enabled; + }; + + /** + Return canvas image data given an Image object. + + @method Q.imageData + @for Quintus + @param {Image} img - image to get image data for + */ + 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. + + Default bindings are: + + * png, jpg, gif, jpeg -> Image + * ogg, wav, m4a, mp3 -> Audio + * Everything else -> Data + + To add a new file extension in to an existing type you can just add it to asset types: + + Q.assetTypes['bmp'] = "Image"; + + To add in a new loader, you'll need to define a method for that type and add to the `Q.assetTypes` object, e.g.: + + Q.loadAssetVideo = function(key,src,callback,errorCallback) { + var vid = new Video(); + vid.addEventListener("canplaythrough",function() { callback(key,vid); }); + vid.onerror = errorCallback; + vid.src = Q.assetUrl(Q.options.imagePath,src); + }; + + Q.assetTypes['mp4'] = 'Video' + + + @for Quintus + @property Q.assetTypes + @type Object + */ + Q.assetTypes = { + png: 'Image', jpg: 'Image', gif: 'Image', jpeg: 'Image', + ogg: 'Audio', wav: 'Audio', m4a: 'Audio', mp3: 'Audio' + }; + + + /** + Return the file extension of a filename + + @for Quintus + @method Q._fileExtension + @param {String} filename + @return {String} lowercased extension + */ + Q._fileExtension = function(filename) { + var fileParts = filename.split("."), + fileExt = fileParts[fileParts.length-1].toLowerCase(); + return fileExt; + }; + + /** + Determine the type of asset based on the `Q.assetTypes` lookup table + + @for Quintus + @method Q.assetType + @param {String} asset + */ + Q.assetType = function(asset) { + /* Determine the lowercase extension of the file */ + var fileExt = Q._fileExtension(asset); + + // 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 + + @for Quintus + @method Q.assetUrl + @param {String} base - base for relative paths + @param {String} url - url to resolve to asset url + @return {String} resolved 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 + + @for Quintus + @method Q.loadAssetImage + @param {String} key + @param {String} src + @param {Function} callback + @param {Function} errorCallback + */ + 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` + + + @for Quintus + @method Q.loadAssetAudio + @param {String} key + @param {String} src + @param {Function} callback + @param {Function} errorCallback + */ + 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); + } + }; + + /** + Asset loader for Audio files if using the WebAudio API engine + + @for Quintus + @method Q.loadAssetWebAudio + @param {String} key + @param {String} src + @param {Function} callback + @param {Function} errorCallback + */ + 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 stores the data returned from an Ajax call. + + Just makes a Ajax request for all other file types + + @for Quintus + @method Q.loadAssetOther + @param {String} key + @param {String} src + @param {Function} callback + @param {Function} errorCallback + */ + Q.loadAssetOther = function(key,src,callback,errorCallback) { + var request = new XMLHttpRequest(); + + var fileParts = src.split("."), + fileExt = fileParts[fileParts.length-1].toLowerCase(); + + if(document.location.origin === "file://" || document.location.origin === "null") { + if(!Q.fileURLAlert) { + Q.fileURLAlert = true; + alert("Quintus Error: Loading assets is not supported from file:// urls - please run from a local web-server and try again"); + } + return errorCallback(); + } + + 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 + + @for Quintus + @method _removeExtension + @param {String} filename + @return {String} filename 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. + + @for Quintus + @method asset + @param {String} name - name of asset to lookup + */ + 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. + }); + + @for Quintus + @method Q.load + @param {String, Array or Array} assets - comma separated string, array or Object hash of assets to load + @param {Function} callback - called when done loading + @param {Object} options + */ + 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 loaded */ + 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 + }); + @for Quintus + @method Q.preload + @param {String or Function} arg - comma separated string of assets to load, or callback + @param {Object} [options] - options to pass to load + */ + 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 + + Do not call `new Q.Matrix2D` - use the provided Q.matrix2D factory function for GC happiness + + var matrix = Q.matrix2d(); + + Good Docs here: https://github.com/heygrady/transform/wiki/calculating-2d-matrices + + Used internally by Quintus for all transforms / collision detection. Most of the methods modify the matrix they are called upon and are chainable. + + @class Q.Matrix2D + @for Quintus + @extends Q.Class + */ + Q.Matrix2D = Q.Class.extend({ + /** + Initialize a matrix from a source or with the identify matrix + + @constructor + @for Q.Matrix2D + */ + 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 + + @for Q.Matrix2D + @method identity + @chainable + */ + 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 + + @for Q.Matrix2D + @method clone + @param {Q.Matrix2D} matrix - matrix to clone + @chainable + */ + 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; + }, + + /** + multiply two matrices (leaving the result in 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 ] ] + + @for Q.Matrix2D + @method clone + @param {Q.Matrix2D} matrix - matrix to multiply by + @chainable + */ + 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 + + @for Q.Matrix2D + @method rotate + @param {Float} radians - angle to rotate by + @chainable + */ + 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 (calls rotate internally) + + @for Q.Matrix2D + @method rotateDeg + @param {Float} degrees + @chainable + */ + 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 + @for Q.Matrix2D + @method scale + @param {Float} sx - scale in x dimension (scaling is uniform unless `sy` is provided) + @param {Float} [sy] - scale in the y dimension + @chainable + */ + 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 + + @for Q.Matrix2D + @method translate + @param {Float} tx + @param {Float} ty + @chainable + */ + 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; + }, + + + /** + Transform x and y coordinates by this matrix + Memory Hoggy version, returns a new Array + + @for Q.Matrix2D + @method transform + @param {Float} x + @param {Float} y + + */ + 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 + @for Q.Matrix2D + @method transformPt + @param {Object} obj + @return {Object} obj + */ + 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 elements by this Matrix and put the result in + the outArr + + @for Q.Matrix2D + @method transformArr + @param {Array} inArr - input array + @param {Array} outArr - output array + @return {Object} obj + */ + 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 coordinate transformed by this Matrix + + @for Q.Matrix2D + @method transformX + @param {Float} x + @param {Float} y + @return {Float} x transformed + */ + transformX: function(x,y) { + return x * this.m[0] + y * this.m[1] + this.m[2]; + }, + + /** + Return just the y coordinate transformed by this Matrix + + @for Q.Matrix2D + @method transformY + @param {Float} x + @param {Float} y + @return {Float} y transformed + */ + transformY: function(x,y) { + return x * this.m[3] + y * this.m[4] + this.m[5]; + }, + + /** + Release this Matrix to be reused + + @for Q.Matrix2D + @method release + */ + release: function() { + Q.matrices2d.push(this); + return null; + }, + + /** + Set the complete transform on a Canvas 2D context + + @for Q.Matrix2D + @method setContextTransform + @param {Context2D} ctx - 2D canvs context + */ + 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]); + } + + }); + + // 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() { + if (typeof window === 'undefined') { + return; + } + + 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 = setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + + if (!window.cancelAnimationFrame) { + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + } +}()); + + +return Quintus; +}; + +if(typeof exports === 'undefined') { + quintusCore(this,"Quintus"); +} else { + var Quintus = quintusCore(module,"exports"); +} + + diff --git a/frontend/vendor/quintus/quintus_2d.js b/frontend/vendor/quintus/quintus_2d.js new file mode 100644 index 0000000..113e91c --- /dev/null +++ b/frontend/vendor/quintus/quintus_2d.js @@ -0,0 +1,550 @@ +/*global Quintus:false, module:false */ + +var quintus2D = function(Quintus) { +"use strict"; + +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; + if(Q._isUndefined(boundingBox) && this.lists.TileLayer !== undefined) { + this.viewport.boundingBox = Q._detect(this.lists.TileLayer, function(layer) { + return layer.p.boundingBox ? { minX: 0, maxX: layer.p.w, minY: 0, maxY: layer.p.h } : null; + }); + } else { + 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.offsetX : + undefined, + followY ? + this.following.p.y - 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 = Math.max(this.boundingBox.maxX - Q.width, this.boundingBox.minX) / 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 = Math.max(this.boundingBox.maxY - Q.height, this.boundingBox.minY) / 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.Sprite.extend("TileLayer",{ + + init: function(props) { + this._super(props,{ + tileW: 32, + tileH: 32, + blockTileW: 10, + blockTileH: 10, + type: 1, + renderAlways: true + }); + if(this.p.dataAsset) { + this.load(this.p.dataAsset); + } + + this.setDimensions(); + + 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.tileProperties = {}; + + this.collisionObject = { + p: { + w: this.p.tileW, + h: this.p.tileH, + cx: this.p.tileW/2, + cy: this.p.tileH/2 + } + }; + + this.tileCollisionObjects = {}; + + this.collisionNormal = { separate: []}; + + this._generateCollisionObjects(); + }, + + // Generate the tileCollisionObject overrides where needed + _generateCollisionObjects: function() { + var self = this; + + function returnPoint(pt) { + return [ pt[0] * self.p.tileW - self.p.tileW/2, + pt[1] * self.p.tileH - self.p.tileH/2 + ]; + } + + if(this.sheet() && this.sheet().frameProperties) { + var frameProperties = this.sheet().frameProperties; + for(var k in frameProperties) { + var colObj = this.tileCollisionObjects[k] = { p: Q._clone(this.collisionObject.p) }; + Q._extend(colObj.p,frameProperties[k]); + + if(colObj.p.points) { + colObj.p.points = Q._map(colObj.p.points, returnPoint); + } + + this.tileCollisionObjects[k] = colObj; + } + } + + }, + + 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 { + throw "file type not supported"; + } + this.p.tiles = data; + }, + + setDimensions: function() { + var tiles = this.p.tiles; + + if(tiles) { + this.p.rows = tiles.length; + this.p.cols = tiles[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]; + }, + + getTileProperty: function(tile, prop) { + if(this.tileProperties[tile] !== undefined) { + return this.tileProperties[tile][prop]; + } else { + return; + } + }, + + getTileProperties: function(tile) { + if(this.tileProperties[tile] !== undefined) { + return this.tileProperties[tile]; + } else { + return {}; + } + }, + + getTilePropertyAt: function(tileX, tileY, prop) { + return this.getTileProperty(this.getTile(tileX, tileY), prop); + }, + + getTilePropertiesAt: function(tileX, tileY) { + return this.getTileProperties(this.getTile(tileX, tileY)); + }, + + tileHasProperty: function(tile, prop) { + return(this.getTileProperty(tile, prop) !== undefined); + }, + + setTile: function(x,y,tile) { + var p = this.p, + blockX = Math.floor(x/p.blockTileW), + blockY = Math.floor(y/p.blockTileH); + + if(x >= 0 && x < this.p.cols && + y >= 0 && y < 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; + }, + + getCollisionObject: function(tileX, tileY) { + var p = this.p, + tile = this.getTile(tileX, tileY), + colObj; + + colObj = (this.tileCollisionObjects[tile] !== undefined) ? + this.tileCollisionObjects[tile] : this.collisionObject; + + colObj.p.x = tileX * p.tileW + p.x + p.tileW/2; + colObj.p.y = tileY * p.tileH + p.y + p.tileH/2; + + return colObj; + }, + + collide: function(obj) { + var p = this.p, + objP = obj.c || obj.p, + tileStartX = Math.floor((objP.x - objP.cx - p.x) / p.tileW), + tileStartY = Math.floor((objP.y - objP.cy - p.y) / p.tileH), + tileEndX = Math.ceil((objP.x - objP.cx + objP.w - p.x) / p.tileW), + tileEndY = Math.ceil((objP.y - objP.cy + objP.h - p.y) / p.tileH), + normal = this.collisionNormal, + col, colObj; + + normal.collided = false; + + for(var tileY = tileStartY; tileY<=tileEndY; tileY++) { + for(var tileX = tileStartX; tileX<=tileEndX; tileX++) { + if(this.tilePresent(tileX,tileY)) { + colObj = this.getCollisionObject(tileX, tileY); + + col = Q.collision(obj,colObj); + + if(col && col.magnitude > 0) { + if(colObj.p.sensor) { + colObj.tile = this.getTile(tileX,tileY); + if(obj.trigger) { + obj.trigger('sensor.tile',colObj); + } + } else if(!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); + if(obj.p.collisions !== undefined) { + obj.p.collisions.push(normal); + } + } + } + } + } + } + + 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); + entity.trigger("bump",col); + } + if(col.normalY > 0.3) { + if(!p.skipCollide && p.vy < 0) { p.vy = 0; } + col.impact = impactY; + + entity.trigger("bump.top",col); + entity.trigger("bump",col); + } + + if(col.normalX < -0.3) { + if(!p.skipCollide && p.vx > 0) { p.vx = 0; } + col.impact = impactX; + entity.trigger("bump.right",col); + entity.trigger("bump",col); + } + if(col.normalX > 0.3) { + if(!p.skipCollide && p.vx < 0) { p.vx = 0; } + col.impact = impactX; + + entity.trigger("bump.left",col); + entity.trigger("bump",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; + } + } + }); + +}; + + +}; + + +if(typeof Quintus === 'undefined') { + module.exports = quintus2D; +} else { + quintus2D(Quintus); +} diff --git a/frontend/vendor/quintus/quintus_input.js b/frontend/vendor/quintus/quintus_input.js new file mode 100644 index 0000000..3ba8ce5 --- /dev/null +++ b/frontend/vendor/quintus/quintus_input.js @@ -0,0 +1,986 @@ +/*global Quintus:false, module: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 +*/ + + +var quintusInput = function(Quintus) { +"use strict"; + +/** + * Quintus Input Module + * + * @class Quintus.Input + */ +Quintus.Input = function(Q) { + /** + * Provided key names mapped to key codes - add more names and key codes as necessary + * + * @for Quintus.Input + * @property KEY_NAMES + * @type Object + * @static + */ + var KEY_NAMES = Q.KEY_NAMES = { + LEFT: 37, RIGHT: 39, + UP: 38, DOWN: 40, + + ZERO : 48, ONE : 49, TWO : 50, + THREE : 51, FOUR : 52, FIVE : 53, + SIX : 54, SEVEN : 55, EIGHT : 56, + NINE : 57, + + A : 65, B : 66, C : 67, + D : 68, E : 69, F : 70, + G : 71, H : 72, I : 73, + J : 74, K : 75, L : 76, + M : 77, N : 78, O : 79, + P : 80, Q : 81, R : 82, + S : 83, T : 84, U : 85, + V : 86, W : 87, X : 88, + Y : 89, Z : 90, + + ENTER: 13, + ESC: 27, + BACKSPACE : 8, + TAB : 9, + SHIFT : 16, + CTRL : 17, + ALT : 18, + SPACE: 32, + + HOME : 36, END : 35, + PGGUP : 33, PGDOWN : 34 + }; + + var DEFAULT_KEYS = { + LEFT: 'left', RIGHT: 'right', + UP: 'up', DOWN: 'down', + SPACE: 'fire', + Z: 'fire', + X: 'action', + ENTER: 'confirm', + ESC: 'esc', + P: 'P', + S: 'S' + }; + + var DEFAULT_TOUCH_CONTROLS = [ ['left','<' ], + ['right','>' ], + [], + ['action','b'], + ['fire', 'a' ]]; + + // Clockwise from midnight (a la CSS) + var DEFAULT_JOYPAD_INPUTS = [ 'up','right','down','left']; + + /** + * Current state of bound inputs + * + * @for Quintus.Input + * @property Q.inputs + * @type Object + */ + Q.inputs = {}; + Q.joypad = {}; + + var hasTouch = !!('ontouchstart' in window); + + + /** + * + * Convert a canvas point to a stage point, x dimension + * + * @method Q.canvasToStageX + * @for Quintus.Input + * @param {Float} x + * @param {Q.Stage} stage + * @returns {Integer} x + */ + 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 + * @param {Float} y + * @param {Q.Stage} stage + * @returns {Integer} y + */ + 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 Q.InputSystem + * @extends Q.Evented + * @for Quintus.Input + */ + Q.InputSystem = Q.Evented.extend({ + keys: {}, + keypad: {}, + keyboardEnabled: false, + touchEnabled: false, + joypadEnabled: false, + + /** + * Bind a key name or keycode to an action name (used by `keyboardControls`) + * + * @method bindKey + * @for Q.InputSystem + * @param {String or Integer} key - name or integer keycode for to bind + * @param {String} name - name of action to bind to + */ + bindKey: function(key,name) { + Q.input.keys[KEY_NAMES[key] || key] = name; + }, + + /** + * Enable keyboard controls by binding to events + * + * @for Q.InputSystem + * @method 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); + } + if(!e.ctrlKey && !e.metaKey) { + 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); + + if(Q.options.autoFocus) { Q.el.focus(); } + this.keyboardEnabled = true; + }, + + + /** + * Convenience method to activate keyboard controls (call `bindKey` and `enableKeyboard` internally) + * + * @method keyboardControls + * @for Q.InputSystem + * @param {Object} [keys] - hash of key names or codes to actions + */ + keyboardControls: function(keys) { + keys = keys || DEFAULT_KEYS; + Q._each(keys,function(name,key) { + this.bindKey(key,name); + },Q.input); + this.enableKeyboard(); + }, + + _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 }; + }, + + /** + * Activate touch button controls - pass in an options hash to override + * + * Default Options: + * + * { + * left: 0, + * gutter:10, + * controls: DEFAULT_TOUCH_CONTROLS, + * width: Q.width, + * bottom: Q.height + * } + * + * Default controls are left and right buttons, a space, and 'a' and 'b' buttons, as defined as an Array of Arrays below: + * + * [ ['left','<' ], + * ['right','>' ], + * [], // use an empty array as a spacer + * ['action','b'], + * ['fire', 'a' ]] + * + * @method touchControls + * @for Q.InputSystem + * @param {Object} [opts] - Options hash + */ + 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, + fullHeight: false + },opts); + + opts.unit = (opts.width / opts.controls.length); + opts.size = opts.unit - (opts.gutter * 2); + + function getKey(touch) { + var pos = Q.input.touchLocation(touch), + minY = opts.bottom - opts.unit; + for(var i=0,len=opts.controls.length;i= minX && pos.x <= (minX+opts.size) && (opts.fullHeight || (pos.y >= minY + opts.gutter && pos.y <= (minY+opts.unit - opts.gutter)))) + { + return opts.controls[i][0]; + } + } + } + + function touchDispatch(event) { + var wasOn = {}, + i, len, tch, key, actionName; + + // Reset all the actions bound to controls + // but keep track of all the actions that were on + for(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['left'] || Q.inputs['right'] || p.landed > 0)) { + if(p.collisions.length === 1) { + collision = p.collisions[0]; + } else { + // If there's more than one possible slope, follow slope with negative Y normal + collision = null; + + for(var i = 0; i < p.collisions.length; i++) { + if(p.collisions[i].normalY < 0) { + collision = p.collisions[i]; + } + } + } + + // Don't climb up walls. + if(collision !== null && collision.normalY > -0.3 && collision.normalY < 0.3) { + collision = null; + } + } + + if(Q.inputs['left']) { + p.direction = 'left'; + if(collision && p.landed > 0) { + p.vx = p.speed * collision.normalY; + p.vy = -p.speed * collision.normalX; + } else { + p.vx = -p.speed; + } + } else if(Q.inputs['right']) { + p.direction = 'right'; + if(collision && p.landed > 0) { + p.vx = -p.speed * collision.normalY; + p.vy = p.speed * collision.normalX; + } else { + p.vx = p.speed; + } + } else { + p.vx = 0; + if(collision && p.landed > 0) { + p.vy = 0; + } + } + + if(p.landed > 0 && (Q.inputs['up'] || Q.inputs['action']) && !p.jumping) { + p.vy = p.jumpSpeed; + p.landed = -dt; + p.jumping = true; + } else if(Q.inputs['up'] || Q.inputs['action']) { + this.entity.trigger('jump', this.entity); + p.jumping = true; + } + + if(p.jumping && !(Q.inputs['up'] || Q.inputs['action'])) { + p.jumping = false; + this.entity.trigger('jumped', this.entity); + if(p.vy < p.jumpSpeed / 3) { + p.vy = p.jumpSpeed / 3; + } + } + } + p.landed -= dt; + } + }); + + + /** + * Step Controls component + * + * Adds Step (square grid based) 4-ways controls onto a Sprite + * + * Adds the following properties to the entity: + * + * { + * stepDistance: 32, // should be tile size + * stepDelay: 0.2 // seconds to delay before next step + * } + * + * + * @class stepControls + * @for Quintus.Input + */ + 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; + } + + } + + }); +}; + + +}; + +if(typeof Quintus === 'undefined') { + module.exports = quintusInput; +} else { + quintusInput(Quintus); +} diff --git a/frontend/vendor/quintus/quintus_scenes.js b/frontend/vendor/quintus/quintus_scenes.js new file mode 100644 index 0000000..4ee1b7e --- /dev/null +++ b/frontend/vendor/quintus/quintus_scenes.js @@ -0,0 +1,1172 @@ +/*global Quintus:false, module:false */ + + +/** +Quintus HTML5 Game Engine - Scenes Module + +The code in `quintus_scenes.js` defines the `Quintus.Scenes` module, which +adds in support for Scenes and Stages into Quintus. + +Depends on the `Quintus.Sprite` module. + +Scenes let you create reusable definitions for setting up levels and screens. + +Stages are the primary container object in Quintus, handling Sprite management, +stepping, rendering and collision detection. + +@module Quintus.Scenes +*/ + + +var quintusScenes = function(Quintus) { +"use strict"; + +/** + * Quintus Scenes Module Class + * + * @class Quintus.Scenes + */ +Quintus.Scenes = function(Q) { + + Q.scenes = {}; + Q.stages = []; + + + /** + Basic scene class, consisting primarily of a scene function + and some options that are passed to the stage. + + Should be instantiated by calling `Q.scene` not new + + @class Q.Scene + @for Quintus.Scenes + */ + Q.Class.extend('Scene',{ + init: function(sceneFunc,opts) { + this.opts = opts || {}; + this.sceneFunc = sceneFunc; + } + }); + + /** + Set up a new scene or return an existing scene. If you don't pass in `sceneFunc`, + it'll return a scene otherwise it'll create a new one. + + @method Q.scene + @for Quintus.Scenes + @param {String} name - name of scene to create or return + @param {Function} [sceneFunc] - scene function: `function(stage) { .. }` that sets up the stage + */ + Q.scene = function(name,sceneFunc,opts) { + if(sceneFunc === void 0) { + return Q.scenes[name]; + } else { + if(Q._isFunction(sceneFunc)) { + sceneFunc = new Q.Scene(sceneFunc,opts); + sceneFunc.name = name; + } + Q.scenes[name] = sceneFunc; + return sceneFunc; + } + }; + + Q._nullContainer = { + c: { + x: 0, + y: 0, + angle: 0, + scale: 1 + }, + matrix: Q.matrix2d() + }; + + + /** + SAT collision detection between two objects + Thanks to doc's at: http://www.sevenson.com.au/actionscript/sat/ + + This is sort of a black box - use the methods on stage like `search` and `collide` to + run the collision system. + + @property Q.collision + @for Quintus.Scenes + */ + 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; + + if(!o1.p.points) { Q._generatePoints(o1); } + if(!o2.p.points) { Q._generatePoints(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; + }()); + + + /** + Check for the overlap of the boudning boxes of two Sprites + + @method Q.overlap + @for Quintus.Scenes + @param {Q.Sprite} o1 + @param {Q.Sprite} o2 + @returns {Boolean} + */ + Q.overlap = function(o1,o2) { + var c1 = o1.c || o1.p || o1; + var c2 = o2.c || o2.p || o2; + + var o1x = c1.x - (c1.cx || 0), + o1y = c1.y - (c1.cy || 0); + var o2x = c2.x - (c2.cx || 0), + o2y = c2.y - (c2.cy || 0); + + return !((o1y+c1.ho2y+c2.h) || + (o1x+c1.wo2x+c2.w)); + }; + + /** + Base stage class, responsible for managing sets of sprites. + + `Q.Stage`'s aren't generally instantiated directly, but rather are created + automatically when you call `Q.stageScene('sceneName')` + + @class Q.Stage + @extends Q.GameObject + @for Quintus.Scenes + */ + Q.Stage = Q.GameObject.extend({ + // Should know whether or not the stage is paused + defaults: { + sort: false, + gridW: 400, + gridH: 400, + x: 0, + y: 0 + }, + + init: function(scene,opts) { + this.scene = scene; + this.items = []; + this.lists = {}; + this.index = {}; + this.removeList = []; + this.grid = {}; + this._collisionLayers = []; + + this.time = 0; + + this.defaults['w'] = Q.width; + this.defaults['h'] = Q.height; + + 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 + + @method loadAssets + @param {Array or String} asset - Array of assets or a string of asset name + @for Q.Stage + */ + // 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; + }, + + + /** + + @method identify + @param {function} func + @for Q.Stage + */ + 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; + }, + + /** + + @method find + @param {Number or String} id + @for Q.Stage + */ + find: function(id) { + return this.index[id]; + }, + + addToLists: function(lists,object) { + for(var i=0;i 0 && (col = this._collideCollisionLayer(obj,collisionMask))) { + if(!skipEvents) { + obj.trigger('hit',col); + obj.trigger('hit.collision',col); + } + Q._generateCollisionPoints(obj); + this.regrid(obj); + curCol--; + } + + curCol = maxCol; + while(curCol > 0 && (col2 = this.gridTest(obj,collisionMask))) { + 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); + } + + Q._generateCollisionPoints(obj); + 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 collision layers + regrid: function(item,skipAdd) { + if(item.collisionLayer) { return; } + item.grid = item.grid || {}; + + 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) { + if(this.index[id]) { + this.index[id].mark = time; + if(this.index[id].container) { this.index[id].container.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