;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s other.x) { this.x = other.x; } if (this.y > other.y) { return this.y = other.y; } }; Vec2d.prototype.floor = function(){ return this.apply(Math.floor); }; Vec2d.prototype.floored = function(){ return this.applied(Math.floor); }; Vec2d.prototype.ceil = function(){ return this.apply(Math.ceil); }; Vec2d.prototype.ceiled = function(){ return this.applied(Math.ceil); }; Vec2d.prototype.project = function(other){ this.scale(this.dot(other) / other.lengthSqrd()); return this; }; Vec2d.prototype.dot = function(other){ return this.x * other.x + this.y * other.y; }; Vec2d.prototype.rotate = function(other){ this.x = this.x * other.x - this.y * other.y; this.y = this.x * other.y + this.y * other.x; return this; }; },{}],3:[function(require,module,exports){ var events = require('events'); exports.isArray = isArray; exports.isDate = function(obj){return Object.prototype.toString.call(obj) === '[object Date]'}; exports.isRegExp = function(obj){return Object.prototype.toString.call(obj) === '[object RegExp]'}; exports.print = function () {}; exports.puts = function () {}; exports.debug = function() {}; exports.inspect = function(obj, showHidden, depth, colors) { var seen = []; var stylize = function(str, styleType) { // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics var styles = { 'bold' : [1, 22], 'italic' : [3, 23], 'underline' : [4, 24], 'inverse' : [7, 27], 'white' : [37, 39], 'grey' : [90, 39], 'black' : [30, 39], 'blue' : [34, 39], 'cyan' : [36, 39], 'green' : [32, 39], 'magenta' : [35, 39], 'red' : [31, 39], 'yellow' : [33, 39] }; var style = { 'special': 'cyan', 'number': 'blue', 'boolean': 'yellow', 'undefined': 'grey', 'null': 'bold', 'string': 'green', 'date': 'magenta', // "name": intentionally not styling 'regexp': 'red' }[styleType]; if (style) { return '\033[' + styles[style][0] + 'm' + str + '\033[' + styles[style][1] + 'm'; } else { return str; } }; if (! colors) { stylize = function(str, styleType) { return str; }; } function format(value, recurseTimes) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it if (value && typeof value.inspect === 'function' && // Filter out the util module, it's inspect function is special value !== exports && // Also filter out any prototype objects using the circular check. !(value.constructor && value.constructor.prototype === value)) { return value.inspect(recurseTimes); } // Primitive types cannot have properties switch (typeof value) { case 'undefined': return stylize('undefined', 'undefined'); case 'string': var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') .replace(/'/g, "\\'") .replace(/\\"/g, '"') + '\''; return stylize(simple, 'string'); case 'number': return stylize('' + value, 'number'); case 'boolean': return stylize('' + value, 'boolean'); } // For some reason typeof null is "object", so special case here. if (value === null) { return stylize('null', 'null'); } // Look up the keys of the object. var visible_keys = Object_keys(value); var keys = showHidden ? Object_getOwnPropertyNames(value) : visible_keys; // Functions without properties can be shortcutted. if (typeof value === 'function' && keys.length === 0) { if (isRegExp(value)) { return stylize('' + value, 'regexp'); } else { var name = value.name ? ': ' + value.name : ''; return stylize('[Function' + name + ']', 'special'); } } // Dates without properties can be shortcutted if (isDate(value) && keys.length === 0) { return stylize(value.toUTCString(), 'date'); } var base, type, braces; // Determine the object type if (isArray(value)) { type = 'Array'; braces = ['[', ']']; } else { type = 'Object'; braces = ['{', '}']; } // Make functions say that they are functions if (typeof value === 'function') { var n = value.name ? ': ' + value.name : ''; base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; } else { base = ''; } // Make dates with properties first say the date if (isDate(value)) { base = ' ' + value.toUTCString(); } if (keys.length === 0) { return braces[0] + base + braces[1]; } if (recurseTimes < 0) { if (isRegExp(value)) { return stylize('' + value, 'regexp'); } else { return stylize('[Object]', 'special'); } } seen.push(value); var output = keys.map(function(key) { var name, str; if (value.__lookupGetter__) { if (value.__lookupGetter__(key)) { if (value.__lookupSetter__(key)) { str = stylize('[Getter/Setter]', 'special'); } else { str = stylize('[Getter]', 'special'); } } else { if (value.__lookupSetter__(key)) { str = stylize('[Setter]', 'special'); } } } if (visible_keys.indexOf(key) < 0) { name = '[' + key + ']'; } if (!str) { if (seen.indexOf(value[key]) < 0) { if (recurseTimes === null) { str = format(value[key]); } else { str = format(value[key], recurseTimes - 1); } if (str.indexOf('\n') > -1) { if (isArray(value)) { str = str.split('\n').map(function(line) { return ' ' + line; }).join('\n').substr(2); } else { str = '\n' + str.split('\n').map(function(line) { return ' ' + line; }).join('\n'); } } } else { str = stylize('[Circular]', 'special'); } } if (typeof name === 'undefined') { if (type === 'Array' && key.match(/^\d+$/)) { return str; } name = JSON.stringify('' + key); if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { name = name.substr(1, name.length - 2); name = stylize(name, 'name'); } else { name = name.replace(/'/g, "\\'") .replace(/\\"/g, '"') .replace(/(^"|"$)/g, "'"); name = stylize(name, 'string'); } } return name + ': ' + str; }); seen.pop(); var numLinesEst = 0; var length = output.reduce(function(prev, cur) { numLinesEst++; if (cur.indexOf('\n') >= 0) numLinesEst++; return prev + cur.length + 1; }, 0); if (length > 50) { output = braces[0] + (base === '' ? '' : base + '\n ') + ' ' + output.join(',\n ') + ' ' + braces[1]; } else { output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; } return output; } return format(obj, (typeof depth === 'undefined' ? 2 : depth)); }; function isArray(ar) { return ar instanceof Array || Array.isArray(ar) || (ar && ar !== Object.prototype && isArray(ar.__proto__)); } function isRegExp(re) { return re instanceof RegExp || (typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]'); } function isDate(d) { if (d instanceof Date) return true; if (typeof d !== 'object') return false; var properties = Date.prototype && Object_getOwnPropertyNames(Date.prototype); var proto = d.__proto__ && Object_getOwnPropertyNames(d.__proto__); return JSON.stringify(proto) === JSON.stringify(properties); } function pad(n) { return n < 10 ? '0' + n.toString(10) : n.toString(10); } var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // 26 Feb 16:19:34 function timestamp() { var d = new Date(); var time = [pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(':'); return [d.getDate(), months[d.getMonth()], time].join(' '); } exports.log = function (msg) {}; exports.pump = null; var Object_keys = Object.keys || function (obj) { var res = []; for (var key in obj) res.push(key); return res; }; var Object_getOwnPropertyNames = Object.getOwnPropertyNames || function (obj) { var res = []; for (var key in obj) { if (Object.hasOwnProperty.call(obj, key)) res.push(key); } return res; }; var Object_create = Object.create || function (prototype, properties) { // from es5-shim var object; if (prototype === null) { object = { '__proto__' : null }; } else { if (typeof prototype !== 'object') { throw new TypeError( 'typeof prototype[' + (typeof prototype) + '] != \'object\'' ); } var Type = function () {}; Type.prototype = prototype; object = new Type(); object.__proto__ = prototype; } if (typeof properties !== 'undefined' && Object.defineProperties) { Object.defineProperties(object, properties); } return object; }; exports.inherits = function(ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object_create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }; var formatRegExp = /%[sdj%]/g; exports.format = function(f) { if (typeof f !== 'string') { var objects = []; for (var i = 0; i < arguments.length; i++) { objects.push(exports.inspect(arguments[i])); } return objects.join(' '); } var i = 1; var args = arguments; var len = args.length; var str = String(f).replace(formatRegExp, function(x) { if (x === '%%') return '%'; if (i >= len) return x; switch (x) { case '%s': return String(args[i++]); case '%d': return Number(args[i++]); case '%j': return JSON.stringify(args[i++]); default: return x; } }); for(var x = args[i]; i < len; x = args[++i]){ if (x === null || typeof x !== 'object') { str += ' ' + x; } else { str += ' ' + exports.inspect(x); } } return str; }; },{"events":4}],4:[function(require,module,exports){ (function(process){if (!process.EventEmitter) process.EventEmitter = function () {}; var EventEmitter = exports.EventEmitter = process.EventEmitter; var isArray = typeof Array.isArray === 'function' ? Array.isArray : function (xs) { return Object.prototype.toString.call(xs) === '[object Array]' } ; function indexOf (xs, x) { if (xs.indexOf) return xs.indexOf(x); for (var i = 0; i < xs.length; i++) { if (x === xs[i]) return i; } return -1; } // By default EventEmitters will print a warning if more than // 10 listeners are added to it. This is a useful default which // helps finding memory leaks. // // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. var defaultMaxListeners = 10; EventEmitter.prototype.setMaxListeners = function(n) { if (!this._events) this._events = {}; this._events.maxListeners = n; }; EventEmitter.prototype.emit = function(type) { // If there is no 'error' event listener then throw. if (type === 'error') { if (!this._events || !this._events.error || (isArray(this._events.error) && !this._events.error.length)) { if (arguments[1] instanceof Error) { throw arguments[1]; // Unhandled 'error' event } else { throw new Error("Uncaught, unspecified 'error' event."); } return false; } } if (!this._events) return false; var handler = this._events[type]; if (!handler) return false; if (typeof handler == 'function') { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: handler.call(this, arguments[1]); break; case 3: handler.call(this, arguments[1], arguments[2]); break; // slower default: var args = Array.prototype.slice.call(arguments, 1); handler.apply(this, args); } return true; } else if (isArray(handler)) { var args = Array.prototype.slice.call(arguments, 1); var listeners = handler.slice(); for (var i = 0, l = listeners.length; i < l; i++) { listeners[i].apply(this, args); } return true; } else { return false; } }; // EventEmitter is defined in src/node_events.cc // EventEmitter.prototype.emit() is also defined there. EventEmitter.prototype.addListener = function(type, listener) { if ('function' !== typeof listener) { throw new Error('addListener only takes instances of Function'); } if (!this._events) this._events = {}; // To avoid recursion in the case that type == "newListeners"! Before // adding it to the listeners, first emit "newListeners". this.emit('newListener', type, listener); if (!this._events[type]) { // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; } else if (isArray(this._events[type])) { // Check for listener leak if (!this._events[type].warned) { var m; if (this._events.maxListeners !== undefined) { m = this._events.maxListeners; } else { m = defaultMaxListeners; } if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); console.trace(); } } // If we've already got an array, just append. this._events[type].push(listener); } else { // Adding the second element, need to change to array. this._events[type] = [this._events[type], listener]; } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener) { var self = this; self.on(type, function g() { self.removeListener(type, g); listener.apply(this, arguments); }); return this; }; EventEmitter.prototype.removeListener = function(type, listener) { if ('function' !== typeof listener) { throw new Error('removeListener only takes instances of Function'); } // does not use listeners(), so no side effect of creating _events[type] if (!this._events || !this._events[type]) return this; var list = this._events[type]; if (isArray(list)) { var i = indexOf(list, listener); if (i < 0) return this; list.splice(i, 1); if (list.length == 0) delete this._events[type]; } else if (this._events[type] === listener) { delete this._events[type]; } return this; }; EventEmitter.prototype.removeAllListeners = function(type) { if (arguments.length === 0) { this._events = {}; return this; } // does not use listeners(), so no side effect of creating _events[type] if (type && this._events && this._events[type]) this._events[type] = null; return this; }; EventEmitter.prototype.listeners = function(type) { if (!this._events) this._events = {}; if (!this._events[type]) this._events[type] = []; if (!isArray(this._events[type])) { this._events[type] = [this._events[type]]; } return this._events[type]; }; })(require("__browserify_process")) },{"__browserify_process":5}],5:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; process.nextTick = (function () { var canSetImmediate = typeof window !== 'undefined' && window.setImmediate; var canPost = typeof window !== 'undefined' && window.postMessage && window.addEventListener ; if (canSetImmediate) { return function (f) { return window.setImmediate(f) }; } if (canPost) { var queue = []; window.addEventListener('message', function (ev) { if (ev.source === window && ev.data === 'process-tick') { ev.stopPropagation(); if (queue.length > 0) { var fn = queue.shift(); fn(); } } }, true); return function nextTick(fn) { queue.push(fn); window.postMessage('process-tick', '*'); }; } return function nextTick(fn) { setTimeout(fn, 0); }; })(); process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.binding = function (name) { throw new Error('process.binding is not supported'); } // TODO(shtylman) process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; },{}],6:[function(require,module,exports){ var KEY_OFFSET = 0; var MOUSE_OFFSET = 256; exports.KEY_OFFSET = KEY_OFFSET; exports.MOUSE_OFFSET = MOUSE_OFFSET; var Key = { Backspace: 8, Tab: 9, Enter: 13, Shift: 16, Ctrl: 17, Alt: 18, Pause: 19, Break: 19, CapsLock: 20, Escape: 27, Space: 32, PageUp: 33, PageDown: 34, End: 35, Home: 36, Left: 37, Up: 38, Right: 39, Down: 40, Insert: 45, Delete: 46, 0: 48, 1: 49, 2: 50, 3: 51, 4: 52, 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, MetaLeft: 91, MetaRight: 92, Select: 93, Numpad0: 96, Numpad1: 97, Numpad2: 98, Numpad3: 99, Numpad4: 100, Numpad5: 101, Numpad6: 102, Numpad7: 103, Numpad8: 104, Numpad9: 105, Multiply: 106, Add: 107, Subtract: 109, Decimal: 110, Divide: 111, F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123, NumLock: 144, ScrollLock: 145, Semicolon: 186, EqualSign: 187, Comma: 188, Dash: 189, Period: 190, SlashForward: 191, Grave: 192, BracketOpen: 219, SlashBack: 220, BracketClose: 221, Quote: 222 }; var Mouse = { Left: 1, Middle: 2, Right: 3 }; // map both Key and Mouse into Button var btnName, val; for (btnName in Key) { val = Key[btnName]; exports["Key" + btnName] = KEY_OFFSET + val; } for (btnName in Mouse) { val = Mouse[btnName]; exports["Mouse" + btnName] = MOUSE_OFFSET + val; } },{}],7:[function(require,module,exports){ var vec2d = require('vec2d'); var resources = require('./resources'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var button = require('./button'); var MOUSE_OFFSET = button.MOUSE_OFFSET; var KEY_OFFSET = button.KEY_OFFSET; var EPSILON = 0.00000001; var MAX_DISPLAY_FPS = 90000; module.exports = Engine; var targetFps = 60; var targetSpf = 1 / targetFps; var fpsSmoothness = 0.9; var fpsOneFrameWeight = 1.0 - fpsSmoothness; var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || fallbackRequestAnimationFrame; util.inherits(Engine, EventEmitter); function Engine(canvas) { EventEmitter.call(this); this.canvas = canvas; this.listeners = []; // add tabindex property to canvas so that it can receive keyboard input this.canvas.tabIndex = 0; this.context = this.canvas.getContext("2d"); this.size = vec2d(this.canvas.width, this.canvas.height); this.fps = targetFps; this.setMinFps(20); } Engine.prototype.setSize = function(size) { this.size = size; this.canvas.width = this.size.x; this.canvas.height = this.size.y; }; Engine.prototype.setMinFps = function(it){ this.maxSpf = 1 / it; }; Engine.prototype.start = function(){ this.attachListeners(); this.startMainLoop(); }; Engine.prototype.stop = function(){ this.stopMainLoop(); this.removeListeners(); }; Engine.prototype.buttonState = function(button){ return !!this.buttonStates[button]; }; Engine.prototype.buttonJustPressed = function(button){ return !!this.btnJustPressed[button]; }; Engine.prototype.draw = function(batch){ for (var i = 0; i < batch.sprites.length; ++i) { var sprites = batch.sprites[i]; for (var id in sprites) { var sprite = sprites[id]; var frame = sprite.animation.frames[sprite.getFrameIndex()]; this.context.save(); this.context.translate(sprite.pos.x, sprite.pos.y); this.context.scale(sprite.scale.x, sprite.scale.y); this.context.rotate(sprite.rotation); this.context.globalAlpha = sprite.alpha; this.context.drawImage(resources.spritesheet, frame.pos.x, frame.pos.y, frame.size.x, frame.size.y, -sprite.animation.anchor.x, -sprite.animation.anchor.y, frame.size.x, frame.size.y); this.context.restore(); } } }; Engine.prototype.drawFps = function(){ this.context.textAlign = 'left'; this.context.fillText(Math.round(this.fps) + " fps", 0, this.size.y); }; // private Engine.prototype.startMainLoop = function(){ var self = this; var previousUpdate = new Date(); this.mainLoopOn = true; requestAnimationFrame(mainLoop, this.canvas); function mainLoop(){ var now = new Date(); var delta = (now - previousUpdate) / 1000; previousUpdate = now; // make sure dt is never zero // if FPS is too low, lag instead of causing physics glitches var dt = delta; if (dt < EPSILON) dt = EPSILON; if (dt > self.maxSpf) dt = self.maxSpf; var multiplier = dt / targetSpf; self.emit('update', dt, multiplier); self.btnJustPressed = {}; self.emit('draw', self.context); var fps = 1 / delta; fps = fps < MAX_DISPLAY_FPS ? fps : MAX_DISPLAY_FPS; self.fps = self.fps * fpsSmoothness + fps * fpsOneFrameWeight; if (self.mainLoopOn) { requestAnimationFrame(mainLoop, self.canvas); } } }; Engine.prototype.attachListeners = function(){ var self = this; this.buttonStates = {}; this.btnJustPressed = {}; // disable right click context menu addListener(this.canvas, 'contextmenu', function(event){ event.preventDefault(); }); // mouse input this.mousePos = vec2d(); addListener(this.canvas, 'mousemove', function(event){ self.mousePos = vec2d( (event.offsetX) != null ? event.offsetX : event.pageX - event.target.offsetLeft, (event.offsetY) != null ? event.offsetY : event.pageY - event.target.offsetTop); self.emit('mousemove', self.mousePos, MOUSE_OFFSET + event.which); }); addListener(this.canvas, 'mousedown', function(event){ var buttonId; buttonId = MOUSE_OFFSET + event.which; self.buttonStates[buttonId] = true; self.btnJustPressed[buttonId] = true; self.emit('buttondown', buttonId); }); addListener(this.canvas, 'mouseup', function(event){ var buttonId; buttonId = MOUSE_OFFSET + event.which; self.buttonStates[buttonId] = false; self.emit('buttonup', buttonId); }); // keyboard input addListener(this.canvas, 'keydown', function(event){ var buttonId; buttonId = KEY_OFFSET + event.which; self.btnJustPressed[buttonId] = !self.buttonStates[buttonId]; self.buttonStates[buttonId] = true; self.emit('buttondown', buttonId); event.preventDefault(); return false; }); addListener(this.canvas, 'keyup', function(event){ var buttonId; buttonId = KEY_OFFSET + event.which; self.buttonStates[buttonId] = false; self.emit('buttonup', buttonId); event.preventDefault(); return false; }); function addListener(element, eventName, listener){ self.listeners.push([element, eventName, listener]); element.addEventListener(eventName, listener, false); } }; Engine.prototype.removeListeners = function(){ this.listeners.forEach(function(listener) { var element = listener[0]; var eventName = listener[1]; var fn = listener[2]; element.removeEventListener(eventName, fn, false); }); this.listeners = []; }; Engine.prototype.stopMainLoop = function(){ this.mainLoopOn = false; }; function fallbackRequestAnimationFrame(cb){ window.setTimeout(cb, targetSpf * 1000); } },{"util":3,"events":4,"./resources":1,"./button":6,"vec2d":2}],8:[function(require,module,exports){ module.exports = Sound; function Sound(url) { this.audioPool = [new Audio(url)]; } function audioReadyToPlay(audio) { return audio.currentTime === 0 || audio.currentTime === audio.duration; } Sound.prototype.play = function() { for (var i = 0; i < this.audioPool.length; ++i) { var audio = this.audioPool[i]; if (audioReadyToPlay(audio)) { audio.play(); return audio; } } var newAudio = new Audio(this.audioPool[0].currentSrc); newAudio.play(); this.audioPool.push(newAudio); }; },{}],9:[function(require,module,exports){ var Vec2d = require('vec2d').Vec2d; var util = require('util'); var EventEmitter = require('events').EventEmitter; var resources = require('./resources'); module.exports = Sprite; Sprite.idCount = 0; util.inherits(Sprite, EventEmitter); function Sprite(animationName, params) { EventEmitter.call(this); params = params || {}; // defaults this.pos = params.pos == null ? new Vec2d(0, 0) : params.pos; this.scale = params.scale == null ? new Vec2d(1, 1) : params.scale; this.zOrder = params.zOrder == null ? 0 : params.zOrder; this.batch = params.batch; this.rotation = params.rotation == null ? 0 : params.rotation; this.alpha = params.alpha == null ? 1 : params.alpha; this.id = Sprite.idCount++; this.setAnimationName(animationName); this.setLoop(params.loop); this.setVisible(params.visible == null ? true : params.visible); this.setFrameIndex(params.frameIndex == null ? 0 : params.frameIndex); if (resources.animations == null) { throw new Error("You may not create Sprites until the onReady event has been fired from Chem."); } } Sprite.prototype.setAnimationName = function(animationName){ var anim = resources.animations[animationName]; if (anim == null) { throw new Error("name not found in animation list: " + animationName); } this.setAnimation(resources.animations[animationName]); }; Sprite.prototype.setAnimation = function(animation){ this.animation = animation; this.animationName = this.animation.name; this._loop = this.loop == null ? this.animation.loop : this.loop; // size of first frame, which does not take scale into account this.size = this.animation.frames[0].size; }; // takes scale and current frame into account Sprite.prototype.getSize = function(){ return this.animation.frames[this.getFrameIndex()].size.times(this.scale.applied(Math.abs)); }; // convenience Sprite.prototype.getAnchor = function(){ return this.animation.anchor.times(this.scale.applied(Math.abs)); }; Sprite.prototype.getTopLeft = function(){ return this.pos.minus(this.getAnchor()); }; Sprite.prototype.getBottomRight = function(){ return this.getTopLeft().plus(this.getSize()); }; Sprite.prototype.getTop = function(){ return this.getTopLeft().y; }; Sprite.prototype.getLeft = function(){ return this.getTopLeft().x; }; Sprite.prototype.getBottom = function(){ return this.getBottomRight().y; }; Sprite.prototype.getRight = function(){ return this.getBottomRight().x; }; Sprite.prototype.setLeft = function(x){ this.pos.x = x + this.animation.anchor.x; }; Sprite.prototype.setRight = function(x){ this.pos.x = x - this.animation.anchor.x; }; Sprite.prototype.setTop = function(y){ this.pos.y = y + this.animation.anchor.y; }; Sprite.prototype.setBottom = function(y){ this.pos.y = y - this.animation.anchor.y; }; Sprite.prototype.isTouching = function(sprite){ var a_tl = this.getTopLeft(); var a_br = this.getBottomRight(); var b_tl = sprite.getTopLeft(); var b_br = sprite.getBottomRight(); var notTouching = a_tl.x >= b_br.x || a_br.x <= b_tl.x || a_tl.y >= b_br.y || a_br.y <= b_tl.y; return !notTouching; }; Sprite.prototype.setVisible = function(visible){ this.visible = visible; if (this.batch == null) { return; } if (this.visible) { this.batch.add(this); } else { this.batch.remove(this); } }; Sprite.prototype.setZOrder = function(zOrder){ if (this.batch != null) { this.batch.remove(this); this.zOrder = zOrder; this.batch.add(this); } else { this.zOrder = zOrder; } }; Sprite.prototype.setFrameIndex = function(frameIndex){ var secondsPassed = frameIndex * this.animation.delay; var date = new Date(); date.setMilliseconds(date.getMilliseconds() - secondsPassed * 1000); this.setAnimationStartDate(date); }; Sprite.prototype.setLoop = function(loop){ // this is the actual value we'll use to check if we're going to loop. this.loop = !!loop; this._loop = this.loop == null ? this.animation.loop : this.loop; this.setUpInterval(); }; Sprite.prototype.setAnimationStartDate = function(animationStartDate){ this.animationStartDate = animationStartDate; this.setUpInterval(); }; Sprite.prototype.getFrameIndex = function(){ var ref$, ref1$; var now = new Date(); var totalTime = (now - this.animationStartDate) / 1000; if (this._loop) { return Math.floor((totalTime % this.animation.duration) / this.animation.delay); } else { return (ref$ = Math.floor(totalTime / this.animation.delay)) < (ref1$ = this.animation.frames.length - 1) ? ref$ : ref1$; } }; Sprite.prototype['delete'] = function(){ if (this.batch) this.batch.remove(this); this.batch = null; }; // private Sprite.prototype.setUpInterval = function(){ var self = this; if (this.interval) this.interval.cancel(); var _schedule = this._loop ? schedule : wait; var now = new Date(); var timeSinceStart = (now - this.animationStartDate) / 1000; var duration = this.animation.duration - timeSinceStart; this.interval = _schedule(duration, function(){ return self.emit('animationend'); }); }; function wait(sec, cb) { var interval = setTimeout(cb, sec * 1000); return { cancel: function(){ if (interval != null) { clearTimeout(interval); interval = null; } } }; } function schedule(sec, cb) { var interval = setInterval(cb, sec * 1000); return { cancel: function(){ if (interval != null) { clearInterval(interval); interval = null; } } }; } },{"util":3,"events":4,"./resources":1,"vec2d":2}],10:[function(require,module,exports){ module.exports = Batch; function Batch() { // indexed by zOrder this.sprites = []; } Batch.prototype.add = function(sprite) { if (sprite.batch) sprite.batch.remove(sprite); sprite.batch = this; if (sprite.visible) { var o = this.sprites[sprite.zOrder]; if (! o) o = this.sprites[sprite.zOrder] = {}; o[sprite.id] = sprite; } }; Batch.prototype.remove = function(sprite) { var o = this.sprites[sprite.zOrder]; if (o) delete o[sprite.id]; }; },{}],11:[function(require,module,exports){ var resources = require('./lib/resources'); module.exports = { vec2d: require('vec2d'), Engine: require('./lib/engine'), button: require('./lib/button'), Sound: require('./lib/sound'), Sprite: require('./lib/sprite'), Batch: require('./lib/batch'), resources: resources, onReady: resources.onReady, }; resources.bootstrap(); },{"./lib/resources":1,"./lib/engine":7,"./lib/sound":8,"./lib/sprite":9,"./lib/button":6,"./lib/batch":10,"vec2d":2}],12:[function(require,module,exports){ var Atom, Batch, Bomb, Collision, Control, ControlsScene, Credits, Engine, Game, GameWindow, Indexable, Map, Rock, Set, Sound, Sprite, Tank, Title, atom_radius, atom_size, button, canvas, chem, cp, max_bias, params, prop, randInt, sign, val, vec2d, _ref, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; chem = require('chem'); cp = require('chipmunk'); vec2d = chem.vec2d, Engine = chem.Engine, Sprite = chem.Sprite, Batch = chem.Batch, button = chem.button, Sound = chem.Sound; atom_size = vec2d(32, 32); atom_radius = atom_size.x / 2; max_bias = 400; sign = function(x) { if (x > 0) { return 1; } else if (x < 0) { return -1; } else { return 0; } }; randInt = function(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }; Collision = { Default: 0, Claw: 1, Atom: 2 }; Control = { MoveLeft: 0, MoveRight: 1, MoveUp: 2, MoveDown: 3, FireMain: 4, FireAlt: 5, SwitchToGrapple: 6, SwitchToRay: 7, SwitchToLazer: 8, COUNT: 9, MOUSE_OFFSET: 255 }; Set = (function() { Set.assertItemHasId = function(item) { if (item.id == null) { throw "item missing id"; } }; function Set() { this.items = {}; this.length = 0; } Set.prototype.add = function(item) { Set.assertItemHasId(item); if (this.items[item.id] == null) { this.length += 1; } this.items[item.id] = item; }; Set.prototype.remove = function(item) { Set.assertItemHasId(item); if (this.items[item.id] != null) { this.length -= 1; delete this.items[item.id]; } }; Set.prototype.each = function(cb) { var id, item, _ref; _ref = this.items; for (id in _ref) { item = _ref[id]; if (!cb(item)) { return; } } }; Set.prototype.clone = function() { var set; set = new Set(); this.each(function(item) { set.add(item); return true; }); return set; }; return Set; })(); Map = (function() { Map.assertItemHasId = function(item) { if (item.id == null) { throw "item missing id"; } }; function Map() { this.pairs = {}; this.length = 0; } Map.prototype.set = function(key, value) { Map.assertItemHasId(key); if (this.pairs[key.id] == null) { this.length += 1; } this.pairs[key.id] = [key, value]; }; Map.prototype.remove = function(key) { Map.assertItemHasId(key); if (this.pairs[key.id] != null) { this.length -= 1; delete this.pairs[key.id]; } }; Map.prototype.each = function(cb) { var id, pair, _ref; _ref = this.pairs; for (id in _ref) { pair = _ref[id]; if (!cb.apply(null, pair)) { return; } } }; Map.prototype.clone = function() { var map; map = new Map(); this.each(function(key, value) { map.set(key, value); return true; }); return map; }; Map.prototype.contains = function(key) { return this.pairs[key.id] != null; }; Map.prototype.keys = function() { var id, key, value, _ref, _ref1, _results; _ref = this.pairs; _results = []; for (id in _ref) { _ref1 = _ref[id], key = _ref1[0], value = _ref1[1]; _results.push(key); } return _results; }; return Map; })(); Indexable = (function() { Indexable.id_count = 0; function Indexable() { this.id = Indexable.id_count++; } return Indexable; })(); Atom = (function(_super) { __extends(Atom, _super); Atom.flavor_count = 6; Atom.max_bonds = 2; function Atom(pos, flavor_index, sprite, space) { var body; this.flavor_index = flavor_index; this.sprite = sprite; this.space = space; this.cleanUp = __bind(this.cleanUp, this); this.unbond = __bind(this.unbond, this); this.bondLoop = __bind(this.bondLoop, this); this.bondTo = __bind(this.bondTo, this); Atom.__super__.constructor.apply(this, arguments); body = new cp.Body(10, 100000); body.setPos(pos); this.shape = new cp.CircleShape(body, atom_radius, vec2d()); this.shape.setFriction(0.5); this.shape.setElasticity(0.05); this.shape.collision_type = Collision.Atom; this.space.addBody(body); this.space.addShape(this.shape); this.shape.atom = this; this.bonds = new Map(); this.marked_for_deletion = false; this.rogue = false; } Atom.prototype.bondTo = function(other) { var joint; if (this.bonds.contains(other)) { return false; } if (this.bonds.length >= Atom.max_bonds || other.bonds.length >= Atom.max_bonds) { return false; } if (this.flavor_index !== other.flavor_index) { return false; } joint = new cp.PinJoint(this.shape.body, other.shape.body, vec2d(), vec2d()); joint.dist = atom_radius * 2.5; joint.maxBias = max_bias; this.bonds.set(other, joint); other.bonds.set(this, joint); this.space.addConstraint(joint); return true; }; Atom.prototype.bondLoop = function() { var atom, dest, found, seen, _ref, _this = this; if (this.bonds.length !== 2) { return null; } seen = new Map(); seen.set(this, true); _ref = this.bonds.keys(), atom = _ref[0], dest = _ref[1]; while (true) { seen.set(atom, true); if (atom === dest) { return seen.keys(); } found = false; atom.bonds.each(function(next_atom, joint) { if (!seen.contains(next_atom)) { atom = next_atom; found = true; return false; } return true; }); if (!found) { return null; } } }; Atom.prototype.unbond = function() { var _this = this; this.bonds.each(function(atom, joint) { atom.bonds.remove(_this); _this.space.removeConstraint(joint); return true; }); return this.bonds = new Map(); }; Atom.prototype.cleanUp = function() { this.unbond(); this.space.removeShape(this.shape); if (!this.rogue) { this.space.removeBody(this.shape.body); } delete this.shape.atom; this.sprite["delete"](); return this.sprite = null; }; return Atom; })(Indexable); Bomb = (function(_super) { __extends(Bomb, _super); Bomb.radius = 16; Bomb.size = vec2d(Bomb.radius * 2, Bomb.radius * 2); function Bomb(pos, sprite, space, timeout) { var body; this.sprite = sprite; this.space = space; this.timeout = timeout; this.cleanUp = __bind(this.cleanUp, this); this.tick = __bind(this.tick, this); Bomb.__super__.constructor.apply(this, arguments); body = new cp.Body(50, 10); body.setPos(pos); this.shape = new cp.CircleShape(body, Bomb.radius, vec2d()); this.shape.setFriction(0.7); this.shape.setElasticity(0.02); this.shape.collision_type = Collision.Default; this.space.addBody(body); this.space.addShape(this.shape); } Bomb.prototype.tick = function(dt) { return this.timeout -= dt; }; Bomb.prototype.cleanUp = function() { this.space.removeShape(this.shape); this.space.removeBody(this.shape.body); this.sprite["delete"](); return this.sprite = null; }; return Bomb; })(Indexable); Rock = (function(_super) { __extends(Rock, _super); Rock.radius = 16; Rock.size = vec2d(Rock.radius * 2, Rock.radius * 2); function Rock(pos, sprite, space) { var body; this.sprite = sprite; this.space = space; this.cleanUp = __bind(this.cleanUp, this); this.tick = __bind(this.tick, this); Rock.__super__.constructor.apply(this, arguments); body = new cp.Body(70, 100000); body.setPos(pos); this.shape = new cp.CircleShape(body, Rock.radius, vec2d()); this.shape.setFriction(0.9); this.shape.setElasticity(0.01); this.shape.collision_type = Collision.Default; this.space.addBody(body); this.space.addShape(this.shape); } Rock.prototype.tick = function(dt) {}; Rock.prototype.cleanUp = function() { this.space.removeShape(this.shape); this.space.removeBody(this.shape.body); this.sprite["delete"](); return this.sprite = null; }; return Rock; })(Indexable); Tank = (function() { function Tank(pos, dims, game, tank_index) { this.pos = pos; this.dims = dims; this.game = game; this.tank_index = tank_index; this.drawLine = __bind(this.drawLine, this); this.drawPrimitives = __bind(this.drawPrimitives, this); this.moveSprites = __bind(this.moveSprites, this); this.respond_to_asplosion = __bind(this.respond_to_asplosion, this); this.computeAtomPointedAt = __bind(this.computeAtomPointedAt, this); this.unattachClaw = __bind(this.unattachClaw, this); this.retractClaw = __bind(this.retractClaw, this); this.playSfx = __bind(this.playSfx, this); this.atomHitAtom = __bind(this.atomHitAtom, this); this.clawHitSomething = __bind(this.clawHitSomething, this); this.processQueuedActions = __bind(this.processQueuedActions, this); this.processInput = __bind(this.processInput, this); this.explodeAtoms = __bind(this.explodeAtoms, this); this.explodeAtom = __bind(this.explodeAtom, this); this.win = __bind(this.win, this); this.lose = __bind(this.lose, this); this.computeDrops = __bind(this.computeDrops, this); this.dropRock = __bind(this.dropRock, this); this.dropBomb = __bind(this.dropBomb, this); this.getDropPos = __bind(this.getDropPos, this); this.computeArmPos = __bind(this.computeArmPos, this); this.initMan = __bind(this.initMan, this); this.adjustCeiling = __bind(this.adjustCeiling, this); this.initCeiling = __bind(this.initCeiling, this); this.initWalls = __bind(this.initWalls, this); this.removeBomb = __bind(this.removeBomb, this); this.removeAtom = __bind(this.removeAtom, this); this.update = __bind(this.update, this); this.initControls = __bind(this.initControls, this); this.initGuns = __bind(this.initGuns, this); this.size = this.dims.times(atom_size); this.other_tank = null; this.atoms = new Set(); this.bombs = new Set(); this.rocks = new Set(); this.queued_asplosions = []; this.min_power = params.power || 3; this.sprite_arm = new Sprite('arm', { batch: this.game.batch, zOrder: this.game.group_fg }); this.sprite_man = new Sprite('still', { batch: this.game.batch, zOrder: this.game.group_main }); this.sprite_claw = new Sprite('claw', { batch: this.game.batch, zOrder: this.game.group_main }); this.space = new cp.Space(); this.space.gravity = vec2d(0, -400); this.space.damping = 0.99; this.space.addCollisionHandler(Collision.Claw, Collision.Default, null, null, this.clawHitSomething); this.space.addCollisionHandler(Collision.Claw, Collision.Atom, null, null, this.clawHitSomething); this.space.addCollisionHandler(Collision.Atom, Collision.Atom, null, null, this.atomHitAtom); this.initControls(); this.mousePos = vec2d(0, 0); this.man_dims = vec2d(1, 2); this.man_size = this.man_dims.times(atom_size); this.time_between_drops = params.fastatoms || 1; this.time_until_next_drop = 0; this.initWalls(); this.initCeiling(); this.initMan(); this.initGuns(); this.arm_offset = vec2d(13, 43); this.arm_len = 24; this.computeArmPos(); this.closest_atom = null; this.equipped_gun = Control.SwitchToGrapple; this.gun_animations = {}; this.gun_animations[Control.SwitchToGrapple] = "arm"; this.gun_animations[Control.SwitchToRay] = "raygun"; this.gun_animations[Control.SwitchToLazer] = "lazergun"; this.bond_queue = []; this.points = 0; this.points_to_crush = 50; this.point_end = vec2d(0.000001, 0.000001); this.lose_ratio = 95 / 300; if (this.tank_index == null) { this.tank_index = randInt(0, 1); } this.sprite_tank = new Sprite("tank" + this.tank_index, { batch: this.game.batch, zOrder: this.game.group_main }); this.game_over = false; this.winner = null; this.atom_drop_enabled = true; this.enable_point_calculation = true; this.sfx_enabled = true; } Tank.prototype.initGuns = function() { this.claw_in_motion = false; this.sprite_claw.setVisible(false); this.claw_radius = 8; this.claw_shoot_speed = 1200; this.min_claw_dist = 60; this.claw_pins_to_add = null; this.claw_pins = null; this.claw_attached = false; this.want_to_remove_claw_pin = false; this.want_to_retract_claw = false; this.lazer_timeout = 0.5; this.lazer_recharge = 0; this.lazer_line = null; this.lazer_line_timeout = 0; this.lazer_line_timeout_start = 0.2; this.ray_atom = null; return this.ray_shoot_speed = 900; }; Tank.prototype.initControls = function() { this.controls = {}; this.controls[button.KeyA] = Control.MoveLeft; this.controls[button.KeyD] = Control.MoveRight; this.controls[button.KeyW] = Control.MoveUp; this.controls[button.KeyS] = Control.MoveDown; this.controls[button.Key1] = Control.SwitchToGrapple; this.controls[button.Key2] = Control.SwitchToRay; this.controls[button.Key3] = Control.SwitchToLazer; this.controls[button.MouseLeft] = Control.FireMain; this.controls[button.MouseRight] = Control.FireAlt; if (params.keyboard === 'dvorak') { this.controls[button.KeyA] = Control.MoveLeft; this.controls[button.KeyE] = Control.MoveRight; this.controls[button.KeyComma] = Control.MoveUp; this.controls[button.KeyS] = Control.MoveDown; } else if (params.keyboard === 'colemak') { this.controls[button.KeyA] = Control.MoveLeft; this.controls[button.KeyS] = Control.MoveRight; this.controls[button.KeyW] = Control.MoveUp; this.controls[button.KeyR] = Control.MoveDown; } this.let_go_of_fire_main = true; return this.let_go_of_fire_alt = true; }; Tank.prototype.update = function(dt) { var delta, i, pin, ratio, step_count, _i, _j, _len, _ref, _this = this; this.adjustCeiling(dt); if (this.atom_drop_enabled) { this.computeDrops(dt); } ratio = this.atoms.length / (this.ceiling.body.p.y - this.size.y / 2); if (ratio > this.lose_ratio || this.ceiling.body.p.y < this.man_size.y) { this.lose(); } this.bombs.clone().each(function(bomb) { var body, damp, direction, dist, power, removeBombSprite, sprite, vector, _i, _len, _ref; bomb.tick(dt); if (bomb.timeout <= 0) { _ref = _this.space.bodies; for (_i = 0, _len = _ref.length; _i < _len; _i++) { body = _ref[_i]; vector = body.p.minus(bomb.shape.body.p); dist = vector.length(); direction = vector.normalized(); power = 6000; damp = 1 - dist / 800; body.applyImpulse(direction.scaled(power * damp), vec2d(0, 0)); } sprite = new Sprite("bombsplode", { batch: _this.game.batch, zOrder: _this.game.group_fg }); sprite.pos = _this.pos.plus(bomb.shape.body.p); sprite.pos.y = 600 - sprite.pos.y; removeBombSprite = null; sprite.on("animationend", (function(sprite) { return function() { return sprite["delete"](); }; })(sprite)); _this.removeBomb(bomb); _this.playSfx("explode"); } return true; }); this.processInput(dt); this.processQueuedActions(); this.computeAtomPointedAt(); step_count = Math.floor(dt / (1 / 60)); if (step_count < 1) { step_count = 1; } delta = dt / step_count; for (i = _i = 0; 0 <= step_count ? _i < step_count : _i > step_count; i = 0 <= step_count ? ++_i : --_i) { this.space.step(delta); } if (this.want_to_remove_claw_pin) { _ref = this.claw_pins; for (_j = 0, _len = _ref.length; _j < _len; _j++) { pin = _ref[_j]; this.space.removeConstraint(pin); } this.claw_pins = null; this.want_to_remove_claw_pin = false; } this.computeArmPos(); return this.man.body.setAngle(this.man_angle); }; Tank.prototype.removeAtom = function(atom) { atom.cleanUp(); return this.atoms.remove(atom); }; Tank.prototype.removeBomb = function(bomb) { bomb.cleanUp(); return this.bombs.remove(bomb); }; Tank.prototype.initWalls = function() { var body, borders, p1, p2, r, shape, _i, _len, _ref; r = 50; borders = [[vec2d(this.size.x + r, this.size.y), vec2d(this.size.x + r, 0)], [vec2d(this.size.x, -r), vec2d(0, -r)], [vec2d(-r, 0), vec2d(-r, this.size.y)]]; for (_i = 0, _len = borders.length; _i < _len; _i++) { _ref = borders[_i], p1 = _ref[0], p2 = _ref[1]; body = new cp.Body(Infinity, Infinity); body.nodeIdleTime = Infinity; shape = new cp.SegmentShape(body, p1, p2, r); shape.setFriction(0.99); shape.setElasticity(0.0); shape.collision_type = Collision.Default; this.space.addStaticShape(shape); } }; Tank.prototype.initCeiling = function() { var body; body = new cp.Body(10000, 100000); body.setPos(vec2d(this.size.x / 2, this.size.y * 1.5)); this.ceiling = new cp.BoxShape(body, this.size.x, this.size.y); this.ceiling.collision_type = Collision.Default; this.space.addShape(this.ceiling); return this.max_ceiling_delta = 200; }; Tank.prototype.adjustCeiling = function(dt) { var adjust, amount, direction, new_sign, new_y, other_points, target_y; if (this.game.server != null) { other_points = this.other_tank.points; } else { other_points = this.game.survival_points; } adjust = (this.points - other_points) / this.points_to_crush * this.size.y; if (adjust > 0) { adjust = 0; } if (this.game_over) { adjust = 0; } target_y = this.size.y * 1.5 + adjust; direction = sign(target_y - this.ceiling.body.p.y); amount = this.max_ceiling_delta * dt; new_y = this.ceiling.body.p.y + amount * direction; new_sign = sign(target_y - new_y); if (direction === -new_sign) { return this.ceiling.body.setPos(vec2d(this.ceiling.body.p.x, target_y)); } else { return this.ceiling.body.setPos(vec2d(this.ceiling.body.p.x, new_y)); } }; Tank.prototype.initMan = function(pos, vel) { var shape; if (pos == null) { pos = vec2d(this.size.x / 2, this.man_size.y / 2); } if (vel == null) { vel = vec2d(0, 0); } shape = new cp.BoxShape(new cp.Body(20, 10000000), this.man_size.x, this.man_size.y); shape.body.setPos(pos); shape.body.setVel(vel); shape.body.w_limit = 0; this.man_angle = shape.body.a; shape.setElasticity(0); shape.setFriction(3.0); shape.collision_type = Collision.Default; this.space.addBody(shape.body); this.space.addShape(shape); return this.man = shape; }; Tank.prototype.computeArmPos = function() { this.arm_pos = this.man.body.p.minus(this.man_size.scaled(0.5)).plus(this.arm_offset); this.point_vector = (this.mousePos.minus(this.arm_pos)).normalized(); return this.point_start = this.arm_pos.plus(this.point_vector.scaled(this.arm_len)); }; Tank.prototype.getDropPos = function(size) { return vec2d(Math.random() * (this.size.x - size.x) + size.x / 2, this.ceiling.body.p.y - this.size.y / 2 - size.y / 2); }; Tank.prototype.dropBomb = function() { var bomb, pos, sprite, timeout; pos = this.getDropPos(Bomb.size); sprite = new Sprite('bomb', { batch: this.game.batch, zOrder: this.game.group_main }); timeout = randInt(1, 5); bomb = new Bomb(pos, sprite, this.space, timeout); return this.bombs.add(bomb); }; Tank.prototype.dropRock = function() { var pos, rock, sprite; pos = this.getDropPos(Rock.size); sprite = new Sprite('rock', { batch: this.game.batch, zOrder: this.game.group_main }); rock = new Rock(pos, sprite, this.space); return this.rocks.add(rock); }; Tank.prototype.computeDrops = function(dt) { var atom, flavor_index, pos; if (this.game_over) { return; } this.time_until_next_drop -= dt; if (this.time_until_next_drop <= 0) { this.time_until_next_drop += this.time_between_drops; flavor_index = randInt(0, Atom.flavor_count - 1); pos = this.getDropPos(atom_size); atom = new Atom(pos, flavor_index, new Sprite(this.game.atom_imgs[flavor_index], { batch: this.game.batch, zOrder: this.game.group_main }), this.space); return this.atoms.add(atom); } }; Tank.prototype.lose = function() { if (this.game_over) { return; } this.game_over = true; this.winner = false; this.explodeAtoms(this.atoms.clone(), "atomfail"); this.sprite_man.setAnimationName("defeat"); this.sprite_man.setFrameIndex(0); this.sprite_arm.setVisible(false); this.retractClaw(); if (this.other_tank != null) { this.other_tank.win(); } return this.playSfx("defeat"); }; Tank.prototype.win = function() { if (this.game_over) { return; } this.game_over = true; this.winner = true; this.explodeAtoms(this.atoms.clone()); this.sprite_man.setAnimationName("victory"); this.sprite_man.setFrameIndex(0); this.sprite_arm.setVisible(false); this.retractClaw(); if (this.other_tank != null) { this.other_tank.lose(); } return this.playSfx("victory"); }; Tank.prototype.explodeAtom = function(atom, animationName) { var clearSprite, _this = this; if (animationName == null) { animationName = "asplosion"; } if (atom === this.ray_atom) { this.ray_atom = null; } if ((this.claw_pins != null) && this.claw_pins[0].b === atom.shape.body) { this.unattachClaw(); } atom.marked_for_deletion = true; clearSprite = function() { return _this.removeAtom(atom); }; atom.sprite.setAnimationName(animationName); atom.sprite.setFrameIndex(0); return atom.sprite.on("animationend", clearSprite); }; Tank.prototype.explodeAtoms = function(atoms, animationName) { var atom, _i, _len, _results, _this = this; if (animationName == null) { animationName = "asplosion"; } if (atoms instanceof Set) { return atoms.each(function(atom) { return _this.explodeAtom(atom, animationName); }); } else { _results = []; for (_i = 0, _len = atoms.length; _i < _len; _i++) { atom = atoms[_i]; _results.push(this.explodeAtom(atom, animationName)); } return _results; } }; Tank.prototype.processInput = function(dt) { var air_move_boost, air_move_force, ani_name, animationName, arm_animation, bb, body, btn, claw_dist, claw_reel_in_speed, claw_reel_out_speed, ctrl, delta, feet_end, feet_start, flip, ground_shapes, grounded, grounded_move_boost, grounded_move_force, max_speed, move_boost, move_force, move_left, move_right, not_moving_x, power, shape, vector, _i, _len, _ref; if (this.game_over) { return; } this.control_state = []; _ref = this.controls; for (btn in _ref) { ctrl = _ref[btn]; this.control_state[ctrl] = this.game.engine.buttonState(btn); } feet_start = this.man.body.p.minus(this.man_size.scaled(0.5)).offset(1, -1); feet_end = feet_start.offset(this.man_size.x - 2, -2); bb = new cp.BB(feet_start.x, feet_end.y, feet_end.x, feet_start.y); ground_shapes = []; this.space.bbQuery(bb, -1, null, function(shape) { return ground_shapes.push(shape); }); grounded = ground_shapes.length > 0; grounded_move_force = 1000; not_moving_x = Math.abs(this.man.body.vx) < 5.0; air_move_force = 200; grounded_move_boost = 30; air_move_boost = 0; move_force = grounded ? grounded_move_force : air_move_force; move_boost = grounded ? grounded_move_boost : air_move_boost; max_speed = 200; move_left = this.control_state[Control.MoveLeft] && !this.control_state[Control.MoveRight]; move_right = this.control_state[Control.MoveRight] && !this.control_state[Control.MoveLeft]; if (move_left) { if (this.man.body.vx >= -max_speed && this.man.body.p.x - this.man_size.x / 2 - 5 > 0) { this.man.body.applyImpulse(vec2d(-move_force, 0), vec2d(0, 0)); if (this.man.body.vx > -move_boost && this.man.body.vx < 0) { this.man.body.vx = -move_boost; } } } else if (move_right) { if (this.man.body.vx <= max_speed && this.man.body.p.x + this.man_size.x / 2 + 3 < this.size.x) { this.man.body.applyImpulse(vec2d(move_force, 0), vec2d(0, 0)); if (this.man.body.vx < move_boost && this.man.body.vx > 0) { this.man.body.vx = move_boost; } } } flip = this.mousePos.x < this.man.body.p.x ? -1 : 1; this.sprite_arm.scale.x = this.sprite_man.scale.x = flip; if (grounded) { if (move_left || move_right) { animationName = "walk"; } else { animationName = "still"; } } else { animationName = "jump"; } if (this.control_state[Control.MoveUp] && grounded) { animationName = "jump"; this.sprite_man.setAnimationName(animationName); this.sprite_man.setFrameIndex(0); this.man.body.vy = 100; this.man.body.applyImpulse(vec2d(0, 2000), vec2d(0, 0)); power = 1000 / ground_shapes.length; for (_i = 0, _len = ground_shapes.length; _i < _len; _i++) { shape = ground_shapes[_i]; shape.body.applyImpulse(vec2d(0, -power), vec2d(0, 0)); } this.playSfx('jump'); } this.sprite_man.setAnimationName(animationName); if (this.control_state[Control.SwitchToGrapple] && this.equipped_gun !== Control.SwitchToGrapple) { this.equipped_gun = Control.SwitchToGrapple; this.playSfx('switch_weapon'); } else if (this.control_state[Control.SwitchToRay] && this.equipped_gun !== Control.SwitchToRay) { this.equipped_gun = Control.SwitchToRay; this.playSfx('switch_weapon'); } else if (this.control_state[Control.SwitchToLazer] && this.equipped_gun !== Control.SwitchToLazer) { this.equipped_gun = Control.SwitchToLazer; this.playSfx('switch_weapon'); } if (this.equipped_gun === Control.SwitchToGrapple) { if (this.claw_in_motion) { ani_name = "arm_flung"; } else { ani_name = "arm"; } arm_animation = ani_name; } else { arm_animation = this.gun_animations[this.equipped_gun]; } this.sprite_arm.setAnimationName(arm_animation); if (this.equipped_gun === Control.SwitchToGrapple) { claw_reel_in_speed = 400; claw_reel_out_speed = 200; if (!this.want_to_remove_claw_pin && !this.want_to_retract_claw && this.let_go_of_fire_main && this.control_state[Control.FireMain] && !this.claw_in_motion) { this.let_go_of_fire_main = false; this.claw_in_motion = true; this.sprite_claw.setVisible(true); body = new cp.Body(5, 1000000); body.setPos(vec2d(this.point_start)); body.setAngle(this.point_vector.angle()); body.setVel(vec2d(this.man.body.vx, this.man.body.vy).plus(this.point_vector.scaled(this.claw_shoot_speed))); this.claw = new cp.CircleShape(body, this.claw_radius, vec2d()); this.claw.setFriction(1); this.claw.setElasticity(0); this.claw.collision_type = Collision.Claw; this.claw_joint = new cp.SlideJoint(this.claw.body, this.man.body, vec2d(0, 0), vec2d(0, 0), 0, this.size.length()); this.claw_joint.maxBias = max_bias; this.space.addBody(body); this.space.addShape(this.claw); this.space.addConstraint(this.claw_joint); this.playSfx('shoot_claw'); } if (this.sprite_claw.visible) { claw_dist = this.claw.body.p.minus(this.man.body.p).length(); } if (this.control_state[Control.FireMain] && this.claw_in_motion) { if (claw_dist < this.min_claw_dist + 8) { if (this.claw_pins != null) { this.want_to_retract_claw = true; this.let_go_of_fire_main = false; } else if (this.claw_attached && this.let_go_of_fire_main) { this.retractClaw(); this.let_go_of_fire_main = false; } } else if (claw_dist > this.min_claw_dist) { if (this.claw_attached && this.claw_joint.max > claw_dist) { this.claw_joint.max = claw_dist; } else { this.claw_joint.max -= claw_reel_in_speed * dt; if (this.claw_joint.max < this.min_claw_dist) { this.claw_joint.max = this.min_claw_dist; } } } } if (this.control_state[Control.FireAlt] && this.claw_attached) { this.unattachClaw(); } } this.lazer_recharge -= dt; if (this.equipped_gun === Control.SwitchToLazer) { if (this.lazer_line != null) { this.lazer_line[0] = this.point_start; } if (this.control_state[Control.FireMain] && this.lazer_recharge <= 0) { this.lazer_recharge = this.lazer_timeout; this.lazer_line = [this.point_start, this.point_end]; this.lazer_line_timeout = this.lazer_line_timeout_start; if (this.closest_atom != null) { this.explodeAtom(this.closest_atom, "atomfail"); this.closest_atom = null; } this.playSfx('lazer'); } } this.lazer_line_timeout -= dt; if (this.lazer_line_timeout <= 0) { this.lazer_line = null; } if (this.ray_atom != null) { vector = this.point_start.minus(this.ray_atom.shape.body.p); delta = vector.normalized().scaled(1000 * dt); if (delta.length() > vector.length()) { this.ray_atom.shape.body.setPos(this.point_start); } else { this.ray_atom.shape.body.setPos(this.ray_atom.shape.body.p.plus(delta)); } } if (this.equipped_gun === Control.SwitchToRay) { if ((this.control_state[Control.FireMain] && this.let_go_of_fire_main) && (this.closest_atom != null) && (this.ray_atom == null) && !this.closest_atom.marked_for_deletion) { this.ray_atom = this.closest_atom; this.ray_atom.rogue = true; this.closest_atom = null; this.space.removeBody(this.ray_atom.shape.body); this.let_go_of_fire_main = false; this.ray_atom.unbond(); this.playSfx('ray'); } else if (((this.control_state[Control.FireMain] && this.let_go_of_fire_main) || this.control_state[Control.FireAlt]) && (this.ray_atom != null)) { this.space.addBody(this.ray_atom.shape.body); this.ray_atom.rogue = false; if (this.control_state[Control.FireMain]) { this.ray_atom.shape.body.setVel(vec2d(this.man.body.vx, this.man.body.vy).plus(this.point_vector.scaled(this.ray_shoot_speed))); this.playSfx('lazer'); } else { this.ray_atom.shape.body.setVel(vec2d(this.man.body.vx, this.man.body.vy)); } this.ray_atom = null; this.let_go_of_fire_main = false; } } if (!this.control_state[Control.FireMain]) { this.let_go_of_fire_main = true; if (this.want_to_retract_claw) { this.want_to_retract_claw = false; this.retractClaw(); } } if (!this.control_state[Control.FireAlt] && !this.let_go_of_fire_alt) { return this.let_go_of_fire_alt = true; } }; Tank.prototype.processQueuedActions = function() { var atom1, atom2, bond_loop, len_bond_loop, pin, _i, _j, _len, _len1, _ref, _ref1, _ref2; if (this.claw_pins_to_add != null) { this.claw_pins = this.claw_pins_to_add; this.claw_pins_to_add = null; _ref = this.claw_pins; for (_i = 0, _len = _ref.length; _i < _len; _i++) { pin = _ref[_i]; this.space.addConstraint(pin); } } _ref1 = this.bond_queue; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { _ref2 = _ref1[_j], atom1 = _ref2[0], atom2 = _ref2[1]; if (atom1.marked_for_deletion || atom2.marked_for_deletion) { continue; } if (atom1 === this.ray_atom || atom2 === this.ray_atom) { continue; } if ((atom1.bonds == null) || (atom2.bonds == null)) { print("Warning: trying to bond with an atom that doesn't exist anymore"); continue; } if (atom1.bondTo(atom2)) { bond_loop = atom1.bondLoop(); if (bond_loop != null) { len_bond_loop = bond_loop.length; if (this.enable_point_calculation) { this.points += len_bond_loop; } this.explodeAtoms(bond_loop); this.queued_asplosions.push([atom1.flavor_index, len_bond_loop]); this.playSfx("merge"); } else { this.playSfx("bond"); } } } return this.bond_queue = []; }; Tank.prototype.clawHitSomething = function(arbiter, space) { var claw, claw_anchor, claw_delta, claw_pin, pos, shape, shape_anchor, _i, _len, _ref; if (this.claw_attached) { return; } claw = arbiter.a; shape = arbiter.b; pos = vec2d(arbiter.contacts[0].p); shape_anchor = pos.minus(shape.body.p); claw_anchor = pos.minus(claw.body.p); claw_delta = claw_anchor.normalized().scaled(-(this.claw_radius + 8)); this.claw.body.setPos(this.claw.body.p.plus(claw_delta)); this.claw_pins_to_add = [new cp.PinJoint(claw.body, shape.body, claw_anchor, shape_anchor), new cp.PinJoint(claw.body, shape.body, vec2d(0, 0), vec2d(0, 0))]; _ref = this.claw_pins_to_add; for (_i = 0, _len = _ref.length; _i < _len; _i++) { claw_pin = _ref[_i]; claw_pin.maxBias = max_bias; } this.claw_attached = true; return this.playSfx("claw_hit"); }; Tank.prototype.atomHitAtom = function(arbiter, space) { var atom1, atom2; atom1 = arbiter.a.atom; atom2 = arbiter.b.atom; if (atom1.flavor_index === atom2.flavor_index) { return this.bond_queue.push([atom1, atom2]); } }; Tank.prototype.playSfx = function(name) { if (this.sfx_enabled && (this.game.sfx != null)) { return this.game.sfx[name].play(); } }; Tank.prototype.retractClaw = function() { if (!this.sprite_claw.visible) { return; } this.claw_in_motion = false; this.sprite_claw.setVisible(false); this.sprite_arm.setAnimationName("arm"); this.claw_attached = false; this.space.removeBody(this.claw.body); this.space.removeShape(this.claw); this.space.removeConstraint(this.claw_joint); this.claw = null; this.unattachClaw(); return this.playSfx("retract"); }; Tank.prototype.unattachClaw = function() { if (this.claw_pins != null) { return this.want_to_remove_claw_pin = true; } }; Tank.prototype.computeAtomPointedAt = function() { var closest_dist, slope, y_intercept, _this = this; if (this.equipped_gun === Control.SwitchToGrapple) { this.closest_atom = null; } else { this.closest_atom = null; closest_dist = null; this.atoms.each(function(atom) { var a, b, c, discriminant, dist, f; if (atom.marked_for_deletion) { return true; } f = atom.shape.body.p.minus(_this.point_start); if (sign(f.x) !== sign(_this.point_vector.x) || sign(f.y) !== sign(_this.point_vector.y)) { return true; } a = _this.point_vector.dot(_this.point_vector); b = 2 * f.dot(_this.point_vector); c = f.dot(f) - atom_radius * atom_radius; discriminant = b * b - 4 * a * c; if (discriminant < 0) { return true; } dist = atom.shape.body.p.distanceSqrd(_this.point_start); if ((_this.closest_atom == null) || dist < closest_dist) { _this.closest_atom = atom; closest_dist = dist; } return true; }); } if (this.closest_atom != null) { return this.point_end = this.closest_atom.shape.body.p.clone(); } else { slope = this.point_vector.y / (this.point_vector.x + 0.00000001); y_intercept = this.point_start.y - slope * this.point_start.x; this.point_end = this.point_start.plus(this.point_vector.scaled(this.size.length())); if (this.point_end.x > this.size.x) { this.point_end.x = this.size.x; this.point_end.y = slope * this.point_end.x + y_intercept; } if (this.point_end.x < 0) { this.point_end.x = 0; this.point_end.y = slope * this.point_end.x + y_intercept; } if (this.point_end.y > this.ceiling.body.p.y - this.size.y / 2) { this.point_end.y = this.ceiling.body.p.y - this.size.y / 2; this.point_end.x = (this.point_end.y - y_intercept) / slope; } if (this.point_end.y < 0) { this.point_end.y = 0; return this.point_end.x = (this.point_end.y - y_intercept) / slope; } } }; Tank.prototype.respond_to_asplosion = function(asplosion) { var flavor, i, power, quantity, _i, _j, _results, _results1; flavor = asplosion[0], quantity = asplosion[1]; power = quantity - this.min_power; if (power <= 0) { return; } if (flavor <= 3) { _results = []; for (i = _i = 0; 0 <= power ? _i < power : _i > power; i = 0 <= power ? ++_i : --_i) { _results.push(this.dropBomb()); } return _results; } else { _results1 = []; for (i = _j = 0; 0 <= power ? _j < power : _j > power; i = 0 <= power ? ++_j : --_j) { _results1.push(this.dropRock()); } return _results1; } }; Tank.prototype.moveSprites = function() { var drawDrawable, _this = this; drawDrawable = function(drawable) { drawable.sprite.pos = drawable.shape.body.p.plus(_this.pos); drawable.sprite.pos.y = 600 - drawable.sprite.pos.y; drawable.sprite.rotation = -drawable.shape.body.rot.angle(); return true; }; this.atoms.each(drawDrawable); this.bombs.each(drawDrawable); this.rocks.each(drawDrawable); this.sprite_man.pos = this.man.body.p.plus(this.pos); this.sprite_man.pos.y = 600 - this.sprite_man.pos.y; this.sprite_man.rotation = -this.man.body.rot.angle(); this.sprite_arm.pos = this.arm_pos.plus(this.pos); this.sprite_arm.pos.y = 600 - this.sprite_arm.pos.y; this.sprite_arm.rotation = -this.mousePos.minus(this.man.body.p).angle(); if (this.mousePos.x < this.man.body.p.x) { this.sprite_arm.rotation = Math.PI - this.sprite_arm.rotation; } this.sprite_tank.pos = this.pos.plus(this.ceiling.body.p); this.sprite_tank.pos.y = 600 - this.sprite_tank.pos.y; if (this.sprite_claw.visible) { this.sprite_claw.pos = this.claw.body.p.plus(this.pos); this.sprite_claw.pos.y = 600 - this.sprite_claw.pos.y; return this.sprite_claw.rotation = -this.claw.body.rot.angle(); } }; Tank.prototype.drawPrimitives = function(context) { var claw_pin, end, invert_y, start, _i, _len, _ref, _ref1, _this = this; if (!this.game_over) { context.fillStyle = '#000000'; this.drawLine(context, this.point_start.plus(this.pos), this.point_end.plus(this.pos), [0, 0, 0, 0.23]); if (this.sprite_claw.visible) { invert_y = this.sprite_claw.pos.clone(); invert_y.y = 600 - invert_y.y; this.drawLine(context, this.point_start.plus(this.pos), invert_y, [255, 255, 0, 1]); } this.atoms.each(function(atom) { if (atom.marked_for_deletion) { return true; } atom.bonds.each(function(other, joint) { _this.drawLine(context, _this.pos.plus(atom.shape.body.p), _this.pos.plus(other.shape.body.p), [0, 0, 255, 1]); return true; }); return true; }); if (this.game.debug) { if (this.claw_pins) { _ref = this.claw_pins; for (_i = 0, _len = _ref.length; _i < _len; _i++) { claw_pin = _ref[_i]; this.drawLine(context, this.pos.plus(claw_pin.a.p).plus(claw_pin.anchr1), this.pos.plus(claw_pin.b.p).plus(claw_pin.anchr2), [255, 0, 255, 1]); } } } if (this.lazer_line != null) { _ref1 = this.lazer_line, start = _ref1[0], end = _ref1[1]; return this.drawLine(context, start.plus(this.pos), end.plus(this.pos), [255, 0, 0, 1]); } } }; Tank.prototype.drawLine = function(context, p1, p2, color) { context.strokeStyle = "rgba(" + color[0] + ", " + color[1] + ", " + color[2] + ", " + color[3] + ")"; context.beginPath(); context.moveTo(p1.x, 600 - p1.y); context.lineTo(p2.x, 600 - p2.y); context.closePath(); return context.stroke(); }; return Tank; })(); Game = (function() { function Game(gw, engine, server) { var i, pos, tank_dims, tank_index, tank_name, tank_pos; this.gw = gw; this.engine = engine; this.server = server; this.draw = __bind(this.draw, this); this.update = __bind(this.update, this); this.debug = params.debug != null; this.batch = new Batch(); this.group_bg = 0; this.group_main = 1; this.group_fg = 2; this.sprite_bg = new Sprite("bg", { batch: this.batch, zOrder: this.group_bg }); this.sprite_bg_top = new Sprite("bg_top", { batch: this.batch, zOrder: this.group_fg }); this.atom_imgs = (function() { var _i, _ref, _results; _results = []; for (i = _i = 0, _ref = Atom.flavor_count; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { _results.push("atom" + i); } return _results; })(); if (params.nofx == null) { this.sfx = { 'jump': new Sound('sfx/jump__dave-des__fast-simple-chop-5.ogg'), 'atom_hit_atom': new Sound('sfx/atomscolide__batchku__colide-18-005.ogg'), 'ray': new Sound('sfx/raygun__owyheesound__decelerate-discharge.ogg'), 'lazer': new Sound('sfx/lazer__supraliminal__laser-short.ogg'), 'merge': new Sound('sfx/atomsmerge__tigersound__disappear.ogg'), 'bond': new Sound('sfx/bond.ogg'), 'victory': new Sound('sfx/victory__iut-paris8__labbefabrice-2011-01.ogg'), 'defeat': new Sound('sfx/defeat__freqman__lostspace.ogg'), 'switch_weapon': new Sound('sfx/switchweapons__erdie__metallic-weapon-low.ogg'), 'explode': new Sound('sfx/atomsexplode3-1.ogg'), 'claw_hit': new Sound('sfx/shootingtheclaw__smcameron__rocks2.ogg'), 'shoot_claw': new Sound('sfx/landonsurface__juskiddink__thud-dry.ogg'), 'retract': new Sound('sfx/clawcomesback__simon-rue__studs-moln-v4.ogg') }; } else { this.sfx = null; } this.fps_display = params.fps != null; tank_dims = vec2d(12, 16); tank_pos = [vec2d(109, 41), vec2d(531, 41)]; if (this.server == null) { this.tanks = [new Tank(tank_pos[0], tank_dims, this)]; this.control_tank = this.tanks[0]; this.survival_points = 0; this.survival_point_timeout = params.hard || 10; this.next_survival_point = this.survival_point_timeout; this.weapon_drop_interval = params.bomb || 10; tank_index = 1 - this.control_tank.tank_index; tank_name = "tank" + tank_index; this.sprite_other_tank = new Sprite(tank_name, { batch: this.batch, zOrder: this.group_main, pos: vec2d(tank_pos[1].x + this.control_tank.size.x / 2, tank_pos[1].y + this.control_tank.size.y / 2) }); this.sprite_other_tank.pos.y = 600 - this.sprite_other_tank.pos.y; } else { this.tanks = (function() { var _i, _len, _results; _results = []; for (i = _i = 0, _len = tank_pos.length; _i < _len; i = ++_i) { pos = tank_pos[i]; _results.push(new Tank(pos, tank_dims, this, i)); } return _results; }).call(this); this.control_tank = this.tanks[0]; this.enemy_tank = this.tanks[1]; this.control_tank.other_tank = this.enemy_tank; this.enemy_tank.other_tank = this.control_tank; this.enemy_tank.atom_drop_enabled = false; this.enemy_tank.enable_point_calculation = false; this.enemy_tank.sfx_enabled = false; } this.engine.on('draw', this.draw); this.engine.on('update', this.update); this.state_render_timeout = 0.3; this.next_state_render = this.state_render_timeout; } Game.prototype.update = function(dt) { var data, mousePos, msg_name, n, new_number, old_number, tank, _i, _j, _len, _len1, _ref, _ref1, _ref2, _results; mousePos = this.engine.mousePos.clone(); mousePos.y = 600 - mousePos.y; _ref = this.tanks; for (_i = 0, _len = _ref.length; _i < _len; _i++) { tank = _ref[_i]; tank.mousePos = mousePos.minus(tank.pos); tank.update(dt); } if (this.server == null) { this.next_survival_point -= dt; if (this.next_survival_point <= 0) { this.next_survival_point += this.survival_point_timeout; old_number = Math.floor(this.survival_points / this.weapon_drop_interval); this.survival_points += randInt(3, 6); new_number = Math.floor(this.survival_points / this.weapon_drop_interval); if (new_number > old_number) { n = randInt(1, 2); if (n === 1) { this.control_tank.dropBomb(); } else { this.control_tank.dropRock(); } } } } if (this.server != null) { this.next_state_render -= dt; if (this.next_state_render <= 0) { this.next_state_render = this.state_render_timeout; this.server.send_msg("StateUpdate", this.control_tank.serialize_state()); _ref1 = this.server.get_messages(); _results = []; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { _ref2 = _ref1[_j], msg_name = _ref2[0], data = _ref2[1]; if (msg_name === 'StateUpdate') { _results.push(this.enemy_tank.restore_state(data)); } else if (msg_name === 'YourOpponentLeftSorryBro') { print("you win - your opponent disconnected."); _results.push(this.control_tank.win()); } else { _results.push(void 0); } } return _results; } } }; Game.prototype.draw = function(context) { var tank, _i, _j, _len, _len1, _ref, _ref1; context.fillStyle = '#000000'; context.fillRect(0, 0, this.engine.size.x, this.engine.size.y); _ref = this.tanks; for (_i = 0, _len = _ref.length; _i < _len; _i++) { tank = _ref[_i]; tank.moveSprites(); } this.engine.draw(this.batch); _ref1 = this.tanks; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { tank = _ref1[_j]; tank.drawPrimitives(context); } if (this.fps_display) { return this.engine.drawFps(); } }; return Game; })(); GameWindow = (function() { function GameWindow(engine, server) { this.engine = engine; this.server = server; this.controls = __bind(this.controls, this); this.credits = __bind(this.credits, this); this.play = __bind(this.play, this); this.title = __bind(this.title, this); this.endCurrent = __bind(this.endCurrent, this); this.current = null; } GameWindow.prototype.endCurrent = function() { if (this.current != null) { this.current.end(); } return this.current = null; }; GameWindow.prototype.title = function() { this.endCurrent(); return this.current = new Title(this, this.engine, this.server); }; GameWindow.prototype.play = function(server_on) { var server; if (server_on == null) { server_on = true; } server = server_on ? this.server : null; this.endCurrent(); return this.current = new Game(this, this.engine, server); }; GameWindow.prototype.credits = function() { this.endCurrent(); return this.current = new Credits(this, this.engine); }; GameWindow.prototype.controls = function() { this.endCurrent(); return this.current = new ControlsScene(this, this.engine); }; return GameWindow; })(); ControlsScene = (function() { function ControlsScene(gw, engine) { this.gw = gw; this.engine = engine; this.onButtonDown = __bind(this.onButtonDown, this); this.end = __bind(this.end, this); this.draw = __bind(this.draw, this); this.batch = new Batch(); this.img = new Sprite("howtoplay", { batch: this.batch }); this.engine.on('draw', this.draw); this.engine.on('buttonup', this.onButtonDown); } ControlsScene.prototype.draw = function(context) { return this.engine.draw(this.batch); }; ControlsScene.prototype.end = function() { this.engine.removeListener('draw', this.draw); return this.engine.removeListener('buttonup', this.onButtonDown); }; ControlsScene.prototype.onButtonDown = function() { return this.gw.title(); }; return ControlsScene; })(); Credits = (function() { function Credits(gw, engine) { this.gw = gw; this.engine = engine; this.onButtonDown = __bind(this.onButtonDown, this); this.end = __bind(this.end, this); this.draw = __bind(this.draw, this); this.batch = new Batch(); this.img = new Sprite("credits", { batch: this.batch }); this.engine.on('draw', this.draw); this.engine.on('buttonup', this.onButtonDown); } Credits.prototype.draw = function() { return this.engine.draw(this.batch); }; Credits.prototype.end = function() { this.engine.removeListener('draw', this.draw); this.engine.removeListener('buttonup', this.onButtonDown); return this.engine.removeListener('update', this.update); }; Credits.prototype.onButtonDown = function(pos) { return this.gw.title(); }; return Credits; })(); Title = (function() { function Title(gw, engine, server) { var font_size, x, y; this.gw = gw; this.engine = engine; this.server = server; this.onButtonDown = __bind(this.onButtonDown, this); this.end = __bind(this.end, this); this.draw = __bind(this.draw, this); this.update = __bind(this.update, this); this.createLabels = __bind(this.createLabels, this); this.engine.on('buttonup', this.onButtonDown); this.engine.on('draw', this.draw); this.engine.on('update', this.update); this.batch = new Batch(); this.img = new Sprite("title", { batch: this.batch }); this.start_pos = vec2d(409, 600 - 305); this.credits_pos = vec2d(360, 600 - 229); this.controls_pos = vec2d(525, 600 - 242); this.click_radius = 50; this.lobby_pos = vec2d(746, 600 - 203); this.lobby_size = vec2d(993.0 - this.lobby_pos.x, 522.0 - this.lobby_pos.y); if (this.server != null) { this.labels = []; this.users = []; this.nick_label = {}; this.nick_user = {}; this.nick = "Guest " + (randInt(1, 99999)); this.server.send_msg("UpdateNick", this.nick); this.my_nick_label = pyglet.text.Label(this.nick, font_size = 16, x = 748, y = 137); this.challenged = {}; } } Title.prototype.createLabels = function() { var font_size, h, label, next_pos, nick, text, user, x, y, _i, _len, _ref, _ref1, _results; this.labels = []; this.nick_label = {}; this.nick_user = {}; h = 18; next_pos = this.lobby_pos.offset(0, this.lobby_size.y - h); _ref = this.users; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { user = _ref[_i]; nick = user['nick']; if (nick === this.nick) { continue; } text = nick; if (user['playing'] != null) { text += " (playing vs " + user['playing'] + ")"; } else if (_ref1 = this.nick, __indexOf.call(user['want_to_play'], _ref1) >= 0) { text += " (click to accept challenge)"; } else if (__indexOf.call(this.challenged, nick) >= 0) { text += " (challenge sent)"; } else { text += " (click to challenge)"; } label = pyglet.text.Label(text, font_size = 13, x = next_pos.x, y = next_pos.y); this.nick_label[nick] = label; this.nick_user[nick] = user; next_pos.y -= h; _results.push(this.labels.push(label)); } return _results; }; Title.prototype.update = function(dt) { var name, payload, _i, _len, _ref, _ref1; if (this.server != null) { _ref = server.get_messages(); for (_i = 0, _len = _ref.length; _i < _len; _i++) { _ref1 = _ref[_i], name = _ref1[0], payload = _ref1[1]; if (name === 'LobbyList') { this.users = payload; this.createLabels(); } else if (name === 'StartGame') { this.gw.play(); return; } } } }; Title.prototype.draw = function() { var label, _i, _len, _ref; this.engine.draw(this.batch); if (this.server != null) { _ref = this.labels; for (_i = 0, _len = _ref.length; _i < _len; _i++) { label = _ref[_i]; label.draw(); } return this.my_nick_label.draw(); } }; Title.prototype.end = function() { this.engine.removeListener('draw', this.draw); this.engine.removeListener('buttonup', this.onButtonDown); return this.engine.removeListener('update', this.update); }; Title.prototype.onButtonDown = function(button) { var click_pos, label, label_pos, label_size, nick, user, _ref, _ref1; click_pos = this.engine.mousePos; if (click_pos.distance(this.start_pos) < this.click_radius) { this.gw.play(false); return; } else if (click_pos.distance(this.credits_pos) < this.click_radius) { this.gw.credits(); return; } else if (click_pos.distance(this.controls_pos) < this.click_radius) { this.gw.controls(); return; } else if (button === button.KeySpace) { this.gw.play(false); return; } if (this.server != null) { _ref = this.nick_label; for (nick in _ref) { label = _ref[nick]; label_pos = vec2d(label.x, label.y); label_size = vec2d(200, 18); if (click_pos.x > label_pos.x && click_pos.y > label_pos.y && click_pos.x < label_pos.x + label_size.x && click_pos.y < label_pos.y + label_size.y) { user = this.nick_user[nick]; if (user == null) { print("warn missing nick" + nick); return; } if (!user['playing']) { if (_ref1 = this.nick, __indexOf.call(user['want_to_play'], _ref1) >= 0) { this.server.send_msg("AcceptPlayRequest", nick); } else { this.server.send_msg("PlayRequest", nick); this.challenged[nick] = true; this.createLabels(); } } return; } } } }; return Title; })(); params = (function() { var key, obj, pair, value, _i, _len, _ref, _ref1; obj = {}; _ref = location.search.substring(1).split("&"); for (_i = 0, _len = _ref.length; _i < _len; _i++) { pair = _ref[_i]; _ref1 = pair.split("="), key = _ref1[0], value = _ref1[1]; obj[unescape(key)] = unescape(value); } return obj; })(); _ref = vec2d.Vec2d.prototype; for (prop in _ref) { val = _ref[prop]; cp.Vect.prototype[prop] = val; } canvas = document.getElementById("game"); chem.onReady(function() { var engine, w; engine = new Engine(canvas); w = new GameWindow(engine, null); w.title(); engine.start(); return canvas.focus(); }); },{"chem":11,"chipmunk":13}],13:[function(require,module,exports){ (function(){(function(){ /* Copyright (c) 2007 Scott Lembcke * * 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. */ Object.create = Object.create || function(o) { function F() {} F.prototype = o; return new F(); }; //var VERSION = CP_VERSION_MAJOR + "." + CP_VERSION_MINOR + "." + CP_VERSION_RELEASE; var cp; if(typeof exports === 'undefined'){ cp = {}; if(typeof window === 'object'){ window.cp = cp; } } else { cp = exports; } var assert = function(value, message) { if (!value) { throw new Error('Assertion failed: ' + message); } }; var assertSoft = function(value, message) { if(!value && console && console.warn) { console.warn("ASSERTION FAILED: " + message); if(console.trace) { console.trace(); } } }; var mymin = function(a, b) { return a < b ? a : b; }; var mymax = function(a, b) { return a > b ? a : b; }; var min, max; if (typeof window === 'object' && window.navigator.userAgent.indexOf('Firefox') > -1){ // On firefox, Math.min and Math.max are really fast: // http://jsperf.com/math-vs-greater-than/8 min = Math.min; max = Math.max; } else { // On chrome and safari, Math.min / max are slooow. The ternery operator above is faster // than the builtins because we only have to deal with 2 arguments that are always numbers. min = mymin; max = mymax; } /* The hashpair function takes two numbers and returns a hash code for them. * Required that hashPair(a, b) === hashPair(b, a). * Chipmunk's hashPair function is defined as: * #define CP_HASH_COEF (3344921057ul) * #define CP_HASH_PAIR(A, B) ((cpHashValue)(A)*CP_HASH_COEF ^ (cpHashValue)(B)*CP_HASH_COEF) * But thats not suitable in javascript because multiplying by a large number will make the number * a large float. * * The result of hashPair is used as the key in objects, so it returns a string. */ var hashPair = function(a, b) { //assert(typeof(a) === 'number', "HashPair used on something not a number"); return a < b ? a + ' ' + b : b + ' ' + a; }; var deleteObjFromList = function(arr, obj) { for(var i=0; i> 1; for(var i=1; i maxx || (x == maxx && y > maxy)){ maxx = x; maxy = y; end = i; } } return [start, end]; }; var SWAP = function(arr, idx1, idx2) { var tmp = arr[idx1*2]; arr[idx1*2] = arr[idx2*2]; arr[idx2*2] = tmp; tmp = arr[idx1*2+1]; arr[idx1*2+1] = arr[idx2*2+1]; arr[idx2*2+1] = tmp; }; var QHullPartition = function(verts, offs, count, a, b, tol) { if(count === 0) return 0; var max = 0; var pivot = offs; var delta = vsub(b, a); var valueTol = tol * vlength(delta); var head = offs; for(var tail = offs+count-1; head <= tail;){ var v = new Vect(verts[head * 2], verts[head * 2 + 1]); var value = vcross(delta, vsub(v, a)); if(value > valueTol){ if(value > max){ max = value; pivot = head; } head++; } else { SWAP(verts, head, tail); tail--; } } // move the new pivot to the front if it's not already there. if(pivot != offs) SWAP(verts, offs, pivot); return head - offs; }; var QHullReduce = function(tol, verts, offs, count, a, pivot, b, resultPos) { if(count < 0){ return 0; } else if(count == 0) { verts[resultPos*2] = pivot.x; verts[resultPos*2+1] = pivot.y; return 1; } else { var left_count = QHullPartition(verts, offs, count, a, pivot, tol); var left = new Vect(verts[offs*2], verts[offs*2+1]); var index = QHullReduce(tol, verts, offs + 1, left_count - 1, a, left, pivot, resultPos); var pivotPos = resultPos + index++; verts[pivotPos*2] = pivot.x; verts[pivotPos*2+1] = pivot.y; var right_count = QHullPartition(verts, offs + left_count, count - left_count, pivot, b, tol); var right = new Vect(verts[(offs+left_count)*2], verts[(offs+left_count)*2+1]); return index + QHullReduce(tol, verts, offs + left_count + 1, right_count - 1, pivot, right, b, resultPos + index); } }; // QuickHull seemed like a neat algorithm, and efficient-ish for large input sets. // My implementation performs an in place reduction using the result array as scratch space. // // Pass an Array into result to put the result of the calculation there. Otherwise, pass null // and the verts list will be edited in-place. // // Expects the verts to be described in the same way as cpPolyShape - which is to say, it should // be a list of [x1,y1,x2,y2,x3,y3,...]. var convexHull = cp.convexHull = function(verts, result, tol) { if(result){ // Copy the line vertexes into the empty part of the result polyline to use as a scratch buffer. for (var i = 0; i < verts.length; i++){ result[i] = verts[i]; } } else { // If a result array was not specified, reduce the input instead. result = verts; } // Degenerate case, all points are the same. var indexes = loopIndexes(verts); var start = indexes[0], end = indexes[1]; if(start == end){ //if(first) (*first) = 0; result.length = 2; return result; } SWAP(result, 0, start); SWAP(result, 1, end == 0 ? start : end); var a = new Vect(result[0], result[1]); var b = new Vect(result[2], result[3]); var count = verts.length >> 1; //if(first) (*first) = start; var resultCount = QHullReduce(tol, result, 2, count - 2, a, b, a, 1) + 1; result.length = resultCount*2; assertSoft(polyValidate(result), "Internal error: cpConvexHull() and cpPolyValidate() did not agree." + "Please report this error with as much info as you can."); return result; }; /// Clamp @c f to be between @c min and @c max. var clamp = function(f, minv, maxv) { return min(max(f, minv), maxv); }; /// Clamp @c f to be between 0 and 1. var clamp01 = function(f) { return max(0, min(f, 1)); }; /// Linearly interpolate (or extrapolate) between @c f1 and @c f2 by @c t percent. var lerp = function(f1, f2, t) { return f1*(1 - t) + f2*t; }; /// Linearly interpolate from @c f1 to @c f2 by no more than @c d. var lerpconst = function(f1, f2, d) { return f1 + clamp(f2 - f1, -d, d); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ // I'm using an array tuple here because (at time of writing) its about 3x faster // than an object on firefox, and the same speed on chrome. var numVects = 0; var traces = {}; var Vect = cp.Vect = function(x, y) { this.x = x; this.y = y; numVects++; // var s = new Error().stack; // traces[s] = traces[s] ? traces[s]+1 : 1; }; cp.v = function (x,y) { return new Vect(x, y) }; var vzero = cp.vzero = new Vect(0,0); // The functions below *could* be rewritten to be instance methods on Vect. I don't // know how that would effect performance. For now, I'm keeping the JS similar to // the original C code. /// Vector dot product. var vdot = cp.v.dot = function(v1, v2) { return v1.x*v2.x + v1.y*v2.y; }; var vdot2 = function(x1, y1, x2, y2) { return x1*x2 + y1*y2; }; /// Returns the length of v. var vlength = cp.v.len = function(v) { return Math.sqrt(vdot(v, v)); }; var vlength2 = cp.v.len2 = function(x, y) { return Math.sqrt(x*x + y*y); }; /// Check if two vectors are equal. (Be careful when comparing floating point numbers!) var veql = cp.v.eql = function(v1, v2) { return (v1.x === v2.x && v1.y === v2.y); }; /// Add two vectors var vadd = cp.v.add = function(v1, v2) { return new Vect(v1.x + v2.x, v1.y + v2.y); }; Vect.prototype.add = function(v2) { this.x += v2.x; this.y += v2.y; return this; }; /// Subtract two vectors. var vsub = cp.v.sub = function(v1, v2) { return new Vect(v1.x - v2.x, v1.y - v2.y); }; Vect.prototype.sub = function(v2) { this.x -= v2.x; this.y -= v2.y; return this; }; /// Negate a vector. var vneg = cp.v.neg = function(v) { return new Vect(-v.x, -v.y); }; Vect.prototype.neg = function() { this.x = -this.x; this.y = -this.y; return this; }; /// Scalar multiplication. var vmult = cp.v.mult = function(v, s) { return new Vect(v.x*s, v.y*s); }; Vect.prototype.mult = function(s) { this.x *= s; this.y *= s; return this; }; /// 2D vector cross product analog. /// The cross product of 2D vectors results in a 3D vector with only a z component. /// This function returns the magnitude of the z value. var vcross = cp.v.cross = function(v1, v2) { return v1.x*v2.y - v1.y*v2.x; }; var vcross2 = function(x1, y1, x2, y2) { return x1*y2 - y1*x2; }; /// Returns a perpendicular vector. (90 degree rotation) var vperp = cp.v.perp = function(v) { return new Vect(-v.y, v.x); }; /// Returns a perpendicular vector. (-90 degree rotation) var vpvrperp = cp.v.pvrperp = function(v) { return new Vect(v.y, -v.x); }; /// Returns the vector projection of v1 onto v2. var vproject = cp.v.project = function(v1, v2) { return vmult(v2, vdot(v1, v2)/vlengthsq(v2)); }; Vect.prototype.project = function(v2) { this.mult(vdot(this, v2) / vlengthsq(v2)); return this; }; /// Uses complex number multiplication to rotate v1 by v2. Scaling will occur if v1 is not a unit vector. var vrotate = cp.v.rotate = function(v1, v2) { return new Vect(v1.x*v2.x - v1.y*v2.y, v1.x*v2.y + v1.y*v2.x); }; Vect.prototype.rotate = function(v2) { this.x = this.x * v2.x - this.y * v2.y; this.y = this.x * v2.y + this.y * v2.x; return this; }; /// Inverse of vrotate(). var vunrotate = cp.v.unrotate = function(v1, v2) { return new Vect(v1.x*v2.x + v1.y*v2.y, v1.y*v2.x - v1.x*v2.y); }; /// Returns the squared length of v. Faster than vlength() when you only need to compare lengths. var vlengthsq = cp.v.lengthsq = function(v) { return vdot(v, v); }; var vlengthsq2 = cp.v.lengthsq2 = function(x, y) { return x*x + y*y; }; /// Linearly interpolate between v1 and v2. var vlerp = cp.v.lerp = function(v1, v2, t) { return vadd(vmult(v1, 1 - t), vmult(v2, t)); }; /// Returns a normalized copy of v. var vnormalize = cp.v.normalize = function(v) { return vmult(v, 1/vlength(v)); }; /// Returns a normalized copy of v or vzero if v was already vzero. Protects against divide by zero errors. var vnormalize_safe = cp.v.normalize_safe = function(v) { return (v.x === 0 && v.y === 0 ? vzero : vnormalize(v)); }; /// Clamp v to length len. var vclamp = cp.v.clamp = function(v, len) { return (vdot(v,v) > len*len) ? vmult(vnormalize(v), len) : v; }; /// Linearly interpolate between v1 towards v2 by distance d. var vlerpconst = cp.v.lerpconst = function(v1, v2, d) { return vadd(v1, vclamp(vsub(v2, v1), d)); }; /// Returns the distance between v1 and v2. var vdist = cp.v.dist = function(v1, v2) { return vlength(vsub(v1, v2)); }; /// Returns the squared distance between v1 and v2. Faster than vdist() when you only need to compare distances. var vdistsq = cp.v.distsq = function(v1, v2) { return vlengthsq(vsub(v1, v2)); }; /// Returns true if the distance between v1 and v2 is less than dist. var vnear = cp.v.near = function(v1, v2, dist) { return vdistsq(v1, v2) < dist*dist; }; /// Spherical linearly interpolate between v1 and v2. var vslerp = cp.v.slerp = function(v1, v2, t) { var omega = Math.acos(vdot(v1, v2)); if(omega) { var denom = 1/Math.sin(omega); return vadd(vmult(v1, Math.sin((1 - t)*omega)*denom), vmult(v2, Math.sin(t*omega)*denom)); } else { return v1; } }; /// Spherical linearly interpolate between v1 towards v2 by no more than angle a radians var vslerpconst = cp.v.slerpconst = function(v1, v2, a) { var angle = Math.acos(vdot(v1, v2)); return vslerp(v1, v2, min(a, angle)/angle); }; /// Returns the unit length vector for the given angle (in radians). var vforangle = cp.v.forangle = function(a) { return new Vect(Math.cos(a), Math.sin(a)); }; /// Returns the angular direction v is pointing in (in radians). var vtoangle = cp.v.toangle = function(v) { return Math.atan2(v.y, v.x); }; /// Returns a string representation of v. Intended mostly for debugging purposes and not production use. var vstr = cp.v.str = function(v) { return "(" + v.x.toFixed(3) + ", " + v.y.toFixed(3) + ")"; }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ /// Chipmunk's axis-aligned 2D bounding box type along with a few handy routines. var numBB = 0; // Bounding boxes are JS objects with {l, b, r, t} = left, bottom, right, top, respectively. var BB = cp.BB = function(l, b, r, t) { this.l = l; this.b = b; this.r = r; this.t = t; numBB++; }; cp.bb = function(l, b, r, t) { return new BB(l, b, r, t); }; var bbNewForCircle = function(p, r) { return new BB( p.x - r, p.y - r, p.x + r, p.y + r ); }; /// Returns true if @c a and @c b intersect. var bbIntersects = function(a, b) { return (a.l <= b.r && b.l <= a.r && a.b <= b.t && b.b <= a.t); }; var bbIntersects2 = function(bb, l, b, r, t) { return (bb.l <= r && l <= bb.r && bb.b <= t && b <= bb.t); }; /// Returns true if @c other lies completely within @c bb. var bbContainsBB = function(bb, other) { return (bb.l <= other.l && bb.r >= other.r && bb.b <= other.b && bb.t >= other.t); }; /// Returns true if @c bb contains @c v. var bbContainsVect = function(bb, v) { return (bb.l <= v.x && bb.r >= v.x && bb.b <= v.y && bb.t >= v.y); }; var bbContainsVect2 = function(l, b, r, t, v) { return (l <= v.x && r >= v.x && b <= v.y && t >= v.y); }; /// Returns a bounding box that holds both bounding boxes. var bbMerge = function(a, b){ return new BB( min(a.l, b.l), min(a.b, b.b), max(a.r, b.r), max(a.t, b.t) ); }; /// Returns a bounding box that holds both @c bb and @c v. var bbExpand = function(bb, v){ return new BB( min(bb.l, v.x), min(bb.b, v.y), max(bb.r, v.x), max(bb.t, v.y) ); }; /// Returns the area of the bounding box. var bbArea = function(bb) { return (bb.r - bb.l)*(bb.t - bb.b); }; /// Merges @c a and @c b and returns the area of the merged bounding box. var bbMergedArea = function(a, b) { return (max(a.r, b.r) - min(a.l, b.l))*(max(a.t, b.t) - min(a.b, b.b)); }; var bbMergedArea2 = function(bb, l, b, r, t) { return (max(bb.r, r) - min(bb.l, l))*(max(bb.t, t) - min(bb.b, b)); }; /// Return true if the bounding box intersects the line segment with ends @c a and @c b. var bbIntersectsSegment = function(bb, a, b) { return (bbSegmentQuery(bb, a, b) != Infinity); }; /// Clamp a vector to a bounding box. var bbClampVect = function(bb, v) { var x = min(max(bb.l, v.x), bb.r); var y = min(max(bb.b, v.y), bb.t); return new Vect(x, y); }; // TODO edge case issue /// Wrap a vector to a bounding box. var bbWrapVect = function(bb, v) { var ix = Math.abs(bb.r - bb.l); var modx = (v.x - bb.l) % ix; var x = (modx > 0) ? modx : modx + ix; var iy = Math.abs(bb.t - bb.b); var mody = (v.y - bb.b) % iy; var y = (mody > 0) ? mody : mody + iy; return new Vect(x + bb.l, y + bb.b); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ /// Segment query info struct. /* These are created using literals where needed. typedef struct cpSegmentQueryInfo { /// The shape that was hit, null if no collision occured. cpShape *shape; /// The normalized distance along the query segment in the range [0, 1]. cpFloat t; /// The normal of the surface hit. cpVect n; } cpSegmentQueryInfo; */ var shapeIDCounter = 0; var CP_NO_GROUP = cp.NO_GROUP = 0; var CP_ALL_LAYERS = cp.ALL_LAYERS = ~0; cp.resetShapeIdCounter = function() { shapeIDCounter = 0; }; /// The cpShape struct defines the shape of a rigid body. // /// Opaque collision shape struct. Do not create directly - instead use /// PolyShape, CircleShape and SegmentShape. var Shape = cp.Shape = function(body) { /// The rigid body this collision shape is attached to. this.body = body; /// The current bounding box of the shape. this.bb_l = this.bb_b = this.bb_r = this.bb_t = 0; this.hashid = shapeIDCounter++; /// Sensor flag. /// Sensor shapes call collision callbacks but don't produce collisions. this.sensor = false; /// Coefficient of restitution. (elasticity) this.e = 0; /// Coefficient of friction. this.u = 0; /// Surface velocity used when solving for friction. this.surface_v = vzero; /// Collision type of this shape used when picking collision handlers. this.collision_type = 0; /// Group of this shape. Shapes in the same group don't collide. this.group = 0; // Layer bitmask for this shape. Shapes only collide if the bitwise and of their layers is non-zero. this.layers = CP_ALL_LAYERS; this.space = null; // Copy the collision code from the prototype into the actual object. This makes collision // function lookups slightly faster. this.collisionCode = this.collisionCode; }; Shape.prototype.setElasticity = function(e) { this.e = e; }; Shape.prototype.setFriction = function(u) { this.body.activate(); this.u = u; }; Shape.prototype.setLayers = function(layers) { this.body.activate(); this.layers = layers; }; Shape.prototype.setSensor = function(sensor) { this.body.activate(); this.sensor = sensor; }; Shape.prototype.setCollisionType = function(collision_type) { this.body.activate(); this.collision_type = collision_type; }; Shape.prototype.getBody = function() { return this.body; }; Shape.prototype.active = function() { // return shape->prev || (shape->body && shape->body->shapeList == shape); return this.body && this.body.shapeList.indexOf(this) !== -1; }; Shape.prototype.setBody = function(body) { assert(!this.active(), "You cannot change the body on an active shape. You must remove the shape from the space before changing the body."); this.body = body; }; Shape.prototype.cacheBB = function() { return this.update(this.body.p, this.body.rot); }; Shape.prototype.update = function(pos, rot) { assert(!isNaN(rot.x), 'Rotation is NaN'); assert(!isNaN(pos.x), 'Position is NaN'); this.cacheData(pos, rot); }; Shape.prototype.pointQuery = function(p) { var info = this.nearestPointQuery(p); if (info.d < 0) return info; }; Shape.prototype.getBB = function() { return new BB(this.bb_l, this.bb_b, this.bb_r, this.bb_t); }; /* Not implemented - all these getters and setters. Just edit the object directly. CP_DefineShapeStructGetter(cpBody*, body, Body); void cpShapeSetBody(cpShape *shape, cpBody *body); CP_DefineShapeStructGetter(cpBB, bb, BB); CP_DefineShapeStructProperty(cpBool, sensor, Sensor, cpTrue); CP_DefineShapeStructProperty(cpFloat, e, Elasticity, cpFalse); CP_DefineShapeStructProperty(cpFloat, u, Friction, cpTrue); CP_DefineShapeStructProperty(cpVect, surface_v, SurfaceVelocity, cpTrue); CP_DefineShapeStructProperty(cpDataPointer, data, UserData, cpFalse); CP_DefineShapeStructProperty(cpCollisionType, collision_type, CollisionType, cpTrue); CP_DefineShapeStructProperty(cpGroup, group, Group, cpTrue); CP_DefineShapeStructProperty(cpLayers, layers, Layers, cpTrue); */ /// Extended point query info struct. Returned from calling pointQuery on a shape. var PointQueryExtendedInfo = function(shape) { /// Shape that was hit, NULL if no collision occurred. this.shape = shape; /// Depth of the point inside the shape. this.d = Infinity; /// Direction of minimum norm to the shape's surface. this.n = vzero; }; var NearestPointQueryInfo = function(shape, p, d) { /// The nearest shape, NULL if no shape was within range. this.shape = shape; /// The closest point on the shape's surface. (in world space coordinates) this.p = p; /// The distance to the point. The distance is negative if the point is inside the shape. this.d = d; }; var SegmentQueryInfo = function(shape, t, n) { /// The shape that was hit, NULL if no collision occured. this.shape = shape; /// The normalized distance along the query segment in the range [0, 1]. this.t = t; /// The normal of the surface hit. this.n = n; }; /// Get the hit point for a segment query. SegmentQueryInfo.prototype.hitPoint = function(start, end) { return vlerp(start, end, this.t); }; /// Get the hit distance for a segment query. SegmentQueryInfo.prototype.hitDist = function(start, end) { return vdist(start, end) * this.t; }; // Circles. var CircleShape = cp.CircleShape = function(body, radius, offset) { this.c = this.tc = offset; this.r = radius; this.type = 'circle'; Shape.call(this, body); }; CircleShape.prototype = Object.create(Shape.prototype); CircleShape.prototype.cacheData = function(p, rot) { //var c = this.tc = vadd(p, vrotate(this.c, rot)); var c = this.tc = vrotate(this.c, rot).add(p); //this.bb = bbNewForCircle(c, this.r); var r = this.r; this.bb_l = c.x - r; this.bb_b = c.y - r; this.bb_r = c.x + r; this.bb_t = c.y + r; }; /// Test if a point lies within a shape. /*CircleShape.prototype.pointQuery = function(p) { var delta = vsub(p, this.tc); var distsq = vlengthsq(delta); var r = this.r; if(distsq < r*r){ var info = new PointQueryExtendedInfo(this); var dist = Math.sqrt(distsq); info.d = r - dist; info.n = vmult(delta, 1/dist); return info; } };*/ CircleShape.prototype.nearestPointQuery = function(p) { var deltax = p.x - this.tc.x; var deltay = p.y - this.tc.y; var d = vlength2(deltax, deltay); var r = this.r; var nearestp = new Vect(this.tc.x + deltax * r/d, this.tc.y + deltay * r/d); return new NearestPointQueryInfo(this, nearestp, d - r); }; var circleSegmentQuery = function(shape, center, r, a, b, info) { // offset the line to be relative to the circle a = vsub(a, center); b = vsub(b, center); var qa = vdot(a, a) - 2*vdot(a, b) + vdot(b, b); var qb = -2*vdot(a, a) + 2*vdot(a, b); var qc = vdot(a, a) - r*r; var det = qb*qb - 4*qa*qc; if(det >= 0) { var t = (-qb - Math.sqrt(det))/(2*qa); if(0 <= t && t <= 1){ return new SegmentQueryInfo(shape, t, vnormalize(vlerp(a, b, t))); } } }; CircleShape.prototype.segmentQuery = function(a, b) { return circleSegmentQuery(this, this.tc, this.r, a, b); }; // The C API has these, and also getters. Its not idiomatic to // write getters and setters in JS. /* CircleShape.prototype.setRadius = function(radius) { this.r = radius; } CircleShape.prototype.setOffset = function(offset) { this.c = offset; }*/ // Segment shape var SegmentShape = cp.SegmentShape = function(body, a, b, r) { this.a = a; this.b = b; this.n = vperp(vnormalize(vsub(b, a))); this.ta = this.tb = this.tn = null; this.r = r; this.a_tangent = vzero; this.b_tangent = vzero; this.type = 'segment'; Shape.call(this, body); }; SegmentShape.prototype = Object.create(Shape.prototype); SegmentShape.prototype.cacheData = function(p, rot) { this.ta = vadd(p, vrotate(this.a, rot)); this.tb = vadd(p, vrotate(this.b, rot)); this.tn = vrotate(this.n, rot); var l,r,b,t; if(this.ta.x < this.tb.x){ l = this.ta.x; r = this.tb.x; } else { l = this.tb.x; r = this.ta.x; } if(this.ta.y < this.tb.y){ b = this.ta.y; t = this.tb.y; } else { b = this.tb.y; t = this.ta.y; } var rad = this.r; this.bb_l = l - rad; this.bb_b = b - rad; this.bb_r = r + rad; this.bb_t = t + rad; }; SegmentShape.prototype.nearestPointQuery = function(p) { var closest = closestPointOnSegment(p, this.ta, this.tb); var deltax = p.x - closest.x; var deltay = p.y - closest.y; var d = vlength2(deltax, deltay); var r = this.r; var nearestp = (d ? vadd(closest, vmult(new Vect(deltax, deltay), r/d)) : closest); return new NearestPointQueryInfo(this, nearestp, d - r); }; SegmentShape.prototype.segmentQuery = function(a, b) { var n = this.tn; var d = vdot(vsub(this.ta, a), n); var r = this.r; var flipped_n = (d > 0 ? vneg(n) : n); var n_offset = vsub(vmult(flipped_n, r), a); var seg_a = vadd(this.ta, n_offset); var seg_b = vadd(this.tb, n_offset); var delta = vsub(b, a); if(vcross(delta, seg_a)*vcross(delta, seg_b) <= 0){ var d_offset = d + (d > 0 ? -r : r); var ad = -d_offset; var bd = vdot(delta, n) - d_offset; if(ad*bd < 0){ return new SegmentQueryInfo(this, ad/(ad - bd), flipped_n); } } else if(r !== 0){ var info1 = circleSegmentQuery(this, this.ta, this.r, a, b); var info2 = circleSegmentQuery(this, this.tb, this.r, a, b); if (info1){ return info2 && info2.t < info1.t ? info2 : info1; } else { return info2; } } }; SegmentShape.prototype.setNeighbors = function(prev, next) { this.a_tangent = vsub(prev, this.a); this.b_tangent = vsub(next, this.b); }; SegmentShape.prototype.setEndpoints = function(a, b) { this.a = a; this.b = b; this.n = vperp(vnormalize(vsub(b, a))); }; /* cpSegmentShapeSetRadius(cpShape *shape, cpFloat radius) { this.r = radius; }*/ /* CP_DeclareShapeGetter(cpSegmentShape, cpVect, A); CP_DeclareShapeGetter(cpSegmentShape, cpVect, B); CP_DeclareShapeGetter(cpSegmentShape, cpVect, Normal); CP_DeclareShapeGetter(cpSegmentShape, cpFloat, Radius); */ /* Copyright (c) 2007 Scott Lembcke * * 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. */ /// Check that a set of vertexes is convex and has a clockwise winding. var polyValidate = function(verts) { var len = verts.length; for(var i=0; i 0){ if(vcross2(bx - ax, by - ay, cx - bx, cy - by) > 0){ return false; } } return true; }; /// Initialize a polygon shape. /// The vertexes must be convex and have a clockwise winding. var PolyShape = cp.PolyShape = function(body, verts, offset) { this.setVerts(verts, offset); this.type = 'poly'; Shape.call(this, body); }; PolyShape.prototype = Object.create(Shape.prototype); var SplittingPlane = function(n, d) { this.n = n; this.d = d; }; SplittingPlane.prototype.compare = function(v) { return vdot(this.n, v) - this.d; }; PolyShape.prototype.setVerts = function(verts, offset) { assert(verts.length >= 4, "Polygons require some verts"); assert(typeof(verts[0]) === 'number', 'Polygon verticies should be specified in a flattened list (eg [x1,y1,x2,y2,x3,y3,...])'); // Fail if the user attempts to pass a concave poly, or a bad winding. assert(polyValidate(verts), "Polygon is concave or has a reversed winding. Consider using cpConvexHull()"); var len = verts.length; var numVerts = len >> 1; // This a pretty bad way to do this in javascript. As a first pass, I want to keep // the code similar to the C. this.verts = new Array(len); this.tVerts = new Array(len); this.planes = new Array(numVerts); this.tPlanes = new Array(numVerts); for(var i=0; i>1] = new SplittingPlane(n, vdot2(n.x, n.y, ax, ay)); this.tPlanes[i>>1] = new SplittingPlane(new Vect(0,0), 0); } }; /// Initialize a box shaped polygon shape. var BoxShape = cp.BoxShape = function(body, width, height) { var hw = width/2; var hh = height/2; return BoxShape2(body, new BB(-hw, -hh, hw, hh)); }; /// Initialize an offset box shaped polygon shape. var BoxShape2 = cp.BoxShape2 = function(body, box) { var verts = [ box.l, box.b, box.l, box.t, box.r, box.t, box.r, box.b, ]; return new PolyShape(body, verts, vzero); }; PolyShape.prototype.transformVerts = function(p, rot) { var src = this.verts; var dst = this.tVerts; var l = Infinity, r = -Infinity; var b = Infinity, t = -Infinity; for(var i=0; i (' + vx + ',' + vy + ')'); dst[i] = vx; dst[i+1] = vy; l = min(l, vx); r = max(r, vx); b = min(b, vy); t = max(t, vy); } this.bb_l = l; this.bb_b = b; this.bb_r = r; this.bb_t = t; }; PolyShape.prototype.transformAxes = function(p, rot) { var src = this.planes; var dst = this.tPlanes; for(var i=0; i 0) outside = true; var v1x = verts[i*2]; var v1y = verts[i*2 + 1]; var closest = closestPointOnSegment2(p.x, p.y, v0x, v0y, v1x, v1y); var dist = vdist(p, closest); if(dist < minDist){ minDist = dist; closestPoint = closest; } v0x = v1x; v0y = v1y; } return new NearestPointQueryInfo(this, closestPoint, (outside ? minDist : -minDist)); }; PolyShape.prototype.segmentQuery = function(a, b) { var axes = this.tPlanes; var verts = this.tVerts; var numVerts = axes.length; var len = numVerts * 2; for(var i=0; i an) continue; var bn = vdot(b, n); var t = (axes[i].d - an)/(bn - an); if(t < 0 || 1 < t) continue; var point = vlerp(a, b, t); var dt = -vcross(n, point); var dtMin = -vcross2(n.x, n.y, verts[i*2], verts[i*2+1]); var dtMax = -vcross2(n.x, n.y, verts[(i*2+2)%len], verts[(i*2+3)%len]); if(dtMin <= dt && dt <= dtMax){ // josephg: In the original C code, this function keeps // looping through axes after finding a match. I *think* // this code is equivalent... return new SegmentQueryInfo(this, t, n); } } }; PolyShape.prototype.valueOnAxis = function(n, d) { var verts = this.tVerts; var m = vdot2(n.x, n.y, verts[0], verts[1]); for(var i=2; i 0) return false; } return true; }; PolyShape.prototype.containsVertPartial = function(vx, vy, n) { var planes = this.tPlanes; for(var i=0; i 0) return false; } return true; }; // These methods are provided for API compatibility with Chipmunk. I recommend against using // them - just access the poly.verts list directly. PolyShape.prototype.getNumVerts = function() { return this.verts.length / 2; }; PolyShape.prototype.getVert = function(i) { return new Vect(this.verts[i * 2], this.verts[i * 2 + 1]); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ /// @defgroup cpBody cpBody /// Chipmunk's rigid body type. Rigid bodies hold the physical properties of an object like /// it's mass, and position and velocity of it's center of gravity. They don't have an shape on their own. /// They are given a shape by creating collision shapes (cpShape) that point to the body. /// @{ var Body = cp.Body = function(m, i) { /// Mass of the body. /// Must agree with cpBody.m_inv! Use body.setMass() when changing the mass for this reason. //this.m; /// Mass inverse. //this.m_inv; /// Moment of inertia of the body. /// Must agree with cpBody.i_inv! Use body.setMoment() when changing the moment for this reason. //this.i; /// Moment of inertia inverse. //this.i_inv; /// Position of the rigid body's center of gravity. this.p = new Vect(0,0); /// Velocity of the rigid body's center of gravity. this.vx = this.vy = 0; /// Force acting on the rigid body's center of gravity. this.f = new Vect(0,0); /// Rotation of the body around it's center of gravity in radians. /// Must agree with cpBody.rot! Use cpBodySetAngle() when changing the angle for this reason. //this.a; /// Angular velocity of the body around it's center of gravity in radians/second. this.w = 0; /// Torque applied to the body around it's center of gravity. this.t = 0; /// Cached unit length vector representing the angle of the body. /// Used for fast rotations using cpvrotate(). //cpVect rot; /// Maximum velocity allowed when updating the velocity. this.v_limit = Infinity; /// Maximum rotational rate (in radians/second) allowed when updating the angular velocity. this.w_limit = Infinity; // This stuff is all private. this.v_biasx = this.v_biasy = 0; this.w_bias = 0; this.space = null; this.shapeList = []; this.arbiterList = null; // These are both wacky linked lists. this.constraintList = null; // This stuff is used to track information on the collision graph. this.nodeRoot = null; this.nodeNext = null; this.nodeIdleTime = 0; // Set this.m and this.m_inv this.setMass(m); // Set this.i and this.i_inv this.setMoment(i); // Set this.a and this.rot this.rot = new Vect(0,0); this.setAngle(0); }; // I wonder if this should use the constructor style like Body... var createStaticBody = function() { body = new Body(Infinity, Infinity); body.nodeIdleTime = Infinity; return body; }; if (typeof DEBUG !== 'undefined' && DEBUG) { var v_assert_nan = function(v, message){assert(v.x == v.x && v.y == v.y, message); }; var v_assert_infinite = function(v, message){assert(Math.abs(v.x) !== Infinity && Math.abs(v.y) !== Infinity, message);}; var v_assert_sane = function(v, message){v_assert_nan(v, message); v_assert_infinite(v, message);}; Body.prototype.sanityCheck = function() { assert(this.m === this.m && this.m_inv === this.m_inv, "Body's mass is invalid."); assert(this.i === this.i && this.i_inv === this.i_inv, "Body's moment is invalid."); v_assert_sane(this.p, "Body's position is invalid."); v_assert_sane(this.f, "Body's force is invalid."); assert(this.vx === this.vx && Math.abs(this.vx) !== Infinity, "Body's velocity is invalid."); assert(this.vy === this.vy && Math.abs(this.vy) !== Infinity, "Body's velocity is invalid."); assert(this.a === this.a && Math.abs(this.a) !== Infinity, "Body's angle is invalid."); assert(this.w === this.w && Math.abs(this.w) !== Infinity, "Body's angular velocity is invalid."); assert(this.t === this.t && Math.abs(this.t) !== Infinity, "Body's torque is invalid."); v_assert_sane(this.rot, "Body's rotation vector is invalid."); assert(this.v_limit === this.v_limit, "Body's velocity limit is invalid."); assert(this.w_limit === this.w_limit, "Body's angular velocity limit is invalid."); }; } else { Body.prototype.sanityCheck = function(){}; } Body.prototype.getPos = function() { return this.p; }; Body.prototype.getVel = function() { return new Vect(this.vx, this.vy); }; Body.prototype.getAngVel = function() { return this.w; }; /// Returns true if the body is sleeping. Body.prototype.isSleeping = function() { return this.nodeRoot !== null; }; /// Returns true if the body is static. Body.prototype.isStatic = function() { return this.nodeIdleTime === Infinity; }; /// Returns true if the body has not been added to a space. Body.prototype.isRogue = function() { return this.space === null; }; // It would be nicer to use defineProperty for this, but its about 30x slower: // http://jsperf.com/defineproperty-vs-setter Body.prototype.setMass = function(mass) { assert(mass > 0, "Mass must be positive and non-zero."); //activate is defined in cpSpaceComponent this.activate(); this.m = mass; this.m_inv = 1/mass; }; Body.prototype.setMoment = function(moment) { assert(moment > 0, "Moment of Inertia must be positive and non-zero."); this.activate(); this.i = moment; this.i_inv = 1/moment; }; Body.prototype.addShape = function(shape) { this.shapeList.push(shape); }; Body.prototype.removeShape = function(shape) { // This implementation has a linear time complexity with the number of shapes. // The original implementation used linked lists instead, which might be faster if // you're constantly editing the shape of a body. I expect most bodies will never // have their shape edited, so I'm just going to use the simplest possible implemention. deleteObjFromList(this.shapeList, shape); }; var filterConstraints = function(node, body, filter) { if(node === filter){ return node.next(body); } else if(node.a === body){ node.next_a = filterConstraints(node.next_a, body, filter); } else { node.next_b = filterConstraints(node.next_b, body, filter); } return node; }; Body.prototype.removeConstraint = function(constraint) { // The constraint must be in the constraints list when this is called. this.constraintList = filterConstraints(this.constraintList, this, constraint); }; Body.prototype.setPos = function(pos) { this.activate(); this.sanityCheck(); // If I allow the position to be set to vzero, vzero will get changed. if (pos === vzero) { pos = cp.v(0,0); } this.p = pos; }; Body.prototype.setVel = function(velocity) { this.activate(); this.vx = velocity.x; this.vy = velocity.y; }; Body.prototype.setAngVel = function(w) { this.activate(); this.w = w; }; Body.prototype.setAngleInternal = function(angle) { assert(!isNaN(angle), "Internal Error: Attempting to set body's angle to NaN"); this.a = angle;//fmod(a, (cpFloat)M_PI*2.0f); //this.rot = vforangle(angle); this.rot.x = Math.cos(angle); this.rot.y = Math.sin(angle); }; Body.prototype.setAngle = function(angle) { this.activate(); this.sanityCheck(); this.setAngleInternal(angle); }; Body.prototype.velocity_func = function(gravity, damping, dt) { //this.v = vclamp(vadd(vmult(this.v, damping), vmult(vadd(gravity, vmult(this.f, this.m_inv)), dt)), this.v_limit); var vx = this.vx * damping + (gravity.x + this.f.x * this.m_inv) * dt; var vy = this.vy * damping + (gravity.y + this.f.y * this.m_inv) * dt; //var v = vclamp(new Vect(vx, vy), this.v_limit); //this.vx = v.x; this.vy = v.y; var v_limit = this.v_limit; var lensq = vx * vx + vy * vy; var scale = (lensq > v_limit*v_limit) ? v_limit / Math.sqrt(lensq) : 1; this.vx = vx * scale; this.vy = vy * scale; var w_limit = this.w_limit; this.w = clamp(this.w*damping + this.t*this.i_inv*dt, -w_limit, w_limit); this.sanityCheck(); }; Body.prototype.position_func = function(dt) { //this.p = vadd(this.p, vmult(vadd(this.v, this.v_bias), dt)); //this.p = this.p + (this.v + this.v_bias) * dt; this.p.x += (this.vx + this.v_biasx) * dt; this.p.y += (this.vy + this.v_biasy) * dt; this.setAngleInternal(this.a + (this.w + this.w_bias)*dt); this.v_biasx = this.v_biasy = 0; this.w_bias = 0; this.sanityCheck(); }; Body.prototype.resetForces = function() { this.activate(); this.f = new Vect(0,0); this.t = 0; }; Body.prototype.applyForce = function(force, r) { this.activate(); this.f = vadd(this.f, force); this.t += vcross(r, force); }; Body.prototype.applyImpulse = function(j, r) { this.activate(); apply_impulse(this, j.x, j.y, r); }; Body.prototype.getVelAtPoint = function(r) { return vadd(new Vect(this.vx, this.vy), vmult(vperp(r), this.w)); }; /// Get the velocity on a body (in world units) at a point on the body in world coordinates. Body.prototype.getVelAtWorldPoint = function(point) { return this.getVelAtPoint(vsub(point, this.p)); }; /// Get the velocity on a body (in world units) at a point on the body in local coordinates. Body.prototype.getVelAtLocalPoint = function(point) { return this.getVelAtPoint(vrotate(point, this.rot)); }; Body.prototype.eachShape = function(func) { for(var i = 0, len = this.shapeList.length; i < len; i++) { func(this.shapeList[i]); } }; Body.prototype.eachConstraint = function(func) { var constraint = this.constraintList; while(constraint) { var next = constraint.next(this); func(constraint); constraint = next; } }; Body.prototype.eachArbiter = function(func) { var arb = this.arbiterList; while(arb){ var next = arb.next(this); arb.swappedColl = (this === arb.body_b); func(arb); arb = next; } }; /// Convert body relative/local coordinates to absolute/world coordinates. Body.prototype.local2World = function(v) { return vadd(this.p, vrotate(v, this.rot)); }; /// Convert body absolute/world coordinates to relative/local coordinates. Body.prototype.world2Local = function(v) { return vunrotate(vsub(v, this.p), this.rot); }; /// Get the kinetic energy of a body. Body.prototype.kineticEnergy = function() { // Need to do some fudging to avoid NaNs var vsq = this.vx*this.vx + this.vy*this.vy; var wsq = this.w * this.w; return (vsq ? vsq*this.m : 0) + (wsq ? wsq*this.i : 0); }; /* Copyright (c) 2010 Scott Lembcke * * 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. */ /** @defgroup cpSpatialIndex cpSpatialIndex Spatial indexes are data structures that are used to accelerate collision detection and spatial queries. Chipmunk provides a number of spatial index algorithms to pick from and they are programmed in a generic way so that you can use them for holding more than just Shapes. It works by using pointers to the objects you add and using a callback to ask your code for bounding boxes when it needs them. Several types of queries can be performed an index as well as reindexing and full collision information. All communication to the spatial indexes is performed through callback functions. Spatial indexes should be treated as opaque structs. This means you shouldn't be reading any of the fields directly. All spatial indexes define the following methods: // The number of objects in the spatial index. count = 0; // Iterate the objects in the spatial index. @c func will be called once for each object. each(func); // Returns true if the spatial index contains the given object. // Most spatial indexes use hashed storage, so you must provide a hash value too. contains(obj, hashid); // Add an object to a spatial index. insert(obj, hashid); // Remove an object from a spatial index. remove(obj, hashid); // Perform a full reindex of a spatial index. reindex(); // Reindex a single object in the spatial index. reindexObject(obj, hashid); // Perform a point query against the spatial index, calling @c func for each potential match. // A pointer to the point will be passed as @c obj1 of @c func. // func(shape); pointQuery(point, func); // Perform a segment query against the spatial index, calling @c func for each potential match. // func(shape); segmentQuery(vect a, vect b, t_exit, func); // Perform a rectangle query against the spatial index, calling @c func for each potential match. // func(shape); query(bb, func); // Simultaneously reindex and find all colliding objects. // @c func will be called once for each potentially overlapping pair of objects found. // If the spatial index was initialized with a static index, it will collide it's objects against that as well. reindexQuery(func); */ var SpatialIndex = cp.SpatialIndex = function(staticIndex) { this.staticIndex = staticIndex; if(staticIndex){ if(staticIndex.dynamicIndex){ throw new Error("This static index is already associated with a dynamic index."); } staticIndex.dynamicIndex = this; } }; // Collide the objects in an index against the objects in a staticIndex using the query callback function. SpatialIndex.prototype.collideStatic = function(staticIndex, func) { if(staticIndex.count > 0){ var query = staticIndex.query; this.each(function(obj) { query(obj, new BB(obj.bb_l, obj.bb_b, obj.bb_r, obj.bb_t), func); }); } }; /* Copyright (c) 2009 Scott Lembcke * * 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. */ // This file implements a modified AABB tree for collision detection. var BBTree = cp.BBTree = function(staticIndex) { SpatialIndex.call(this, staticIndex); this.velocityFunc = null; // This is a hash from object ID -> object for the objects stored in the BBTree. this.leaves = {}; // A count of the number of leaves in the BBTree. this.count = 0; this.root = null; // A linked list containing an object pool of tree nodes and pairs. this.pooledNodes = null; this.pooledPairs = null; this.stamp = 0; }; BBTree.prototype = Object.create(SpatialIndex.prototype); var numNodes = 0; var Node = function(tree, a, b) { this.obj = null; this.bb_l = min(a.bb_l, b.bb_l); this.bb_b = min(a.bb_b, b.bb_b); this.bb_r = max(a.bb_r, b.bb_r); this.bb_t = max(a.bb_t, b.bb_t); this.parent = null; this.setA(a); this.setB(b); }; BBTree.prototype.makeNode = function(a, b) { var node = this.pooledNodes; if(node){ this.pooledNodes = node.parent; node.constructor(this, a, b); return node; } else { numNodes++; return new Node(this, a, b); } }; var numLeaves = 0; var Leaf = function(tree, obj) { this.obj = obj; tree.getBB(obj, this); this.parent = null; this.stamp = 1; this.pairs = null; numLeaves++; }; // **** Misc Functions BBTree.prototype.getBB = function(obj, dest) { var velocityFunc = this.velocityFunc; if(velocityFunc){ var coef = 0.1; var x = (obj.bb_r - obj.bb_l)*coef; var y = (obj.bb_t - obj.bb_b)*coef; var v = vmult(velocityFunc(obj), 0.1); dest.bb_l = obj.bb_l + min(-x, v.x); dest.bb_b = obj.bb_b + min(-y, v.y); dest.bb_r = obj.bb_r + max( x, v.x); dest.bb_t = obj.bb_t + max( y, v.y); } else { dest.bb_l = obj.bb_l; dest.bb_b = obj.bb_b; dest.bb_r = obj.bb_r; dest.bb_t = obj.bb_t; } }; BBTree.prototype.getStamp = function() { var dynamic = this.dynamicIndex; return (dynamic && dynamic.stamp ? dynamic.stamp : this.stamp); }; BBTree.prototype.incrementStamp = function() { if(this.dynamicIndex && this.dynamicIndex.stamp){ this.dynamicIndex.stamp++; } else { this.stamp++; } } // **** Pair/Thread Functions var numPairs = 0; // Objects created with constructors are faster than object literals. :( var Pair = function(leafA, nextA, leafB, nextB) { this.prevA = null; this.leafA = leafA; this.nextA = nextA; this.prevB = null; this.leafB = leafB; this.nextB = nextB; }; BBTree.prototype.makePair = function(leafA, nextA, leafB, nextB) { //return new Pair(leafA, nextA, leafB, nextB); var pair = this.pooledPairs; if (pair) { this.pooledPairs = pair.prevA; pair.prevA = null; pair.leafA = leafA; pair.nextA = nextA; pair.prevB = null; pair.leafB = leafB; pair.nextB = nextB; //pair.constructor(leafA, nextA, leafB, nextB); return pair; } else { numPairs++; return new Pair(leafA, nextA, leafB, nextB); } }; Pair.prototype.recycle = function(tree) { this.prevA = tree.pooledPairs; tree.pooledPairs = this; }; var unlinkThread = function(prev, leaf, next) { if(next){ if(next.leafA === leaf) next.prevA = prev; else next.prevB = prev; } if(prev){ if(prev.leafA === leaf) prev.nextA = next; else prev.nextB = next; } else { leaf.pairs = next; } }; Leaf.prototype.clearPairs = function(tree) { var pair = this.pairs, next; this.pairs = null; while(pair){ if(pair.leafA === this){ next = pair.nextA; unlinkThread(pair.prevB, pair.leafB, pair.nextB); } else { next = pair.nextB; unlinkThread(pair.prevA, pair.leafA, pair.nextA); } pair.recycle(tree); pair = next; } }; var pairInsert = function(a, b, tree) { var nextA = a.pairs, nextB = b.pairs; var pair = tree.makePair(a, nextA, b, nextB); a.pairs = b.pairs = pair; if(nextA){ if(nextA.leafA === a) nextA.prevA = pair; else nextA.prevB = pair; } if(nextB){ if(nextB.leafA === b) nextB.prevA = pair; else nextB.prevB = pair; } }; // **** Node Functions Node.prototype.recycle = function(tree) { this.parent = tree.pooledNodes; tree.pooledNodes = this; }; Leaf.prototype.recycle = function(tree) { // Its not worth the overhead to recycle leaves. }; Node.prototype.setA = function(value) { this.A = value; value.parent = this; }; Node.prototype.setB = function(value) { this.B = value; value.parent = this; }; Leaf.prototype.isLeaf = true; Node.prototype.isLeaf = false; Node.prototype.otherChild = function(child) { return (this.A == child ? this.B : this.A); }; Node.prototype.replaceChild = function(child, value, tree) { assertSoft(child == this.A || child == this.B, "Node is not a child of parent."); if(this.A == child){ this.A.recycle(tree); this.setA(value); } else { this.B.recycle(tree); this.setB(value); } for(var node=this; node; node = node.parent){ //node.bb = bbMerge(node.A.bb, node.B.bb); var a = node.A; var b = node.B; node.bb_l = min(a.bb_l, b.bb_l); node.bb_b = min(a.bb_b, b.bb_b); node.bb_r = max(a.bb_r, b.bb_r); node.bb_t = max(a.bb_t, b.bb_t); } }; Node.prototype.bbArea = Leaf.prototype.bbArea = function() { return (this.bb_r - this.bb_l)*(this.bb_t - this.bb_b); }; var bbTreeMergedArea = function(a, b) { return (max(a.bb_r, b.bb_r) - min(a.bb_l, b.bb_l))*(max(a.bb_t, b.bb_t) - min(a.bb_b, b.bb_b)); }; // **** Subtree Functions // Would it be better to make these functions instance methods on Node and Leaf? var bbProximity = function(a, b) { return Math.abs(a.bb_l + a.bb_r - b.bb_l - b.bb_r) + Math.abs(a.bb_b + a.bb_t - b.bb_b - b.bb_t); }; var subtreeInsert = function(subtree, leaf, tree) { // var s = new Error().stack; // traces[s] = traces[s] ? traces[s]+1 : 1; if(subtree == null){ return leaf; } else if(subtree.isLeaf){ return tree.makeNode(leaf, subtree); } else { var cost_a = subtree.B.bbArea() + bbTreeMergedArea(subtree.A, leaf); var cost_b = subtree.A.bbArea() + bbTreeMergedArea(subtree.B, leaf); if(cost_a === cost_b){ cost_a = bbProximity(subtree.A, leaf); cost_b = bbProximity(subtree.B, leaf); } if(cost_b < cost_a){ subtree.setB(subtreeInsert(subtree.B, leaf, tree)); } else { subtree.setA(subtreeInsert(subtree.A, leaf, tree)); } // subtree.bb = bbMerge(subtree.bb, leaf.bb); subtree.bb_l = min(subtree.bb_l, leaf.bb_l); subtree.bb_b = min(subtree.bb_b, leaf.bb_b); subtree.bb_r = max(subtree.bb_r, leaf.bb_r); subtree.bb_t = max(subtree.bb_t, leaf.bb_t); return subtree; } }; Node.prototype.intersectsBB = Leaf.prototype.intersectsBB = function(bb) { return (this.bb_l <= bb.r && bb.l <= this.bb_r && this.bb_b <= bb.t && bb.b <= this.bb_t); }; var subtreeQuery = function(subtree, bb, func) { //if(bbIntersectsBB(subtree.bb, bb)){ if(subtree.intersectsBB(bb)){ if(subtree.isLeaf){ func(subtree.obj); } else { subtreeQuery(subtree.A, bb, func); subtreeQuery(subtree.B, bb, func); } } }; /// Returns the fraction along the segment query the node hits. Returns Infinity if it doesn't hit. var nodeSegmentQuery = function(node, a, b) { var idx = 1/(b.x - a.x); var tx1 = (node.bb_l == a.x ? -Infinity : (node.bb_l - a.x)*idx); var tx2 = (node.bb_r == a.x ? Infinity : (node.bb_r - a.x)*idx); var txmin = min(tx1, tx2); var txmax = max(tx1, tx2); var idy = 1/(b.y - a.y); var ty1 = (node.bb_b == a.y ? -Infinity : (node.bb_b - a.y)*idy); var ty2 = (node.bb_t == a.y ? Infinity : (node.bb_t - a.y)*idy); var tymin = min(ty1, ty2); var tymax = max(ty1, ty2); if(tymin <= txmax && txmin <= tymax){ var min_ = max(txmin, tymin); var max_ = min(txmax, tymax); if(0.0 <= max_ && min_ <= 1.0) return max(min_, 0.0); } return Infinity; }; var subtreeSegmentQuery = function(subtree, a, b, t_exit, func) { if(subtree.isLeaf){ return func(subtree.obj); } else { var t_a = nodeSegmentQuery(subtree.A, a, b); var t_b = nodeSegmentQuery(subtree.B, a, b); if(t_a < t_b){ if(t_a < t_exit) t_exit = min(t_exit, subtreeSegmentQuery(subtree.A, a, b, t_exit, func)); if(t_b < t_exit) t_exit = min(t_exit, subtreeSegmentQuery(subtree.B, a, b, t_exit, func)); } else { if(t_b < t_exit) t_exit = min(t_exit, subtreeSegmentQuery(subtree.B, a, b, t_exit, func)); if(t_a < t_exit) t_exit = min(t_exit, subtreeSegmentQuery(subtree.A, a, b, t_exit, func)); } return t_exit; } }; BBTree.prototype.subtreeRecycle = function(node) { if(node.isLeaf){ this.subtreeRecycle(node.A); this.subtreeRecycle(node.B); node.recycle(this); } }; var subtreeRemove = function(subtree, leaf, tree) { if(leaf == subtree){ return null; } else { var parent = leaf.parent; if(parent == subtree){ var other = subtree.otherChild(leaf); other.parent = subtree.parent; subtree.recycle(tree); return other; } else { parent.parent.replaceChild(parent, parent.otherChild(leaf), tree); return subtree; } } }; // **** Marking Functions /* typedef struct MarkContext { bbTree *tree; Node *staticRoot; cpSpatialIndexQueryFunc func; } MarkContext; */ var bbTreeIntersectsNode = function(a, b) { return (a.bb_l <= b.bb_r && b.bb_l <= a.bb_r && a.bb_b <= b.bb_t && b.bb_b <= a.bb_t); }; Leaf.prototype.markLeafQuery = function(leaf, left, tree, func) { if(bbTreeIntersectsNode(leaf, this)){ if(left){ pairInsert(leaf, this, tree); } else { if(this.stamp < leaf.stamp) pairInsert(this, leaf, tree); if(func) func(leaf.obj, this.obj); } } }; Node.prototype.markLeafQuery = function(leaf, left, tree, func) { if(bbTreeIntersectsNode(leaf, this)){ this.A.markLeafQuery(leaf, left, tree, func); this.B.markLeafQuery(leaf, left, tree, func); } }; Leaf.prototype.markSubtree = function(tree, staticRoot, func) { if(this.stamp == tree.getStamp()){ if(staticRoot) staticRoot.markLeafQuery(this, false, tree, func); for(var node = this; node.parent; node = node.parent){ if(node == node.parent.A){ node.parent.B.markLeafQuery(this, true, tree, func); } else { node.parent.A.markLeafQuery(this, false, tree, func); } } } else { var pair = this.pairs; while(pair){ if(this === pair.leafB){ if(func) func(pair.leafA.obj, this.obj); pair = pair.nextB; } else { pair = pair.nextA; } } } }; Node.prototype.markSubtree = function(tree, staticRoot, func) { this.A.markSubtree(tree, staticRoot, func); this.B.markSubtree(tree, staticRoot, func); }; // **** Leaf Functions Leaf.prototype.containsObj = function(obj) { return (this.bb_l <= obj.bb_l && this.bb_r >= obj.bb_r && this.bb_b <= obj.bb_b && this.bb_t >= obj.bb_t); }; Leaf.prototype.update = function(tree) { var root = tree.root; var obj = this.obj; //if(!bbContainsBB(this.bb, bb)){ if(!this.containsObj(obj)){ tree.getBB(this.obj, this); root = subtreeRemove(root, this, tree); tree.root = subtreeInsert(root, this, tree); this.clearPairs(tree); this.stamp = tree.getStamp(); return true; } return false; }; Leaf.prototype.addPairs = function(tree) { var dynamicIndex = tree.dynamicIndex; if(dynamicIndex){ var dynamicRoot = dynamicIndex.root; if(dynamicRoot){ dynamicRoot.markLeafQuery(this, true, dynamicIndex, null); } } else { var staticRoot = tree.staticIndex.root; this.markSubtree(tree, staticRoot, null); } }; // **** Insert/Remove BBTree.prototype.insert = function(obj, hashid) { var leaf = new Leaf(this, obj); this.leaves[hashid] = leaf; this.root = subtreeInsert(this.root, leaf, this); this.count++; leaf.stamp = this.getStamp(); leaf.addPairs(this); this.incrementStamp(); }; BBTree.prototype.remove = function(obj, hashid) { var leaf = this.leaves[hashid]; delete this.leaves[hashid]; this.root = subtreeRemove(this.root, leaf, this); this.count--; leaf.clearPairs(this); leaf.recycle(this); }; BBTree.prototype.contains = function(obj, hashid) { return this.leaves[hashid] != null; }; // **** Reindex var voidQueryFunc = function(obj1, obj2){}; BBTree.prototype.reindexQuery = function(func) { if(!this.root) return; // LeafUpdate() may modify this.root. Don't cache it. var hashid, leaves = this.leaves; for (hashid in leaves) { leaves[hashid].update(this); } var staticIndex = this.staticIndex; var staticRoot = staticIndex && staticIndex.root; this.root.markSubtree(this, staticRoot, func); if(staticIndex && !staticRoot) this.collideStatic(this, staticIndex, func); this.incrementStamp(); }; BBTree.prototype.reindex = function() { this.reindexQuery(voidQueryFunc); }; BBTree.prototype.reindexObject = function(obj, hashid) { var leaf = this.leaves[hashid]; if(leaf){ if(leaf.update(this)) leaf.addPairs(this); this.incrementStamp(); } }; // **** Query // This has since been removed from upstream Chipmunk - which recommends you just use query() below // directly. BBTree.prototype.pointQuery = function(point, func) { this.query(new BB(point.x, point.y, point.x, point.y), func); }; BBTree.prototype.segmentQuery = function(a, b, t_exit, func) { if(this.root) subtreeSegmentQuery(this.root, a, b, t_exit, func); }; BBTree.prototype.query = function(bb, func) { if(this.root) subtreeQuery(this.root, bb, func); }; // **** Misc BBTree.prototype.count = function() { return this.count; }; BBTree.prototype.each = function(func) { var hashid; for(hashid in this.leaves) { func(this.leaves[hashid].obj); } }; // **** Tree Optimization var bbTreeMergedArea2 = function(node, l, b, r, t) { return (max(node.bb_r, r) - min(node.bb_l, l))*(max(node.bb_t, t) - min(node.bb_b, b)); }; var partitionNodes = function(tree, nodes, offset, count) { if(count == 1){ return nodes[offset]; } else if(count == 2) { return tree.makeNode(nodes[offset], nodes[offset + 1]); } // Find the AABB for these nodes //var bb = nodes[offset].bb; var node = nodes[offset]; var bb_l = node.bb_l, bb_b = node.bb_b, bb_r = node.bb_r, bb_t = node.bb_t; var end = offset + count; for(var i=offset + 1; i bb_t - bb_b); // Sort the bounds and use the median as the splitting point var bounds = new Array(count*2); if(splitWidth){ for(var i=offset; inext = next; if(prev.body_a === body) { prev.thread_a_next = next; } else { prev.thread_b_next = next; } } else { body.arbiterList = next; } if(next){ // cpArbiterThreadForBody(next, body)->prev = prev; if(next.body_a === body){ next.thread_a_prev = prev; } else { next.thread_b_prev = prev; } } }; Arbiter.prototype.unthread = function() { unthreadHelper(this, this.body_a, this.thread_a_prev, this.thread_a_next); unthreadHelper(this, this.body_b, this.thread_b_prev, this.thread_b_next); this.thread_a_prev = this.thread_a_next = null; this.thread_b_prev = this.thread_b_next = null; }; //cpFloat //cpContactsEstimateCrushingImpulse(cpContact *contacts, int numContacts) //{ // cpFloat fsum = 0; // cpVect vsum = vzero; // // for(int i=0; i= mindist*mindist) return; var dist = Math.sqrt(distsq); // Allocate and initialize the contact. return new Contact( vadd(p1, vmult(delta, 0.5 + (r1 - 0.5*mindist)/(dist ? dist : Infinity))), (dist ? vmult(delta, 1/dist) : new Vect(1, 0)), dist - mindist, 0 ); }; // Collide circle shapes. var circle2circle = function(circ1, circ2) { var contact = circle2circleQuery(circ1.tc, circ2.tc, circ1.r, circ2.r); return contact ? [contact] : NONE; }; var circle2segment = function(circleShape, segmentShape) { var seg_a = segmentShape.ta; var seg_b = segmentShape.tb; var center = circleShape.tc; var seg_delta = vsub(seg_b, seg_a); var closest_t = clamp01(vdot(seg_delta, vsub(center, seg_a))/vlengthsq(seg_delta)); var closest = vadd(seg_a, vmult(seg_delta, closest_t)); var contact = circle2circleQuery(center, closest, circleShape.r, segmentShape.r); if(contact){ var n = contact.n; // Reject endcap collisions if tangents are provided. return ( (closest_t === 0 && vdot(n, segmentShape.a_tangent) < 0) || (closest_t === 1 && vdot(n, segmentShape.b_tangent) < 0) ) ? NONE : [contact]; } else { return NONE; } } // Find the minimum separating axis for the given poly and axis list. // // This function needs to return two values - the index of the min. separating axis and // the value itself. Short of inlining MSA, returning values through a global like this // is the fastest implementation. // // See: http://jsperf.com/return-two-values-from-function/2 var last_MSA_min = 0; var findMSA = function(poly, planes) { var min_index = 0; var min = poly.valueOnAxis(planes[0].n, planes[0].d); if(min > 0) return -1; for(var i=1; i 0) { return -1; } else if(dist > min){ min = dist; min_index = i; } } last_MSA_min = min; return min_index; }; // Add contacts for probably penetrating vertexes. // This handles the degenerate case where an overlap was detected, but no vertexes fall inside // the opposing polygon. (like a star of david) var findVertsFallback = function(poly1, poly2, n, dist) { var arr = []; var verts1 = poly1.tVerts; for(var i=0; i>1))); } } var verts2 = poly2.tVerts; for(var i=0; i>1))); } } return (arr.length ? arr : findVertsFallback(poly1, poly2, n, dist)); }; // Collide poly shapes together. var poly2poly = function(poly1, poly2) { var mini1 = findMSA(poly2, poly1.tPlanes); if(mini1 == -1) return NONE; var min1 = last_MSA_min; var mini2 = findMSA(poly1, poly2.tPlanes); if(mini2 == -1) return NONE; var min2 = last_MSA_min; // There is overlap, find the penetrating verts if(min1 > min2) return findVerts(poly1, poly2, poly1.tPlanes[mini1].n, min1); else return findVerts(poly1, poly2, vneg(poly2.tPlanes[mini2].n), min2); }; // Like cpPolyValueOnAxis(), but for segments. var segValueOnAxis = function(seg, n, d) { var a = vdot(n, seg.ta) - seg.r; var b = vdot(n, seg.tb) - seg.r; return min(a, b) - d; }; // Identify vertexes that have penetrated the segment. var findPointsBehindSeg = function(arr, seg, poly, pDist, coef) { var dta = vcross(seg.tn, seg.ta); var dtb = vcross(seg.tn, seg.tb); var n = vmult(seg.tn, coef); var verts = poly.tVerts; for(var i=0; i= dt && dt >= dtb){ arr.push(new Contact(new Vect(vx, vy), n, pDist, hashPair(poly.hashid, i))); } } } }; // This one is complicated and gross. Just don't go there... // TODO: Comment me! var seg2poly = function(seg, poly) { var arr = []; var planes = poly.tPlanes; var numVerts = planes.length; var segD = vdot(seg.tn, seg.ta); var minNorm = poly.valueOnAxis(seg.tn, segD) - seg.r; var minNeg = poly.valueOnAxis(vneg(seg.tn), -segD) - seg.r; if(minNeg > 0 || minNorm > 0) return NONE; var mini = 0; var poly_min = segValueOnAxis(seg, planes[0].n, planes[0].d); if(poly_min > 0) return NONE; for(var i=0; i 0){ return NONE; } else if(dist > poly_min){ poly_min = dist; mini = i; } } var poly_n = vneg(planes[mini].n); var va = vadd(seg.ta, vmult(poly_n, seg.r)); var vb = vadd(seg.tb, vmult(poly_n, seg.r)); if(poly.containsVert(va.x, va.y)) arr.push(new Contact(va, poly_n, poly_min, hashPair(seg.hashid, 0))); if(poly.containsVert(vb.x, vb.y)) arr.push(new Contact(vb, poly_n, poly_min, hashPair(seg.hashid, 1))); // Floating point precision problems here. // This will have to do for now. // poly_min -= cp_collision_slop; // TODO is this needed anymore? if(minNorm >= poly_min || minNeg >= poly_min) { if(minNorm > minNeg) findPointsBehindSeg(arr, seg, poly, minNorm, 1); else findPointsBehindSeg(arr, seg, poly, minNeg, -1); } // If no other collision points are found, try colliding endpoints. if(arr.length === 0){ var mini2 = mini * 2; var verts = poly.tVerts; var poly_a = new Vect(verts[mini2], verts[mini2+1]); var con; if((con = circle2circleQuery(seg.ta, poly_a, seg.r, 0, arr))) return [con]; if((con = circle2circleQuery(seg.tb, poly_a, seg.r, 0, arr))) return [con]; var len = numVerts * 2; var poly_b = new Vect(verts[(mini2+2)%len], verts[(mini2+3)%len]); if((con = circle2circleQuery(seg.ta, poly_b, seg.r, 0, arr))) return [con]; if((con = circle2circleQuery(seg.tb, poly_b, seg.r, 0, arr))) return [con]; } // console.log(poly.tVerts, poly.tPlanes); // console.log('seg2poly', arr); return arr; }; // This one is less gross, but still gross. // TODO: Comment me! var circle2poly = function(circ, poly) { var planes = poly.tPlanes; var mini = 0; var min = vdot(planes[0].n, circ.tc) - planes[0].d - circ.r; for(var i=0; i 0){ return NONE; } else if(dist > min) { min = dist; mini = i; } } var n = planes[mini].n; var verts = poly.tVerts; var len = verts.length; var mini2 = mini<<1; //var a = poly.tVerts[mini]; //var b = poly.tVerts[(mini + 1)%poly.tVerts.length]; var ax = verts[mini2]; var ay = verts[mini2+1]; var bx = verts[(mini2+2)%len]; var by = verts[(mini2+3)%len]; var dta = vcross2(n.x, n.y, ax, ay); var dtb = vcross2(n.x, n.y, bx, by); var dt = vcross(n, circ.tc); if(dt < dtb){ var con = circle2circleQuery(circ.tc, new Vect(bx, by), circ.r, 0, con); return con ? [con] : NONE; } else if(dt < dta) { return [new Contact( vsub(circ.tc, vmult(n, circ.r + min/2)), vneg(n), min, 0 )]; } else { var con = circle2circleQuery(circ.tc, new Vect(ax, ay), circ.r, 0, con); return con ? [con] : NONE; } }; // The javascripty way to do this would be either nested object or methods on the prototypes. // // However, the *fastest* way is the method below. // See: http://jsperf.com/dispatch // These are copied from the prototypes into the actual objects in the Shape constructor. CircleShape.prototype.collisionCode = 0; SegmentShape.prototype.collisionCode = 1; PolyShape.prototype.collisionCode = 2; CircleShape.prototype.collisionTable = [ circle2circle, circle2segment, circle2poly ]; SegmentShape.prototype.collisionTable = [ null, function(segA, segB) { return NONE; }, // seg2seg seg2poly ]; PolyShape.prototype.collisionTable = [ null, null, poly2poly ]; var collideShapes = cp.collideShapes = function(a, b) { assert(a.collisionCode <= b.collisionCode, 'Collided shapes must be sorted by type'); return a.collisionTable[b.collisionCode](a, b); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var defaultCollisionHandler = new CollisionHandler(); /// Basic Unit of Simulation in Chipmunk var Space = cp.Space = function() { this.stamp = 0; this.curr_dt = 0; this.bodies = []; this.rousedBodies = []; this.sleepingComponents = []; this.staticShapes = new BBTree(null); this.activeShapes = new BBTree(this.staticShapes); this.arbiters = []; this.contactBuffersHead = null; this.cachedArbiters = {}; //this.pooledArbiters = []; this.constraints = []; this.locked = 0; this.collisionHandlers = {}; this.defaultHandler = defaultCollisionHandler; this.postStepCallbacks = []; /// Number of iterations to use in the impulse solver to solve contacts. this.iterations = 10; /// Gravity to pass to rigid bodies when integrating velocity. this.gravity = vzero; /// Damping rate expressed as the fraction of velocity bodies retain each second. /// A value of 0.9 would mean that each body's velocity will drop 10% per second. /// The default value is 1.0, meaning no damping is applied. /// @note This damping value is different than those of cpDampedSpring and cpDampedRotarySpring. this.damping = 1; /// Speed threshold for a body to be considered idle. /// The default value of 0 means to let the space guess a good threshold based on gravity. this.idleSpeedThreshold = 0; /// Time a group of bodies must remain idle in order to fall asleep. /// Enabling sleeping also implicitly enables the the contact graph. /// The default value of Infinity disables the sleeping algorithm. this.sleepTimeThreshold = Infinity; /// Amount of encouraged penetration between colliding shapes.. /// Used to reduce oscillating contacts and keep the collision cache warm. /// Defaults to 0.1. If you have poor simulation quality, /// increase this number as much as possible without allowing visible amounts of overlap. this.collisionSlop = 0.1; /// Determines how fast overlapping shapes are pushed apart. /// Expressed as a fraction of the error remaining after each second. /// Defaults to pow(1.0 - 0.1, 60.0) meaning that Chipmunk fixes 10% of overlap each frame at 60Hz. this.collisionBias = Math.pow(1 - 0.1, 60); /// Number of frames that contact information should persist. /// Defaults to 3. There is probably never a reason to change this value. this.collisionPersistence = 3; /// Rebuild the contact graph during each step. Must be enabled to use the cpBodyEachArbiter() function. /// Disabled by default for a small performance boost. Enabled implicitly when the sleeping feature is enabled. this.enableContactGraph = false; /// The designated static body for this space. /// You can modify this body, or replace it with your own static body. /// By default it points to a statically allocated cpBody in the cpSpace struct. this.staticBody = new Body(Infinity, Infinity); this.staticBody.nodeIdleTime = Infinity; // Cache the collideShapes callback function for the space. this.collideShapes = this.makeCollideShapes(); }; Space.prototype.getCurrentTimeStep = function() { return this.curr_dt; }; Space.prototype.setIterations = function(iter) { this.iterations = iter; }; /// returns true from inside a callback and objects cannot be added/removed. Space.prototype.isLocked = function() { return this.locked; }; var assertSpaceUnlocked = function(space) { assert(!space.locked, "This addition/removal cannot be done safely during a call to cpSpaceStep() \ or during a query. Put these calls into a post-step callback."); }; // **** Collision handler function management /// Set a collision handler to be used whenever the two shapes with the given collision types collide. /// You can pass null for any function you don't want to implement. Space.prototype.addCollisionHandler = function(a, b, begin, preSolve, postSolve, separate) { assertSpaceUnlocked(this); // Remove any old function so the new one will get added. this.removeCollisionHandler(a, b); var handler = new CollisionHandler(); handler.a = a; handler.b = b; if(begin) handler.begin = begin; if(preSolve) handler.preSolve = preSolve; if(postSolve) handler.postSolve = postSolve; if(separate) handler.separate = separate; this.collisionHandlers[hashPair(a, b)] = handler; }; /// Unset a collision handler. Space.prototype.removeCollisionHandler = function(a, b) { assertSpaceUnlocked(this); delete this.collisionHandlers[hashPair(a, b)]; }; /// Set a default collision handler for this space. /// The default collision handler is invoked for each colliding pair of shapes /// that isn't explicitly handled by a specific collision handler. /// You can pass null for any function you don't want to implement. Space.prototype.setDefaultCollisionHandler = function(begin, preSolve, postSolve, separate) { assertSpaceUnlocked(this); var handler = new CollisionHandler(); if(begin) handler.begin = begin; if(preSolve) handler.preSolve = preSolve; if(postSolve) handler.postSolve = postSolve; if(separate) handler.separate = separate; this.defaultHandler = handler; }; Space.prototype.lookupHandler = function(a, b) { return this.collisionHandlers[hashPair(a, b)] || this.defaultHandler; }; // **** Body, Shape, and Joint Management /// Add a collision shape to the simulation. /// If the shape is attached to a static body, it will be added as a static shape. Space.prototype.addShape = function(shape) { var body = shape.body; if(body.isStatic()) return this.addStaticShape(shape); assert(!shape.space, "This shape is already added to a space and cannot be added to another."); assertSpaceUnlocked(this); body.activate(); body.addShape(shape); shape.update(body.p, body.rot); this.activeShapes.insert(shape, shape.hashid); shape.space = this; return shape; }; /// Explicity add a shape as a static shape to the simulation. Space.prototype.addStaticShape = function(shape) { assert(!shape.space, "This shape is already added to a space and cannot be added to another."); assertSpaceUnlocked(this); var body = shape.body; body.addShape(shape); shape.update(body.p, body.rot); this.staticShapes.insert(shape, shape.hashid); shape.space = this; return shape; }; /// Add a rigid body to the simulation. Space.prototype.addBody = function(body) { assert(!body.isStatic(), "Static bodies cannot be added to a space as they are not meant to be simulated."); assert(!body.space, "This body is already added to a space and cannot be added to another."); assertSpaceUnlocked(this); this.bodies.push(body); body.space = this; return body; }; /// Add a constraint to the simulation. Space.prototype.addConstraint = function(constraint) { assert(!constraint.space, "This shape is already added to a space and cannot be added to another."); assertSpaceUnlocked(this); var a = constraint.a, b = constraint.b; a.activate(); b.activate(); this.constraints.push(constraint); // Push onto the heads of the bodies' constraint lists constraint.next_a = a.constraintList; a.constraintList = constraint; constraint.next_b = b.constraintList; b.constraintList = constraint; constraint.space = this; return constraint; }; Space.prototype.filterArbiters = function(body, filter) { for (var hash in this.cachedArbiters) { var arb = this.cachedArbiters[hash]; // Match on the filter shape, or if it's null the filter body if( (body === arb.body_a && (filter === arb.a || filter === null)) || (body === arb.body_b && (filter === arb.b || filter === null)) ){ // Call separate when removing shapes. if(filter && arb.state !== 'cached') arb.callSeparate(this); arb.unthread(); deleteObjFromList(this.arbiters, arb); //this.pooledArbiters.push(arb); delete this.cachedArbiters[hash]; } } }; /// Remove a collision shape from the simulation. Space.prototype.removeShape = function(shape) { var body = shape.body; if(body.isStatic()){ this.removeStaticShape(shape); } else { assert(this.containsShape(shape), "Cannot remove a shape that was not added to the space. (Removed twice maybe?)"); assertSpaceUnlocked(this); body.activate(); body.removeShape(shape); this.filterArbiters(body, shape); this.activeShapes.remove(shape, shape.hashid); shape.space = null; } }; /// Remove a collision shape added using addStaticShape() from the simulation. Space.prototype.removeStaticShape = function(shape) { assert(this.containsShape(shape), "Cannot remove a static or sleeping shape that was not added to the space. (Removed twice maybe?)"); assertSpaceUnlocked(this); var body = shape.body; if(body.isStatic()) body.activateStatic(shape); body.removeShape(shape); this.filterArbiters(body, shape); this.staticShapes.remove(shape, shape.hashid); shape.space = null; }; /// Remove a rigid body from the simulation. Space.prototype.removeBody = function(body) { assert(this.containsBody(body), "Cannot remove a body that was not added to the space. (Removed twice maybe?)"); assertSpaceUnlocked(this); body.activate(); // this.filterArbiters(body, null); deleteObjFromList(this.bodies, body); body.space = null; }; /// Remove a constraint from the simulation. Space.prototype.removeConstraint = function(constraint) { assert(this.containsConstraint(constraint), "Cannot remove a constraint that was not added to the space. (Removed twice maybe?)"); assertSpaceUnlocked(this); constraint.a.activate(); constraint.b.activate(); deleteObjFromList(this.constraints, constraint); constraint.a.removeConstraint(constraint); constraint.b.removeConstraint(constraint); constraint.space = null; }; /// Test if a collision shape has been added to the space. Space.prototype.containsShape = function(shape) { return (shape.space === this); }; /// Test if a rigid body has been added to the space. Space.prototype.containsBody = function(body) { return (body.space == this); }; /// Test if a constraint has been added to the space. Space.prototype.containsConstraint = function(constraint) { return (constraint.space == this); }; Space.prototype.uncacheArbiter = function(arb) { delete this.cachedArbiters[hashPair(arb.a.hashid, arb.b.hashid)]; deleteObjFromList(this.arbiters, arb); }; // **** Iteration /// Call @c func for each body in the space. Space.prototype.eachBody = function(func) { this.lock(); { var bodies = this.bodies; for(var i=0; i keThreshold ? 0 : body.nodeIdleTime + dt); } } // Awaken any sleeping bodies found and then push arbiters to the bodies' lists. var arbiters = this.arbiters; for(var i=0, count=arbiters.length; i= 0, "Internal Error: Space lock underflow."); if(this.locked === 0 && runPostStep){ var waking = this.rousedBodies; for(var i=0; i this.collisionPersistence){ // The tail buffer is available, rotate the ring var tail = head.next; tail.stamp = stamp; tail.contacts.length = 0; this.contactBuffersHead = tail; } else { // Allocate a new buffer and push it into the ring var buffer = new ContactBuffer(stamp, head); this.contactBuffersHead = head.next = buffer; } }; cpContact * cpContactBufferGetArray(cpSpace *space) { if(space.contactBuffersHead.numContacts + CP_MAX_CONTACTS_PER_ARBITER > CP_CONTACTS_BUFFER_SIZE){ // contact buffer could overflow on the next collision, push a fresh one. space.pushFreshContactBuffer(); } cpContactBufferHeader *head = space.contactBuffersHead; return ((cpContactBuffer *)head)->contacts + head.numContacts; } void cpSpacePushContacts(cpSpace *space, int count) { cpAssertHard(count <= CP_MAX_CONTACTS_PER_ARBITER, "Internal Error: Contact buffer overflow!"); space.contactBuffersHead.numContacts += count; } static void cpSpacePopContacts(cpSpace *space, int count){ space.contactBuffersHead.numContacts -= count; } */ // **** Collision Detection Functions /* Use this to re-enable object pools. static void * cpSpaceArbiterSetTrans(cpShape **shapes, cpSpace *space) { if(space.pooledArbiters.num == 0){ // arbiter pool is exhausted, make more int count = CP_BUFFER_BYTES/sizeof(cpArbiter); cpAssertHard(count, "Internal Error: Buffer size too small."); cpArbiter *buffer = (cpArbiter *)cpcalloc(1, CP_BUFFER_BYTES); cpArrayPush(space.allocatedBuffers, buffer); for(int i=0; i b.collisionCode){ var temp = a; a = b; b = temp; } // Narrow-phase collision detection. //cpContact *contacts = cpContactBufferGetArray(space); //int numContacts = cpCollideShapes(a, b, contacts); var contacts = collideShapes(a, b); if(contacts.length === 0) return; // Shapes are not colliding. //cpSpacePushContacts(space, numContacts); // Get an arbiter from space.arbiterSet for the two shapes. // This is where the persistant contact magic comes from. var arbHash = hashPair(a.hashid, b.hashid); var arb = space.cachedArbiters[arbHash]; if (!arb){ arb = space.cachedArbiters[arbHash] = new Arbiter(a, b); } arb.update(contacts, handler, a, b); // Call the begin function first if it's the first step if(arb.state == 'first coll' && !handler.begin(arb, space)){ arb.ignore(); // permanently ignore the collision until separation } if( // Ignore the arbiter if it has been flagged (arb.state !== 'ignore') && // Call preSolve handler.preSolve(arb, space) && // Process, but don't add collisions for sensors. !sensor ){ space.arbiters.push(arb); } else { //cpSpacePopContacts(space, numContacts); arb.contacts = null; // Normally arbiters are set as used after calling the post-solve callback. // However, post-solve callbacks are not called for sensors or arbiters rejected from pre-solve. if(arb.state !== 'ignore') arb.state = 'normal'; } // Time stamp the arbiter so we know it was used recently. arb.stamp = space.stamp; }; }; // Hashset filter func to throw away old arbiters. Space.prototype.arbiterSetFilter = function(arb) { var ticks = this.stamp - arb.stamp; var a = arb.body_a, b = arb.body_b; // TODO should make an arbiter state for this so it doesn't require filtering arbiters for // dangling body pointers on body removal. // Preserve arbiters on sensors and rejected arbiters for sleeping objects. // This prevents errant separate callbacks from happenening. if( (a.isStatic() || a.isSleeping()) && (b.isStatic() || b.isSleeping()) ){ return true; } // Arbiter was used last frame, but not this one if(ticks >= 1 && arb.state != 'cached'){ arb.callSeparate(this); arb.state = 'cached'; } if(ticks >= this.collisionPersistence){ arb.contacts = null; //cpArrayPush(this.pooledArbiters, arb); return false; } return true; }; // **** All Important cpSpaceStep() Function var updateFunc = function(shape) { var body = shape.body; shape.update(body.p, body.rot); }; /// Step the space forward in time by @c dt. Space.prototype.step = function(dt) { // don't step if the timestep is 0! if(dt === 0) return; assert(vzero.x === 0 && vzero.y === 0, "vzero is invalid"); this.stamp++; var prev_dt = this.curr_dt; this.curr_dt = dt; var i; var j; var hash; var bodies = this.bodies; var constraints = this.constraints; var arbiters = this.arbiters; // Reset and empty the arbiter lists. for(i=0; imaxForce*(dt)) // a and b are bodies. var relative_velocity = function(a, b, r1, r2){ //var v1_sum = vadd(a.v, vmult(vperp(r1), a.w)); var v1_sumx = a.vx + (-r1.y) * a.w; var v1_sumy = a.vy + ( r1.x) * a.w; //var v2_sum = vadd(b.v, vmult(vperp(r2), b.w)); var v2_sumx = b.vx + (-r2.y) * b.w; var v2_sumy = b.vy + ( r2.x) * b.w; // return vsub(v2_sum, v1_sum); return new Vect(v2_sumx - v1_sumx, v2_sumy - v1_sumy); }; var normal_relative_velocity = function(a, b, r1, r2, n){ //return vdot(relative_velocity(a, b, r1, r2), n); var v1_sumx = a.vx + (-r1.y) * a.w; var v1_sumy = a.vy + ( r1.x) * a.w; var v2_sumx = b.vx + (-r2.y) * b.w; var v2_sumy = b.vy + ( r2.x) * b.w; return vdot2(v2_sumx - v1_sumx, v2_sumy - v1_sumy, n.x, n.y); }; /* var apply_impulse = function(body, j, r){ body.v = vadd(body.v, vmult(j, body.m_inv)); body.w += body.i_inv*vcross(r, j); }; var apply_impulses = function(a, b, r1, r2, j) { apply_impulse(a, vneg(j), r1); apply_impulse(b, j, r2); }; */ var apply_impulse = function(body, jx, jy, r){ // body.v = body.v.add(vmult(j, body.m_inv)); body.vx += jx * body.m_inv; body.vy += jy * body.m_inv; // body.w += body.i_inv*vcross(r, j); body.w += body.i_inv*(r.x*jy - r.y*jx); }; var apply_impulses = function(a, b, r1, r2, jx, jy) { apply_impulse(a, -jx, -jy, r1); apply_impulse(b, jx, jy, r2); }; var apply_bias_impulse = function(body, jx, jy, r) { //body.v_bias = vadd(body.v_bias, vmult(j, body.m_inv)); body.v_biasx += jx * body.m_inv; body.v_biasy += jy * body.m_inv; body.w_bias += body.i_inv*vcross2(r.x, r.y, jx, jy); }; /* var apply_bias_impulses = function(a, b, r1, r2, j) { apply_bias_impulse(a, vneg(j), r1); apply_bias_impulse(b, j, r2); };*/ var k_scalar_body = function(body, r, n) { var rcn = vcross(r, n); return body.m_inv + body.i_inv*rcn*rcn; }; var k_scalar = function(a, b, r1, r2, n) { var value = k_scalar_body(a, r1, n) + k_scalar_body(b, r2, n); assertSoft(value !== 0, "Unsolvable collision or constraint."); return value; }; // k1 and k2 are modified by the function to contain the outputs. var k_tensor = function(a, b, r1, r2, k1, k2) { // calculate mass matrix // If I wasn't lazy and wrote a proper matrix class, this wouldn't be so gross... var k11, k12, k21, k22; var m_sum = a.m_inv + b.m_inv; // start with I*m_sum k11 = m_sum; k12 = 0; k21 = 0; k22 = m_sum; // add the influence from r1 var a_i_inv = a.i_inv; var r1xsq = r1.x * r1.x * a_i_inv; var r1ysq = r1.y * r1.y * a_i_inv; var r1nxy = -r1.x * r1.y * a_i_inv; k11 += r1ysq; k12 += r1nxy; k21 += r1nxy; k22 += r1xsq; // add the influnce from r2 var b_i_inv = b.i_inv; var r2xsq = r2.x * r2.x * b_i_inv; var r2ysq = r2.y * r2.y * b_i_inv; var r2nxy = -r2.x * r2.y * b_i_inv; k11 += r2ysq; k12 += r2nxy; k21 += r2nxy; k22 += r2xsq; // invert var determinant = k11*k22 - k12*k21; assertSoft(determinant !== 0, "Unsolvable constraint."); var det_inv = 1/determinant; k1.x = k22*det_inv; k1.y = -k12*det_inv; k2.x = -k21*det_inv; k2.y = k11*det_inv; }; var mult_k = function(vr, k1, k2) { return new Vect(vdot(vr, k1), vdot(vr, k2)); }; var bias_coef = function(errorBias, dt) { return 1 - Math.pow(errorBias, dt); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ // TODO: Comment me! // a and b are bodies that the constraint applies to. var Constraint = cp.Constraint = function(a, b) { /// The first body connected to this constraint. this.a = a; /// The second body connected to this constraint. this.b = b; this.space = null; this.next_a = null; this.next_b = null; /// The maximum force that this constraint is allowed to use. this.maxForce = Infinity; /// The rate at which joint error is corrected. /// Defaults to pow(1 - 0.1, 60) meaning that it will /// correct 10% of the error every 1/60th of a second. this.errorBias = Math.pow(1 - 0.1, 60); /// The maximum rate at which joint error is corrected. this.maxBias = Infinity; }; Constraint.prototype.activateBodies = function() { if(this.a) this.a.activate(); if(this.b) this.b.activate(); }; /// These methods are overridden by the constraint itself. Constraint.prototype.preStep = function(dt) {}; Constraint.prototype.applyCachedImpulse = function(dt_coef) {}; Constraint.prototype.applyImpulse = function() {}; Constraint.prototype.getImpulse = function() { return 0; }; /// Function called before the solver runs. This can be overridden by the user /// to customize the constraint. /// Animate your joint anchors, update your motor torque, etc. Constraint.prototype.preSolve = function(space) {}; /// Function called after the solver runs. This can be overridden by the user /// to customize the constraint. /// Use the applied impulse to perform effects like breakable joints. Constraint.prototype.postSolve = function(space) {}; Constraint.prototype.next = function(body) { return (this.a === body ? this.next_a : this.next_b); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var PinJoint = cp.PinJoint = function(a, b, anchr1, anchr2) { Constraint.call(this, a, b); this.anchr1 = anchr1; this.anchr2 = anchr2; // STATIC_BODY_CHECK var p1 = (a ? vadd(a.p, vrotate(anchr1, a.rot)) : anchr1); var p2 = (b ? vadd(b.p, vrotate(anchr2, b.rot)) : anchr2); this.dist = vlength(vsub(p2, p1)); assertSoft(this.dist > 0, "You created a 0 length pin joint. A pivot joint will be much more stable."); this.r1 = this.r2 = null; this.n = null; this.nMass = 0; this.jnAcc = this.jnMax = 0; this.bias = 0; }; PinJoint.prototype = Object.create(Constraint.prototype); PinJoint.prototype.preStep = function(dt) { var a = this.a; var b = this.b; this.r1 = vrotate(this.anchr1, a.rot); this.r2 = vrotate(this.anchr2, b.rot); var delta = vsub(vadd(b.p, this.r2), vadd(a.p, this.r1)); var dist = vlength(delta); this.n = vmult(delta, 1/(dist ? dist : Infinity)); // calculate mass normal this.nMass = 1/k_scalar(a, b, this.r1, this.r2, this.n); // calculate bias velocity var maxBias = this.maxBias; this.bias = clamp(-bias_coef(this.errorBias, dt)*(dist - this.dist)/dt, -maxBias, maxBias); // compute max impulse this.jnMax = this.maxForce * dt; }; PinJoint.prototype.applyCachedImpulse = function(dt_coef) { var j = vmult(this.n, this.jnAcc*dt_coef); apply_impulses(this.a, this.b, this.r1, this.r2, j.x, j.y); }; PinJoint.prototype.applyImpulse = function() { var a = this.a; var b = this.b; var n = this.n; // compute relative velocity var vrn = normal_relative_velocity(a, b, this.r1, this.r2, n); // compute normal impulse var jn = (this.bias - vrn)*this.nMass; var jnOld = this.jnAcc; this.jnAcc = clamp(jnOld + jn, -this.jnMax, this.jnMax); jn = this.jnAcc - jnOld; // apply impulse apply_impulses(a, b, this.r1, this.r2, n.x*jn, n.y*jn); }; PinJoint.prototype.getImpulse = function() { return Math.abs(this.jnAcc); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var SlideJoint = cp.SlideJoint = function(a, b, anchr1, anchr2, min, max) { Constraint.call(this, a, b); this.anchr1 = anchr1; this.anchr2 = anchr2; this.min = min; this.max = max; this.r1 = this.r2 = this.n = null; this.nMass = 0; this.jnAcc = this.jnMax = 0; this.bias = 0; }; SlideJoint.prototype = Object.create(Constraint.prototype); SlideJoint.prototype.preStep = function(dt) { var a = this.a; var b = this.b; this.r1 = vrotate(this.anchr1, a.rot); this.r2 = vrotate(this.anchr2, b.rot); var delta = vsub(vadd(b.p, this.r2), vadd(a.p, this.r1)); var dist = vlength(delta); var pdist = 0; if(dist > this.max) { pdist = dist - this.max; this.n = vnormalize_safe(delta); } else if(dist < this.min) { pdist = this.min - dist; this.n = vneg(vnormalize_safe(delta)); } else { this.n = vzero; this.jnAcc = 0; } // calculate mass normal this.nMass = 1/k_scalar(a, b, this.r1, this.r2, this.n); // calculate bias velocity var maxBias = this.maxBias; this.bias = clamp(-bias_coef(this.errorBias, dt)*pdist/dt, -maxBias, maxBias); // compute max impulse this.jnMax = this.maxForce * dt; }; SlideJoint.prototype.applyCachedImpulse = function(dt_coef) { var jn = this.jnAcc * dt_coef; apply_impulses(this.a, this.b, this.r1, this.r2, this.n.x * jn, this.n.y * jn); }; SlideJoint.prototype.applyImpulse = function() { if(this.n.x === 0 && this.n.y === 0) return; // early exit var a = this.a; var b = this.b; var n = this.n; var r1 = this.r1; var r2 = this.r2; // compute relative velocity var vr = relative_velocity(a, b, r1, r2); var vrn = vdot(vr, n); // compute normal impulse var jn = (this.bias - vrn)*this.nMass; var jnOld = this.jnAcc; this.jnAcc = clamp(jnOld + jn, -this.jnMax, 0); jn = this.jnAcc - jnOld; // apply impulse apply_impulses(a, b, this.r1, this.r2, n.x * jn, n.y * jn); }; SlideJoint.prototype.getImpulse = function() { return Math.abs(this.jnAcc); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ // Pivot joints can also be created with (a, b, pivot); var PivotJoint = cp.PivotJoint = function(a, b, anchr1, anchr2) { Constraint.call(this, a, b); if(typeof anchr2 === 'undefined') { var pivot = anchr1; anchr1 = (a ? a.world2Local(pivot) : pivot); anchr2 = (b ? b.world2Local(pivot) : pivot); } this.anchr1 = anchr1; this.anchr2 = anchr2; this.r1 = this.r2 = vzero; this.k1 = new Vect(0,0); this.k2 = new Vect(0,0); this.jAcc = vzero; this.jMaxLen = 0; this.bias = vzero; }; PivotJoint.prototype = Object.create(Constraint.prototype); PivotJoint.prototype.preStep = function(dt) { var a = this.a; var b = this.b; this.r1 = vrotate(this.anchr1, a.rot); this.r2 = vrotate(this.anchr2, b.rot); // Calculate mass tensor. Result is stored into this.k1 & this.k2. k_tensor(a, b, this.r1, this.r2, this.k1, this.k2); // compute max impulse this.jMaxLen = this.maxForce * dt; // calculate bias velocity var delta = vsub(vadd(b.p, this.r2), vadd(a.p, this.r1)); this.bias = vclamp(vmult(delta, -bias_coef(this.errorBias, dt)/dt), this.maxBias); }; PivotJoint.prototype.applyCachedImpulse = function(dt_coef) { apply_impulses(this.a, this.b, this.r1, this.r2, this.jAcc.x * dt_coef, this.jAcc.y * dt_coef); }; PivotJoint.prototype.applyImpulse = function() { var a = this.a; var b = this.b; var r1 = this.r1; var r2 = this.r2; // compute relative velocity var vr = relative_velocity(a, b, r1, r2); // compute normal impulse var j = mult_k(vsub(this.bias, vr), this.k1, this.k2); var jOld = this.jAcc; this.jAcc = vclamp(vadd(this.jAcc, j), this.jMaxLen); // apply impulse apply_impulses(a, b, this.r1, this.r2, this.jAcc.x - jOld.x, this.jAcc.y - jOld.y); }; PivotJoint.prototype.getImpulse = function() { return vlength(this.jAcc); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var GrooveJoint = cp.GrooveJoint = function(a, b, groove_a, groove_b, anchr2) { Constraint.call(this, a, b); this.grv_a = groove_a; this.grv_b = groove_b; this.grv_n = vperp(vnormalize(vsub(groove_b, groove_a))); this.anchr2 = anchr2; this.grv_tn = null; this.clamp = 0; this.r1 = this.r2 = null; this.k1 = new Vect(0,0); this.k2 = new Vect(0,0); this.jAcc = vzero; this.jMaxLen = 0; this.bias = null; }; GrooveJoint.prototype = Object.create(Constraint.prototype); GrooveJoint.prototype.preStep = function(dt) { var a = this.a; var b = this.b; // calculate endpoints in worldspace var ta = a.local2World(this.grv_a); var tb = a.local2World(this.grv_b); // calculate axis var n = vrotate(this.grv_n, a.rot); var d = vdot(ta, n); this.grv_tn = n; this.r2 = vrotate(this.anchr2, b.rot); // calculate tangential distance along the axis of r2 var td = vcross(vadd(b.p, this.r2), n); // calculate clamping factor and r2 if(td <= vcross(ta, n)){ this.clamp = 1; this.r1 = vsub(ta, a.p); } else if(td >= vcross(tb, n)){ this.clamp = -1; this.r1 = vsub(tb, a.p); } else { this.clamp = 0; this.r1 = vsub(vadd(vmult(vperp(n), -td), vmult(n, d)), a.p); } // Calculate mass tensor k_tensor(a, b, this.r1, this.r2, this.k1, this.k2); // compute max impulse this.jMaxLen = this.maxForce * dt; // calculate bias velocity var delta = vsub(vadd(b.p, this.r2), vadd(a.p, this.r1)); this.bias = vclamp(vmult(delta, -bias_coef(this.errorBias, dt)/dt), this.maxBias); }; GrooveJoint.prototype.applyCachedImpulse = function(dt_coef) { apply_impulses(this.a, this.b, this.r1, this.r2, this.jAcc.x * dt_coef, this.jAcc.y * dt_coef); }; GrooveJoint.prototype.grooveConstrain = function(j){ var n = this.grv_tn; var jClamp = (this.clamp*vcross(j, n) > 0) ? j : vproject(j, n); return vclamp(jClamp, this.jMaxLen); }; GrooveJoint.prototype.applyImpulse = function() { var a = this.a; var b = this.b; var r1 = this.r1; var r2 = this.r2; // compute impulse var vr = relative_velocity(a, b, r1, r2); var j = mult_k(vsub(this.bias, vr), this.k1, this.k2); var jOld = this.jAcc; this.jAcc = this.grooveConstrain(vadd(jOld, j)); // apply impulse apply_impulses(a, b, this.r1, this.r2, this.jAcc.x - jOld.x, this.jAcc.y - jOld.y); }; GrooveJoint.prototype.getImpulse = function() { return vlength(this.jAcc); }; GrooveJoint.prototype.setGrooveA = function(value) { this.grv_a = value; this.grv_n = vperp(vnormalize(vsub(this.grv_b, value))); this.activateBodies(); }; GrooveJoint.prototype.setGrooveB = function(value) { this.grv_b = value; this.grv_n = vperp(vnormalize(vsub(value, this.grv_a))); this.activateBodies(); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var defaultSpringForce = function(spring, dist){ return (spring.restLength - dist)*spring.stiffness; }; var DampedSpring = cp.DampedSpring = function(a, b, anchr1, anchr2, restLength, stiffness, damping) { Constraint.call(this, a, b); this.anchr1 = anchr1; this.anchr2 = anchr2; this.restLength = restLength; this.stiffness = stiffness; this.damping = damping; this.springForceFunc = defaultSpringForce; this.target_vrn = this.v_coef = 0; this.r1 = this.r2 = null; this.nMass = 0; this.n = null; }; DampedSpring.prototype = Object.create(Constraint.prototype); DampedSpring.prototype.preStep = function(dt) { var a = this.a; var b = this.b; this.r1 = vrotate(this.anchr1, a.rot); this.r2 = vrotate(this.anchr2, b.rot); var delta = vsub(vadd(b.p, this.r2), vadd(a.p, this.r1)); var dist = vlength(delta); this.n = vmult(delta, 1/(dist ? dist : Infinity)); var k = k_scalar(a, b, this.r1, this.r2, this.n); assertSoft(k !== 0, "Unsolvable this."); this.nMass = 1/k; this.target_vrn = 0; this.v_coef = 1 - Math.exp(-this.damping*dt*k); // apply this force var f_spring = this.springForceFunc(this, dist); apply_impulses(a, b, this.r1, this.r2, this.n.x * f_spring * dt, this.n.y * f_spring * dt); }; DampedSpring.prototype.applyCachedImpulse = function(dt_coef){}; DampedSpring.prototype.applyImpulse = function() { var a = this.a; var b = this.b; var n = this.n; var r1 = this.r1; var r2 = this.r2; // compute relative velocity var vrn = normal_relative_velocity(a, b, r1, r2, n); // compute velocity loss from drag var v_damp = (this.target_vrn - vrn)*this.v_coef; this.target_vrn = vrn + v_damp; v_damp *= this.nMass; apply_impulses(a, b, this.r1, this.r2, this.n.x * v_damp, this.n.y * v_damp); }; DampedSpring.prototype.getImpulse = function() { return 0; }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var defaultSpringTorque = function(spring, relativeAngle){ return (relativeAngle - spring.restAngle)*spring.stiffness; } var DampedRotarySpring = cp.DampedRotarySpring = function(a, b, restAngle, stiffness, damping) { Constraint.call(this, a, b); this.restAngle = restAngle; this.stiffness = stiffness; this.damping = damping; this.springTorqueFunc = defaultSpringTorque; this.target_wrn = 0; this.w_coef = 0; this.iSum = 0; }; DampedRotarySpring.prototype = Object.create(Constraint.prototype); DampedRotarySpring.prototype.preStep = function(dt) { var a = this.a; var b = this.b; var moment = a.i_inv + b.i_inv; assertSoft(moment !== 0, "Unsolvable spring."); this.iSum = 1/moment; this.w_coef = 1 - Math.exp(-this.damping*dt*moment); this.target_wrn = 0; // apply this torque var j_spring = this.springTorqueFunc(this, a.a - b.a)*dt; a.w -= j_spring*a.i_inv; b.w += j_spring*b.i_inv; }; // DampedRotarySpring.prototype.applyCachedImpulse = function(dt_coef){}; DampedRotarySpring.prototype.applyImpulse = function() { var a = this.a; var b = this.b; // compute relative velocity var wrn = a.w - b.w;//normal_relative_velocity(a, b, r1, r2, n) - this.target_vrn; // compute velocity loss from drag // not 100% certain spring is derived correctly, though it makes sense var w_damp = (this.target_wrn - wrn)*this.w_coef; this.target_wrn = wrn + w_damp; //apply_impulses(a, b, this.r1, this.r2, vmult(this.n, v_damp*this.nMass)); var j_damp = w_damp*this.iSum; a.w += j_damp*a.i_inv; b.w -= j_damp*b.i_inv; }; // DampedRotarySpring.prototype.getImpulse = function(){ return 0; }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var RotaryLimitJoint = cp.RotaryLimitJoint = function(a, b, min, max) { Constraint.call(this, a, b); this.min = min; this.max = max; this.jAcc = 0; this.iSum = this.bias = this.jMax = 0; }; RotaryLimitJoint.prototype = Object.create(Constraint.prototype); RotaryLimitJoint.prototype.preStep = function(dt) { var a = this.a; var b = this.b; var dist = b.a - a.a; var pdist = 0; if(dist > this.max) { pdist = this.max - dist; } else if(dist < this.min) { pdist = this.min - dist; } // calculate moment of inertia coefficient. this.iSum = 1/(1/a.i + 1/b.i); // calculate bias velocity var maxBias = this.maxBias; this.bias = clamp(-bias_coef(this.errorBias, dt)*pdist/dt, -maxBias, maxBias); // compute max impulse this.jMax = this.maxForce * dt; // If the bias is 0, the joint is not at a limit. Reset the impulse. if(!this.bias) this.jAcc = 0; }; RotaryLimitJoint.prototype.applyCachedImpulse = function(dt_coef) { var a = this.a; var b = this.b; var j = this.jAcc*dt_coef; a.w -= j*a.i_inv; b.w += j*b.i_inv; }; RotaryLimitJoint.prototype.applyImpulse = function() { if(!this.bias) return; // early exit var a = this.a; var b = this.b; // compute relative rotational velocity var wr = b.w - a.w; // compute normal impulse var j = -(this.bias + wr)*this.iSum; var jOld = this.jAcc; if(this.bias < 0){ this.jAcc = clamp(jOld + j, 0, this.jMax); } else { this.jAcc = clamp(jOld + j, -this.jMax, 0); } j = this.jAcc - jOld; // apply impulse a.w -= j*a.i_inv; b.w += j*b.i_inv; }; RotaryLimitJoint.prototype.getImpulse = function() { return Math.abs(joint.jAcc); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var RatchetJoint = cp.RatchetJoint = function(a, b, phase, ratchet) { Constraint.call(this, a, b); this.angle = 0; this.phase = phase; this.ratchet = ratchet; // STATIC_BODY_CHECK this.angle = (b ? b.a : 0) - (a ? a.a : 0); this.iSum = this.bias = this.jAcc = this.jMax = 0; }; RatchetJoint.prototype = Object.create(Constraint.prototype); RatchetJoint.prototype.preStep = function(dt) { var a = this.a; var b = this.b; var angle = this.angle; var phase = this.phase; var ratchet = this.ratchet; var delta = b.a - a.a; var diff = angle - delta; var pdist = 0; if(diff*ratchet > 0){ pdist = diff; } else { this.angle = Math.floor((delta - phase)/ratchet)*ratchet + phase; } // calculate moment of inertia coefficient. this.iSum = 1/(a.i_inv + b.i_inv); // calculate bias velocity var maxBias = this.maxBias; this.bias = clamp(-bias_coef(this.errorBias, dt)*pdist/dt, -maxBias, maxBias); // compute max impulse this.jMax = this.maxForce * dt; // If the bias is 0, the joint is not at a limit. Reset the impulse. if(!this.bias) this.jAcc = 0; }; RatchetJoint.prototype.applyCachedImpulse = function(dt_coef) { var a = this.a; var b = this.b; var j = this.jAcc*dt_coef; a.w -= j*a.i_inv; b.w += j*b.i_inv; }; RatchetJoint.prototype.applyImpulse = function() { if(!this.bias) return; // early exit var a = this.a; var b = this.b; // compute relative rotational velocity var wr = b.w - a.w; var ratchet = this.ratchet; // compute normal impulse var j = -(this.bias + wr)*this.iSum; var jOld = this.jAcc; this.jAcc = clamp((jOld + j)*ratchet, 0, this.jMax*Math.abs(ratchet))/ratchet; j = this.jAcc - jOld; // apply impulse a.w -= j*a.i_inv; b.w += j*b.i_inv; }; RatchetJoint.prototype.getImpulse = function(joint) { return Math.abs(joint.jAcc); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var GearJoint = cp.GearJoint = function(a, b, phase, ratio) { Constraint.call(this, a, b); this.phase = phase; this.ratio = ratio; this.ratio_inv = 1/ratio; this.jAcc = 0; this.iSum = this.bias = this.jMax = 0; }; GearJoint.prototype = Object.create(Constraint.prototype); GearJoint.prototype.preStep = function(dt) { var a = this.a; var b = this.b; // calculate moment of inertia coefficient. this.iSum = 1/(a.i_inv*this.ratio_inv + this.ratio*b.i_inv); // calculate bias velocity var maxBias = this.maxBias; this.bias = clamp(-bias_coef(this.errorBias, dt)*(b.a*this.ratio - a.a - this.phase)/dt, -maxBias, maxBias); // compute max impulse this.jMax = this.maxForce * dt; }; GearJoint.prototype.applyCachedImpulse = function(dt_coef) { var a = this.a; var b = this.b; var j = this.jAcc*dt_coef; a.w -= j*a.i_inv*this.ratio_inv; b.w += j*b.i_inv; }; GearJoint.prototype.applyImpulse = function() { var a = this.a; var b = this.b; // compute relative rotational velocity var wr = b.w*this.ratio - a.w; // compute normal impulse var j = (this.bias - wr)*this.iSum; var jOld = this.jAcc; this.jAcc = clamp(jOld + j, -this.jMax, this.jMax); j = this.jAcc - jOld; // apply impulse a.w -= j*a.i_inv*this.ratio_inv; b.w += j*b.i_inv; }; GearJoint.prototype.getImpulse = function() { return Math.abs(this.jAcc); }; GearJoint.prototype.setRatio = function(value) { this.ratio = value; this.ratio_inv = 1/value; this.activateBodies(); }; /* Copyright (c) 2007 Scott Lembcke * * 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. */ var SimpleMotor = cp.SimpleMotor = function(a, b, rate) { Constraint.call(this, a, b); this.rate = rate; this.jAcc = 0; this.iSum = this.jMax = 0; }; SimpleMotor.prototype = Object.create(Constraint.prototype); SimpleMotor.prototype.preStep = function(dt) { // calculate moment of inertia coefficient. this.iSum = 1/(this.a.i_inv + this.b.i_inv); // compute max impulse this.jMax = this.maxForce * dt; }; SimpleMotor.prototype.applyCachedImpulse = function(dt_coef) { var a = this.a; var b = this.b; var j = this.jAcc*dt_coef; a.w -= j*a.i_inv; b.w += j*b.i_inv; }; SimpleMotor.prototype.applyImpulse = function() { var a = this.a; var b = this.b; // compute relative rotational velocity var wr = b.w - a.w + this.rate; // compute normal impulse var j = -wr*this.iSum; var jOld = this.jAcc; this.jAcc = clamp(jOld + j, -this.jMax, this.jMax); j = this.jAcc - jOld; // apply impulse a.w -= j*a.i_inv; b.w += j*b.i_inv; }; SimpleMotor.prototype.getImpulse = function() { return Math.abs(this.jAcc); }; })(); })() },{}]},{},[12]) ;