1 /**
  2  * arcade.js
  3  * Copyright (c) 2010-2011,  Martin Wendt (http://wwWendt.de)
  4  *
  5  * Dual licensed under the MIT or GPL Version 2 licenses.
  6  * http://code.google.com/p/arcade-js/wiki/LicenseInfo
  7  *
  8  * A current version and some documentation is available at
  9  *     http://arcade-js.googlecode.com/
 10  *
 11  * @fileOverview A 2D game engine that provides a render loop and support for
 12  * multiple moving objects.
 13  *
 14  * @author Martin Wendt
 15  * @version 0.0.1
 16  */
 17 /*******************************************************************************
 18 
 19  * Helpers
 20  */
 21 
 22 /**
 23  * Taken from John Resig's http://ejohn.org/blog/simple-javascript-inheritance/
 24  * Inspired by base2 and Prototype
 25  */
 26 (function(){
 27   var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
 28 
 29   // The base Class implementation (does nothing)
 30   /**@ignore*/
 31   this.Class = function(){};
 32 
 33   // Create a new Class that inherits from this class
 34   Class.extend = function(prop) {
 35 	var _super = this.prototype;
 36 
 37 	// Instantiate a base class (but only create the instance,
 38 	// don't run the init constructor)
 39 	initializing = true;
 40 	var prototype = new this();
 41 	initializing = false;
 42 
 43 	// Copy the properties over onto the new prototype
 44 	for (var name in prop) {
 45 	  // Check if we're overwriting an existing function
 46 	  prototype[name] = typeof prop[name] == "function" &&
 47 		typeof _super[name] == "function" && fnTest.test(prop[name]) ?
 48 		(function(name, fn){
 49 		  return function() {
 50 			var tmp = this._super;
 51 
 52 			// Add a new ._super() method that is the same method
 53 			// but on the super-class
 54 			this._super = _super[name];
 55 
 56 			// The method only need to be bound temporarily, so we
 57 			// remove it when we're done executing
 58 			var ret = fn.apply(this, arguments);
 59 			this._super = tmp;
 60 
 61 			return ret;
 62 		  };
 63 		})(name, prop[name]) :
 64 		prop[name];
 65 	}
 66 
 67 	// The dummy class constructor
 68 	function Class() {
 69 	  // All construction is actually done in the init method
 70 	  if ( !initializing && this.init ){
 71 		this.init.apply(this, arguments);
 72 	  }
 73 	}
 74 
 75 	// Populate our constructed prototype object
 76 	Class.prototype = prototype;
 77 
 78 	// Enforce the constructor to be what we expect
 79 	Class.constructor = Class;
 80 
 81 	// And make this class extendable
 82 	Class.extend = arguments.callee;
 83 
 84 	return Class;
 85   };
 86 })();
 87 
 88 
 89 /*----------------------------------------------------------------------------*/
 90 
 91 
 92 /**
 93  * Sound support based on <audio> element.
 94  * @class
 95  * @example
 96  * var clickSound = new AudioJS('click.wav')
 97  * var sound2 = new AudioJS(['click.ogg', 'click.mp3'])
 98  * var sound3 = new AudioJS({src: ['click.ogg', 'click.mp3'], loop: true)
 99  * [...]
100  * clickSound.play();
101  * 
102  * @param {string|string[]|object} opts Audio URL, list of URLs or option dictionary.
103  * @param {string|string[]} opts.src Audio URL or list of URLs.
104  * @param {boolean} [opts.loop=false]  
105  * @param {float} [opts.volume=1] 
106  */
107 AudioJS = function(opts){
108 	if(typeof opts == "string"){
109 		opts = {src: [opts]};
110 	}else if($.isArray(opts)){
111 		opts = {src: opts};
112 	}
113 	this.opts = $.extend({}, AudioJS.defaultOpts, opts);
114 	this.audio = AudioJS.load(opts.src);
115 }
116 // Static members
117 $.extend(AudioJS,
118 	/** @lends AudioJS  */
119 	{
120 	_soundElement: null,
121 	_audioList: {},
122 	/**true, if browser supports the Audio class.*/
123 	audioObjSupport: undefined,
124 	/**true, if browser supports Audio.play().*/
125 	basicAudioSupport: undefined,
126 	/**true if browser supports looping (repeating) */
127 	loopSupport: undefined,
128 	defaultOpts: {
129 		loop: false,
130 		volume: 1
131 	},
132 	/**Load and cache audio element for this URL.
133 	 * This internal function is called by the constructor. 
134 	 * @param {string} url
135 	 */
136 	load: function(src) {
137 		var tag;
138 		if(typeof src == "string"){
139 			tag = src;
140 		}else{
141 			tag = src.join("~");
142 		}
143 		var audio = this._audioList[tag];
144 		if( !audio ) {
145 			if( !this._soundElement ) {
146 				this._soundElement = document.createElement("div");
147 				this._soundElement.setAttribute("id", "AudioJS-sounds");
148 //				this._soundElement.setAttribute("hidden", true);
149 				document.body.appendChild(this._soundElement);
150 //				$(this._soundElement).bind('ended',{}, function() {
151 //					window.console.log("AudioJS("+this+") loaded");
152 //				});
153 			}
154 			audio = this._audioList[tag] = document.createElement("audio");
155 //			audio.setAttribute("autoplay", true);
156 //			audio.setAttribute("preload", true);
157 			audio.setAttribute("preload", "auto");
158 			audio.setAttribute("autobuffer", true);
159 //			audio.setAttribute("src", url);
160 			for(var i=0; i<src.length; i++){
161 				var srcElement = document.createElement("source");
162 				srcElement.setAttribute("src", src[i]);
163 				audio.appendChild(srcElement);
164 			}
165 			this._soundElement.appendChild(audio);
166 //			var audio2 = document.getElementsByTagName("audio");
167 //			var audio3 = document.createElement("audio");
168 			$(audio).bind("ended", {}, function() {
169 				// TODO: we can simulate looping if it is not natively supported:
170 //			  	$(this).trigger('play');
171 				if(window.console){
172 					window.console.log("AudioJS("+tag+") ended");
173 				}
174 			});
175 		}
176 		return audio;
177 	}
178 });
179 try {
180 	var audio = new Audio("");
181 	AudioJS.audioObjSupport = !!(audio.canPlayType);
182 	AudioJS.basicAudioSupport = !!(!AudioJS.audioObjSupport ? audio.play : false);
183 } catch (e) {
184 	AudioJS.audioObjSupport = false;
185 	AudioJS.basicAudioSupport = false;
186 }
187 
188 AudioJS.prototype = {
189 	/**Return string representation.
190 	 * @returns {string}
191 	 */
192 	toString: function() {
193 		return "AudioJS("+this.opts.src+")";
194 	},
195 	/**Play this sound.
196 	 *  @param {boolean} loop Optional, default: false
197 	 */
198 	play: function(loop) {
199 		if(this.audio.ended === true){
200 			// Interrupt currently playing sound
201 			//this.audio.pause();
202 			this.audio.currentTime = 0;
203 		}
204 		try{
205 			this.audio.play();
206 		}catch(e){
207 			if(window.console){
208 				window.console.log("audio.play() failed: " + e);
209 			}
210 		}
211 	},
212 	__lastentry: undefined
213 }
214 
215 
216 
217 /*----------------------------------------------------------------------------*/
218 
219 
220 var ArcadeJS = Class.extend(
221 /** @lends ArcadeJS.prototype */
222 {
223 	/**
224 	 * A canvas based 2d game engine.
225 	 * @param {canvas|string} canvas Canvas element or element id
226 	 * @param {object} opts Game configuration
227 	 * @param {string} [opts.name]
228 	 * @param {int} [opts.fps=30] 
229 	 * @param {string} [opts.resizeMode='adjust'] Adjust internal canvas width/height to match its outer dimensions
230 	 * @param {boolean} [opts.fullscreenMode=false] Resize canvas to window extensions
231 	 * @param {object} [opts.fullscreenMargin={top: 0, right: 0, bottom: 0, left: 0}]
232 	 * @param {boolean} [opts.timeCorrection=true] Adjust object velocities for constant speed when frame rate drops.
233 	 * @param {object} opts.debug Additional debug settings
234 	 * 
235 	 * @constructs
236 	 */
237 	init: function(canvas, opts) {
238 		/**Game options (ArcadeJS.defaultGameOptions + options passed to the constructor).*/
239 		this.opts = $.extend(true, {}, ArcadeJS.defaultGameOptions, opts);
240 		// TODO: required?
241 		this.opts.debug = $.extend({}, ArcadeJS.defaultGameOptions.debug, opts.debug);
242 		// Copy selected options as object attributes
243 		ArcadeJS.extendAttributes(this, this.opts,
244 			"name fps resizeMode fullscreenMode fullscreenMargin timeCorrection");
245 
246 		this._logBuffer = [];
247 		/**HTML5 canvas element*/
248 		if(typeof(canvas) == "string"){
249 			if(canvas[0] == "#") {
250 				canvas = canvas.substr(1);
251 			}
252 			canvas = document.getElementById(canvas);
253 		}
254 		if(!canvas || !canvas.getContext){
255 			throw "Invalid canvas (expected Canvas element or element ID)";
256 		}
257 		/**The augmented HTML <canvas> element. 
258 		 * @See CanvasObject */
259 		this.canvas = canvas;
260 		/**The 2D Rendering Context*/
261 		this.context = canvas.getContext("2d");
262 		$.extend(this.context, ArcadeCanvas);
263 
264 		var $canvas = $(this.canvas);
265 		$canvas.css("backgroundColor", this.opts.backgroundColor);
266 
267 		// Adjust canvas height and width (if specified as %, it would default to 300x150)
268 		this.canvas.width = $canvas.width();
269 		this.canvas.height = $canvas.height();
270 		this.context.strokeStyle = this.opts.strokeStyle;
271 		this.context.fillStyle = this.opts.fillStyle;
272 
273 		/** Usable canvas area (defaults to full extent) */
274 		this.canvasArea = null;
275 		this.resetCanvasArea();
276 
277 		/** Requested viewport */
278 		this.viewportOrg = null;
279 		/** Realized viewport (adjusted according to map mode).*/
280 		this.viewport = null;
281 		/** Viewport map mode ('none', 'extend', 'trim', 'stretch').*/
282 		this.viewportMapMode = "none";
283 		this.debug("game.init()");
284 		this._realizeViewport();
285 
286 		this.objects = [];
287 		this.canvasObjects = [];
288 		this.idMap = {};
289 		this.keyListeners = [ this ];
290 		this.mouseListeners = [ this ];
291 		this.touchListeners = [ this ];
292 		this.activityListeners = [ this ];
293 		this.dragListeners = [];
294 		this._draggedObjects = [];
295 		this.typeMap = {};
296 //		this.downKeyCodes = [];
297 		this._activity = "idle";
298 
299 		/**Current time in ticks*/
300 		this.time = new Date().getTime();
301 		/**Total number of frames rendered so far.*/
302 		this.frameCount = 0;
303 		/**Time elapsed since previous frame in seconds.*/
304 		this.frameDuration = 0;
305 		/**Frames per second rate that was achieved recently.*/
306 		this.realFps = 0;
307 		this._sampleTime = this.time;
308 		this._sampleFrameCount = 0;
309 		/**Correction factor that will assure constant screen speed when FpS drops.*/
310 		this.fpsCorrection = 1.0;
311 		this._lastSecondTicks = 0;
312 		this._lastFrameTicks = 0;
313 		/**Temporary dictionary to store data during one render loop step.*/
314 		this.frameCache = {};
315 		this._deadCount = 0;
316 
317 		this._runLoopId = null;
318 		this.stopRequest = false;
319 		this.freezeMode = false;
320 		this._timeout = null;
321 //		this._timoutCallback = null;
322 
323 		/**True if the left mouse button is down. */
324 		this.leftButtonDown = undefined;
325 		/**True if the middle mouse button is down. */
326 		this.middleButtonDown = undefined;
327 		/**True if then right mouse button is down. */
328 		this.rightButtonDown = undefined;
329 		/**Current mouse position in World Coordinates. */
330 		this.mousePos = undefined;
331 		/**Current mouse position in Canvas Coordinates. */
332 		this.mousePosCC = undefined;
333 		/**Position of last mouse click in World Coordinates. */
334 		this.clickPos = undefined;
335 		/**Position of last mouse click in Canvas Coordinates. */
336 		this.clickPosCC = undefined;
337 		/**Distance between clickPos and mousePos while dragging in World Coordinates. */
338 		this.dragOffset = undefined;
339 		/**Distance between clickPos and mousePos while dragging in Canvas Coordinates. */
340 		this.dragOffsetCC = undefined;
341 		/**e.touches recorded at last touch event.*/
342 		this.touches = undefined;
343 
344 		this.keyCode = undefined;
345 		this.key = undefined;
346 		this.downKeyCodes = [];
347 
348 		if(!ArcadeJS._firstGame) {
349 			ArcadeJS._firstGame = this;
350 		}
351 		// Bind keyboard events
352 		var self = this;
353 		$(document).bind("keyup keydown keypress", function(e){
354 			if( e.type === "keyup"){
355 				self.keyCode = null;
356 				self.key = null;
357 				var idx = self.downKeyCodes.indexOf(e.keyCode);
358 				if(idx >= 0){
359 					self.downKeyCodes.splice(idx, 1);
360 				}
361 //            	self.debug("Keyup %s: %o", ArcadeJS.keyCodeToString(e), self.downKeyCodes);
362 			} else if( e.type === "keydown"){
363 				self.keyCode = e.keyCode;
364 				self.key = ArcadeJS.keyCodeToString(e);
365 				if( self.downKeyCodes.indexOf(self.keyCode) < 0){
366 					self.downKeyCodes.push(self.keyCode);
367 				}
368 //            	self.debug("Keydown %s: %o", self.key, self.downKeyCodes);
369 			} else { // keypress
370 //            	self.debug("Keypress %s: %o", self.key, e);
371 				// Ctrl+Shift+D toggles debug mode
372 				if(self.key == "ctrl+meta+D"){
373 					self.setDebug(!self.opts.debug.showActivity);
374 				}
375 			}
376 			for(var i=0; i<self.keyListeners.length; i++) {
377 				var obj = self.keyListeners[i];
378 				if(e.type == "keypress" && obj.onKeypress) {
379 					obj.onKeypress(e);
380 				} else if(e.type == "keyup" && obj.onKeyup) {
381 					obj.onKeyup(e, self.key);
382 				} else if(e.type == "keydown" && obj.onKeydown) {
383 					obj.onKeydown(e, self.key);
384 				}
385 			}
386 		});
387 		// Prevent context menu on right clicks
388 		$(document).bind("contextmenu", function(e){
389 			return false;
390 		});
391 		// Bind mouse events
392 		// Note: jquery.mousehweel.js plugin is required for Mousewheel events
393 		$(document).bind("mousemove mousedown mouseup mousewheel", function(e){
394 			// Mouse position in canvas coordinates
395 //        	self.mousePos = new Point2(e.clientX-e.target.offsetLeft, e.clientY-e.target.offsetTop);
396 			self.mousePosCC = new Point2(e.pageX - self.canvas.offsetLeft,
397 					e.pageY - self.canvas.offsetTop);
398 			self.mousePos = Point2.transform(self.mousePosCC, self.cc2wc);
399 //    		self.debug("%s: %s (%s)", e.type, self.mousePos, self.mousePosCC);
400 			var startDrag = false,
401 				drop = false,
402 				cancelDrag = false;
403 			switch (e.type) {
404 			case "mousedown":
405 				self.clickPosCC = self.mousePosCC.copy();
406 				self.clickPos = self.mousePos.copy();
407 				switch(e.which){
408 				case 1: self.leftButtonDown = true; break;
409 				case 2: self.middleButtonDown = true; break;
410 				case 3: self.rightButtonDown = true; break;
411 				}
412 				cancelDrag = !!self._dragging;
413 				self._dragging = false;
414 				break;
415 			case "mouseup":
416 				self.clickPosCC = self.clickPos = null;
417 				switch(e.which){
418 				case 1: self.leftButtonDown = false; break;
419 				case 2: self.middleButtonDown = false; break;
420 				case 3: self.rightButtonDown = false; break;
421 				}
422 				drop = !!self._dragging;
423 				self._dragging = false;
424 				break;
425 			case "mousemove":
426 //	    		self.debug("%s: %s (%s) - %s", e.type, self.clickPosCC, self.mousePosCC, self.clickPosCC.distanceTo(self.mousePosCC));
427 				if(self._dragging || self.clickPosCC && self.clickPosCC.distanceTo(self.mousePosCC) > 4 ){
428 					startDrag = !self._dragging;
429 					self._dragging = true;
430 					self.dragOffsetCC = self.clickPosCC.vectorTo(self.mousePosCC);
431 					self.dragOffset = self.clickPos.vectorTo(self.mousePos);
432 //	        		self.debug("dragging: %s (%s)", self.dragOffset, self.dragOffsetCC);
433 				} else {
434 					self.dragOffsetCC = self.dragOffset = null;
435 				}
436 				break;
437 			}
438 			for(var i=0, l=self.mouseListeners.length; i<l; i++) {
439 				var obj = self.mouseListeners[i];
440 				if(e.type == "mousemove" && obj.onMousemove) {
441 					obj.onMousemove(arguments[0]);
442 				} else if(e.type == "mousedown" && obj.onMousedown) {
443 					obj.onMousedown(arguments[0]);
444 				} else if(e.type == "mouseup" && obj.onMouseup) {
445 					obj.onMouseup(arguments[0]);
446 				} else if(e.type == "mousewheel" && obj.onMousewheel) {
447 					obj.onMousewheel(arguments[0], arguments[1]);
448 				}
449 			}
450 			if(startDrag){
451 				self._draggedObjects = [];
452 				for(var i=0; i<self.dragListeners.length; i++) {
453 					var obj = self.dragListeners[i];
454 					if(obj.useCC){
455 						if( obj.containsCC(self.clickPosCC) && obj.onDragstart(self.clickPosCC) === true ) {
456 							self._draggedObjects.push(obj);
457 						}
458 					}else{
459 						if( obj.contains(self.clickPos) && obj.onDragstart(self.clickPos) === true ) {
460 							self._draggedObjects.push(obj);
461 						}
462 					}
463 				}
464 			}else{
465 				for(var i=0; i<self._draggedObjects.length; i++) {
466 					var obj = self._draggedObjects[i],
467 						do2 = obj.useCC ? self.dragOffsetCC : self.dragOffset;
468 					if(drop && obj.onDrop) {
469 						obj.onDrop(do2);
470 					} else if(cancelDrag && obj.onDragcancel) {
471 						obj.onDragcancel(do2);
472 					} else if(self._dragging && e.type == "mousemove" && obj.onDrag) {
473 						obj.onDrag(do2);
474 					}
475 				}
476 //            	if(drop || cancelDrag)
477 //            		self._draggedObjects = [];
478 			}
479 		});
480 		// Bind touch and gesture events
481 		$(canvas).bind("touchstart touchend touchmove touchcancel gesturestart gestureend gesturechange", function(e){
482 			self.touches = e.originalEvent.touches;
483 //			self.touches = e.originalEvent.targetTouches;
484 			self.debug("game got " + e.type + ": " + e.target.nodeName + ", touches.length=" + self.touches.length);
485 			self.debug("    orgtarget" + e.originalEvent.target.nodeName);
486 			// Prevent default handling (i.e. don't scroll or dselect the canvas)
487 			// Standard <a> handling is OK
488 //			if(e.target.nodeName != "A"){
489 				e.originalEvent.preventDefault();
490 //			}
491 
492 			for(var i=0, l=self.touchListeners.length; i<l; i++) {
493 				var obj = self.touchListeners[i];
494 				if(obj.onTouchevent) {
495 					obj.onTouchevent(e, e.originalEvent);
496 				}
497 			}
498 		});
499 		// Adjust canvas height and width on resize events
500 		$(window).resize(function(e){
501 			var $c = $(self.canvas),
502 				width = $c.width(),
503 				height = $c.height();
504 			self.debug("window.resize: $canvas: " + width + " x " + height + "px");
505 			if(self.fullscreenMode){
506 				var pad = self.fullscreenMargin;
507 				height =  $(window).height() - (pad.top + pad.bottom);
508 				width =  $(window).width() - (pad.left + pad.right);
509 				$c.css("position", "absolute")
510 				  .css("top", pad.top)
511 				  .css("left", pad.left);
512 			}
513 			// Call onResize() and let user prevent default processing
514 			if(!self.onResize || self.onResize(width, height, e) !== false) {
515 				switch(self.resizeMode) {
516 				case "adjust":
517 					var hasChanged = false;
518 					if(self.canvas.width != width){
519 						self.debug("adjsting canvas.width from " + self.canvas.width + " to " + width);
520 						self.canvas.width = width;
521 						hasChanged = true;
522 					}
523 					if(self.canvas.height != height){
524 						self.debug("adjsting canvas.height from " + self.canvas.height + " to " + height);
525 						self.canvas.height = height;
526 						hasChanged = true;
527 					}
528 					// Adjust WC-to-CC transformation
529 					if(hasChanged){
530 						self._realizeViewport();
531 					}
532 					break;
533 				default:
534 					// Keep current coordinate range and zoom/shrink output(default 300x150)
535 				}
536 				// Resizing resets the canvas context(?)
537 				self.context.strokeStyle = self.opts.strokeStyle;
538 				self.context.fillStyle = self.opts.fillStyle;
539 				// Let canvas objects adjust their positions
540 				var ol = self.canvasObjects;
541 				for(var i=0, l=ol.length; i<l; i++){
542 					var o = ol[i];
543 					if( !o._dead && o.onResize ) {
544 						o.onResize(width, height, e);
545 					}
546 				}
547 				// Trigger afterResize callback
548 				self.afterResize && self.afterResize(e);
549 			}
550 		});
551 		// Trigger first resize event on page load
552 		self.debug("Trigger canvas.resize on init");
553 		$(window).resize();
554 	},
555 	toString: function() {
556 		return "ArcadeJS<" + this.name + ">";
557 	},
558 	/**Enable debug output
559 	 *
560 	 */
561 	setDebug: function(flag){
562 		flag = !!flag;
563 		var d = this.opts.debug;
564 		d.showActivity = d.showKeys = d.showObjects = d.showMouse
565 		= d.showVelocity = d.showBCircle = flag;
566 	},
567 	/**Output string to console.
568 	 * @param: {string} msg
569 	 */
570 	debug: function(msg) {
571 		if(this.opts.debug.logToCanvas){
572 			// Optionally store recent x lines in a string list
573 			var maxLines = this.opts.debug.logBufferLength;
574 			while( this._logBuffer.length > maxLines ){
575 				this._logBuffer.shift();
576 			}
577 			var dt = new Date(),
578 				tag = "" + dt.getHours() + ":" + dt.getMinutes() + "." + dt.getMilliseconds(),
579 				s = tag + " - " + Array.prototype.join.apply(arguments, [", "]);
580 			this._logBuffer.push(s);
581 		}
582 		if(window.console && window.console.log) {
583 //        	var args = Array.prototype.slice.apply(arguments, [1]);
584 //			Function.prototype.call.bind(console.log, console);
585 			try{
586 				// works on Firefox, Safari and Chrome and supports '%o', etc.
587 				window.console.log.apply(window.console, arguments);
588 			}catch(e){
589 				// works with IE as well, but can't make use of '%o' formatters
590 				window.console.log(Array.prototype.join.call(arguments));
591 			}
592 		}
593 	},
594 	/**Return current activity.
595 	 * @returns {string}
596 	 */
597 	getActivity: function() {
598 		return this._activity;
599 	},
600 	/**Set current activity and trigger onSetActivity events.
601 	 * @param {string} activity
602 	 * @returns {string} previous activity
603 	 */
604 	setActivity: function(activity) {
605 		var prev = this._activity;
606 		this._activity = activity;
607 		for(var i=0, l=this.activityListeners.length; i<l; i++) {
608 			var obj = this.activityListeners[i];
609 			if(obj.onSetActivity){
610 				obj.onSetActivity(this, activity, prev);
611 			}
612 		}
613 		return prev;
614 	},
615 	/**Return true, if current activity is in the list.
616 	 * @param {string | string array} activities
617 	 * @returns {boolean}
618 	 */
619 	isActivity: function(activities) {
620 //		if(typeof activities == "string"){
621 //			activities = activities.replace(",", " ").split(" ");
622 //		}
623 		activities = ArcadeJS.explode(activities);
624 		for(var i=0, l=activities.length; i<l; i++) {
625 			if(activities[i] == this._activity){
626 				return true;
627 			}
628 		}
629 		return false;
630 	},
631 	/**Schedule a callback to be triggered after a number of seconds.
632 	 * @param {float} seconds delay until callback is triggered
633 	 * @param {function} [callback=this.onTimout] function to be called
634 	 * @param {Misc} [data] Additional data passed to callback
635 	 */
636 	later: function(seconds, callback, data) {
637 		var timeout = {
638 			id: ArcadeJS._nextTimeoutId++,
639 			time: new Date().getTime() + 1000 * seconds,
640 			frame: this.fps * seconds,
641 			callback: callback || this.onTimeout,
642 			data: (data === undefined ? null : data)
643 		};
644 		// TODO: append to a sorted list instead
645 		this._timeout = timeout;
646 		return timeout.id;
647 	},
648 	/**Define the usable part of the canvas.
649 	 *
650 	 * If set, the viewport is projected into this region.
651 	 * This method should be called on startup and onResize.
652 	 *
653 	 * @param {float} x upper left corner in canvas coordinates
654 	 * @param {float} y upper left corner in canvas coordinates
655 	 * @param {float} width in canvas coordinates
656 	 * @param {float} height in canvas coordinates
657 	 * @param {boolean} clip prevent drawing outside this area (default: true)
658 	 */
659 	setCanvasArea: function(x, y, width, height, clip) {
660 		clip = (clip !== false);
661 		this.canvasArea = {x: x, y: y,
662 				width: width, height: height,
663 				clip: !!clip};
664 		this._customCanvasArea = true;
665 		this.debug("setCanvasArea: %o", this.canvasArea);
666 		this._realizeViewport();
667 	},
668 	/**Reset the usable part of the canvas to full extent.
669 	 */
670 	resetCanvasArea: function() {
671 		var $canvas = $(this.canvas);
672 		this.canvasArea = {x: 0, y: 0,
673 			width: $canvas.width(), height: $canvas.height(),
674 //			width: this.canvas.width, height: this.canvas.height,
675 			clip: false};
676 		this.debug("resetCanvasArea: %o", this.canvasArea);
677 		this._customCanvasArea = false;
678 	},
679 
680 	/**Define the visible part of the world.
681 	 * @param {float} x lower left corner in world coordinates
682 	 * @param {float} y lower left corner in world coordinates
683 	 * @param {float} width in world coordinate units
684 	 * @param {float} height in world coordinate units
685 	 * @param {string} mapMode 'stretch' | 'fit' | 'extend' | 'trim' | 'none'
686 	 */
687 	setViewport: function(x, y, width, height, mapMode) {
688 		this.viewportMapMode = mapMode || "extend";
689 		this.viewportOrg = {x: x, y: y, width: width, height: height};
690 		this.debug("setViewport('" + mapMode + "')");
691 		this._realizeViewport();
692 	},
693 
694 	_realizeViewport: function() {
695 		// Recalc usable canvas size. (In case of custom areas the user has to
696 		// do this in the onResize event.)
697 		if(this._customCanvasArea === false){
698 			this.resetCanvasArea();
699 		}
700 		var mapMode = this.viewportMapMode,
701 			ccWidth = this.canvasArea.width,
702 			ccHeight = this.canvasArea.height,
703 			ccAspect = ccWidth / ccHeight;
704 
705 		this.debug("_realizeViewport('" + mapMode + "') for canvas " + ccWidth + " x " + ccHeight + " px");
706 		if(mapMode == "none"){
707 //			this.viewport = {x: 0, y: 0, width: ccWidth, height: ccHeight};
708 			this.viewport = {x: 0, y: ccHeight, width: ccWidth, height: -ccHeight};
709 			this.viewportOrg = this.viewport;
710 			this.wc2cc = new Matrix3();
711 			this.cc2wc = new Matrix3();
712 			this.onePixelWC = 1;
713 			return;
714 
715 		}
716 		// Calculate the adjusted viewport dimensions
717 		var vp = this.viewportOrg,
718 			vpa = {x: vp.x, y: vp.y, width: vp.width, height: vp.height},
719 			vpAspect = vp.width / vp.height;
720 		this.viewport = vpa;
721 
722 		this.debug("    viewportOrg:  ", vp.x, vp.y, vp.width, vp.height, mapMode);
723 
724 		switch(mapMode){
725 		case "fit":
726 		case "extend":
727 			if(vpAspect > ccAspect){
728 				// Increase viewport height
729 				vpa.height = vp.width / ccAspect;
730 				vpa.y -= 0.5 * (vpa.height - vp.height);
731 			}else{
732 				// Increase viewport width
733 				vpa.width = vp.height * ccAspect;
734 				vpa.x -= 0.5 * (vpa.width - vp.width);
735 			}
736 			break;
737 		case "trim":
738 			if(vpAspect > ccAspect){
739 				// Decrease viewport width
740 				vpa.width = vp.height * ccAspect;
741 				vpa.x -= 0.5 * (vpa.width - vp.width);
742 			}else{
743 				// Decrease viewport height
744 				vpa.height = vp.width / ccAspect;
745 				vpa.y -= 0.5 * (vpa.height - vp.height);
746 			}
747 			break;
748 		case "stretch":
749 			break;
750 		default:
751 			throw "Invalid mapMode: '" + vp.mapMode + "'";
752 		}
753 		this.debug("    viewport adjusted ", vpa.x, vpa.y, vpa.width, vpa.height);
754 		// Define transformation matrices
755 		this.wc2cc = new Matrix3()
756 			.translate(-vpa.x, -vpa.y)
757 			.scale(ccWidth/vpa.width, -ccHeight/vpa.height)
758 			.translate(0, ccHeight)
759 			.translate(this.canvasArea.x, this.canvasArea.y)
760 			;
761 //		this.debug("wc2cc: %s", this.wc2cc);
762 		this.cc2wc = this.wc2cc.copy().invert();
763 		this.onePixelWC = vpa.width / ccWidth;
764 //		this.debug("cc2wc: %s", this.cc2wc);
765 	},
766 
767 	_renderLoop: function(){
768 //        try {
769 //        	p.focused = document.hasFocus();
770 //		} catch(e) {}
771 		// Fire timeout event, if one was scheduled
772 		var timeout = this._timeout;
773 		if(timeout &&
774 			((this.timeCorrection && this.time >= timeout.time)
775 			 || (!this.timeCorrection && this.frameCount >= timeout.frame))
776 			 ){
777 			this._timeout = null;
778 			this.debug(this.toString() + " timeout " + timeout);
779 			timeout.callback.call(this, timeout.data);
780 		}
781 //		if( this._timeout > 0) {
782 //			this._timeout--;
783 //			if( this._timeout === 0) {
784 //		    	var callback = this._timeoutCallback || self.onTimeout;
785 //				callback.call(this);
786 //			}
787 //		}
788 		try {
789 			this.frameCache = {collisionCache: {}};
790 			this._stepAll();
791 			this._redrawAll();
792 			if( this.stopRequest ){
793 				this.stopLoop();
794 				this.stopRequest = false;
795 			}
796 		} catch(e) {
797 		   this.stopLoop();
798 		   this.debug("Exception in render loop: %o", e);
799 		   throw e;
800 		}
801 	},
802 	_stepAll: function() {
803 		// Some bookkeeping and timings
804 		var ticks = new Date().getTime(),
805 			sampleDuration = .001 * (ticks - this._sampleTime);
806 
807 		if(this.timeCorrection){
808 			this.frameDuration = .001 * (ticks - this.time);
809 		}else{
810 			this.frameDuration = 1.0 / this.fps;
811 		}
812 		this.time = ticks;
813 		this.frameCount++;
814 //		this.debug("Frame #%s, frameDuration=%s, realFps=%s", this.frameCount, this.frameDuration, this.realFps);
815 		// Update number of actually achieved FPS ever second
816 		if(sampleDuration >= 1.0){
817 			this.realFps = (this.frameCount - this._sampleFrameCount) / sampleDuration;
818 			this._sampleTime = ticks;
819 			this._sampleFrameCount = this.frameCount;
820 		}
821 		if(this.freezeMode){
822 			return;
823 		}
824 		if(this.preStep){
825 			this.preStep.call(this);
826 		}
827 		var ol = this.objects;
828 		for(var i=0, l=ol.length; i<l; i++){
829 			var o = ol[i];
830 			if( !o._dead ){
831 				o._step();
832 			}
833 		}
834 		if(this.postStep){
835 			this.postStep.call(this);
836 		}
837 	},
838 	_redrawAll: function() {
839 		var ctx = this.context,
840 			ol, i, o;
841 
842 		ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
843 		// Draw background canvas objects
844 		ol = this.canvasObjects;
845 		for(i=0, l=ol.length; i<l; i++){
846 			o = ol[i];
847 			if( !o._dead && !o.hidden && o.isBackground) {
848 				o._redraw(ctx);
849 			}
850 		}
851 		// Push current transformation and rendering context
852 		ctx.save();
853 		try{
854 			if(this._customCanvasArea){
855 				var cca = this.canvasArea;
856 				if(cca.clip){
857 					ctx.beginPath();
858 					ctx.rect(cca.x, cca.y, cca.width, cca.height);
859 //				    ctx.stroke();
860 					ctx.clip();
861 				}
862 			}
863 			ctx.transformMatrix3(this.wc2cc);
864 			ctx.lineWidth = this.onePixelWC;
865 			if(this.preDraw){
866 				this.preDraw(ctx);
867 			}
868 			// Draw non-canvas objects
869 			ol = this.objects;
870 			for(i=0, l=ol.length; i<l; i++){
871 				o = ol[i];
872 				if( !o._dead && !o.hidden && !o.useCC) {
873 					o._redraw(ctx);
874 				}
875 			}
876 			if(this.postDraw){
877 				this.postDraw(ctx);
878 			}
879 		}finally{
880 			// Restore previous transformation and rendering context
881 			ctx.restore();
882 		}
883 		// Display FpS
884 		if(this.opts.debug.showFps){
885 			ctx.save();
886 			ctx.font = "12px sans-serif";
887 			ctx.fillText(this.realFps.toFixed(1) + " fps", this.canvas.width-50, 15);
888 			ctx.restore();
889 		}
890 		if(this.opts.debug.logToCanvas){
891 			ctx.save();
892 			ctx.font = "12px sans-serif";
893 			var x = 10,
894 				y = this.canvas.height - 15;
895 			for(var i=this._logBuffer.length-1; i>0; i--){
896 				ctx.fillText(this._logBuffer[i], x, y);
897 				y -= 15;
898 			}
899 			ctx.restore();
900 		}
901 		// Draw debug infos
902 		var infoList = [];
903 		if(this.opts.debug.showActivity){
904 			infoList.push("Activity: '" + this._activity + "'");
905 		}
906 		if(this.opts.debug.showKeys){
907 			infoList.push("Keys: [" + this.downKeyCodes + "]");
908 			infoList.push("Mouse: [" + (this.leftButtonDown ? "L" : ".")  + (this.middleButtonDown ? "M" : ".") + (this.rightButtonDown ? "R" : ".") + "]");
909 		}
910 		if(this.opts.debug.showObjects){
911 			infoList.push("Objects: " + this.objects.length + " (dead: "+ this._deadCount+")");
912 		}
913 		if(this.opts.debug.showMouse && this.mousePos){
914 			infoList.push(this.mousePos.toString(4));
915 			infoList.push("CC: " + this.mousePosCC);
916 			var hits = this.getObjectsAtPosition(this.mousePos);
917 			if(hits.length){
918 //				infoList.push("Hits: " + hits);
919 				ctx.font = "12px sans-serif";
920 				ctx.fillStyle = this.opts.debug.strokeStyle;
921 				ctx.fillText(hits, 10, this.canvas.height - 20);
922 			}
923 		}
924 		if(infoList.length){
925 			ctx.save();
926 			ctx.font = "12px sans-serif";
927 			ctx.fillStyle = this.opts.debug.strokeStyle;
928 			ctx.fillText(infoList.join(", "), 10, this.canvas.height - 5);
929 			ctx.restore();
930 		}
931 		// Let derived class draw overlays in CC
932 		if(this.postDrawCC){
933 			this.postDrawCC(ctx);
934 		}
935 		// Draw foreground canvas objects
936 		ol = this.canvasObjects;
937 		for(i=0, l=ol.length; i<l; i++){
938 			o = ol[i];
939 			if( !o._dead && !o.hidden && !o.isBackground) {
940 				o._redraw(ctx);
941 			}
942 		}
943 	},
944 	/**Start render loop.
945 	 */
946 	startLoop: function(){
947 		if( !this._runLoopId) {
948 			this.realFps = 0;
949 			this._sampleTime = new Date().getTime();
950 			this._sampleFrameCount = this.frameCount;
951 			var self = this;
952 			this._runLoopId = window.setInterval(
953 				function(){
954 					self._renderLoop.call(self);
955 				}, 1000/this.fps);
956 		}
957 	},
958 	/**Stop render loop.
959 	 */
960 	stopLoop: function(){
961 		this.stopRequest = false;
962 		if(this._runLoopId) {
963 			window.clearInterval(this._runLoopId);
964 			this._runLoopId = null;
965 		}
966 	},
967 	/**Return true, if render loop is active.
968 	 * @returns Boolean
969 	 */
970 	isRunning: function(){
971 		return !!this._runLoopId;
972 	},
973 	/**Add game object to object list.
974 	 * @param: {Movable} o
975 	 * @returns {Movable}
976 	 */
977 	addObject: function(o) {
978 		if( this.idMap[o.id] ) {
979 			throw "addObject("+o.id+"): duplicate entry";
980 		}
981 		//
982 		o.game = this;
983 
984 		this.purge(false);
985 
986 		this.objects.push(o);
987 		if(o.useCC){
988 			this.canvasObjects.push(o);
989 		}else{
990 //			this.objects.push(o);
991 		}
992 		this.idMap[o.id] = o;
993 
994 		if( typeof o.onKeydown === "function"
995 			|| typeof o.onKeypress === "function"
996 			|| typeof o.onKeyup === "function") {
997 			this.keyListeners.push(o);
998 		}
999 		if( typeof o.onMousedown === "function"
1000 			|| typeof o.onMousemove === "function"
1001 			|| typeof o.onMouseup === "function"
1002 			|| typeof o.onMousewheel === "function") {
1003 			this.mouseListeners.push(o);
1004 		}
1005 		if( typeof o.onTouchevent === "function") {
1006 			this.touchListeners.push(o);
1007 		}
1008 		if( typeof o.onSetActivity === "function") {
1009 			this.activityListeners.push(o);
1010 		}
1011 		if( typeof o.onDragstart === "function") {
1012 			this.dragListeners.push(o);
1013 		}
1014 		if( this.typeMap[o.type] ) {
1015 			this.typeMap[o.type].push(o);
1016 		} else {
1017 			this.typeMap[o.type] = [ o ];
1018 		}
1019 		// Call onResize on startup, so object can initialize o.pos
1020 		if( typeof o.onResize === "function") {
1021 			o.onResize(this.canvas.width, this.canvas.height);
1022 		}
1023 		return o;
1024 	},
1025 	/**Purge dead objects from object list.
1026 	 * @param: {boolean} force false: only if opts.purgeRate is reached.
1027 	 */
1028 	purge: function(force) {
1029 		var ol = this.objects;
1030 		if( this._purging
1031 			|| ol.length < 1
1032 			|| (!force && (this._deadCount/ol.length) < this.opts.purgeRate)
1033 			){
1034 			return false;
1035 		}
1036 		this._purging = true;
1037 		this.debug("Purging objects: " + this._deadCount + "/" + ol.length + " dead.");
1038 		this.objects = [];
1039 		this.keyListeners = [ this ];
1040 		this.mouseListeners = [ this ];
1041 		this.touchListeners = [ this ];
1042 		this.activityListeners = [ this ];
1043 		this.dragListeners = [ ];
1044 		this.idMap = {};
1045 		this.typeMap = {};
1046 		for(var i=0; i<ol.length; i++){
1047 			var o = ol[i];
1048 			if( !o._dead ){
1049 				this.addObject(o);
1050 			}
1051 		}
1052 		this._deadCount = 0;
1053 		this._purging = false;
1054 		return true;
1055 	},
1056 	/**Return objects with a given ID or null.
1057 	 * @param: {string} id
1058 	 */
1059 	getObjectById: function(id) {
1060 		return this.idMap[id] || null;
1061 	},
1062 	/**Return an array of objects with a given type (array may be empty).
1063 	 * @param: {string} type (separate multiple types with a space)
1064 	 */
1065 	getObjectsByType: function(types) {
1066 //		return this.typeMap[type] ? this.typeMap[type] : [];
1067 		types = ArcadeJS.explode(types);
1068 		var res = [];
1069 		for(var i=0; i<types.length; i++){
1070 			var list = this.typeMap[types[i]];
1071 			if(list && list.length) {
1072 				res = res.concat(list);
1073 			}
1074 		}
1075 		return res;
1076 	},
1077 	/**Call func(obj) for all objects.
1078 	 * @param: {function} func callback(game, object)
1079 	 * @param: {string} types Restrict objects to this space separated typenames
1080 	 */
1081 	visitObjects: function(func, types) {
1082 		if(types){
1083 			types = ArcadeJS.explode(types);
1084 		}
1085 		var self = this;
1086 		var __visitList = function(list){
1087 			// don't cache list.length here!! may be recalced in callback
1088 			for(var i=0; i<list.length; i++){
1089 				var obj = list[i];
1090 				if(obj._dead){
1091 					continue;
1092 				}
1093 				var res = func.call(self, obj);
1094 				if(res === false){
1095 					return false;
1096 				}
1097 			}
1098 		};
1099 		if(types && types.length){
1100 			for(var i=0; i<types.length; i++){
1101 				var list = this.typeMap[types[i]];
1102 				if(list && list.length) {
1103 					var res = __visitList(list);
1104 					if(res === false){
1105 						break;
1106 					}
1107 				}
1108 			}
1109 		}else{
1110 			__visitList(this.objects);
1111 		}
1112 	},
1113 	/**Return an array of objects at a given point.
1114 	 * @param: {Point2} pt Position in world coordinates
1115 	 * @param: {string} types Restrict search to this comma separated typenames
1116 	 */
1117 	getObjectsAtPosition: function(pt, types, stopOnFirst) {
1118 		pt = pt || this.mousePos;
1119 		var matches = [];
1120 		this.visitObjects(function(obj){
1121 			if(obj.contains(pt)){
1122 				matches.push(obj);
1123 				if(stopOnFirst){
1124 					return false;
1125 				}
1126 			}
1127 		}, types);
1128 		return matches;
1129 	},
1130 	/**Return true, if a key is currently pressed.
1131 	 * @param: {int} keyCode
1132 	 * @returns {boolean}
1133 	 * @see Movable.onKeypress
1134 	 */
1135 	isKeyDown: function(keyCode) {
1136 		return this.downKeyCodes.indexOf(keyCode) >= 0;
1137 	},
1138 	/**Wide check if object1 and object2 are collision candiates.
1139 	 * @param: {Movable} object1
1140 	 * @param: {Movable} object2
1141 	 * @returns {boolean}
1142 	 */
1143 	preCheckCollision: function(object1, object2) {
1144 		// Objects cannot collide with themselves
1145 		if(object1 === object2) {
1146 			return false;
1147 		} else if(object1.hidden || object2.hidden || object1._dead || object2._dead ) {
1148 			return false;
1149 		}
1150 		var id1 = ""+object1.id,
1151 			id2 = ""+object2.id,
1152 			tag = (id1 < id2) ? id1 + "~" + id2 : id2 + "~" + id1,
1153 			cc = this.frameCache.collisionCache;
1154 		// This pair was already checked
1155 		if( cc[tag] ) {
1156 			return false;
1157 		}
1158 		cc[tag] = true;
1159 		// Check bounding circles if possible
1160 //		if( object1.getBoundingRadius && object2.getBoundingRadius
1161 //			&& object1.pos.distanceTo(object2.pos) > (object1.getBoundingRadius() + object2.getBoundingRadius())) {
1162 //			return false;
1163 //		}
1164 		if( object1.getBoundingCircle && object2.getBoundingCircle ){
1165 			var bs1 = object1.getBoundingCircle(),
1166 				bs2 = object2.getBoundingCircle();
1167 			if( bs1.center.distanceTo(bs2.center) > (bs1.r + bs2.r)) {
1168 				return false;
1169 			}
1170 		}
1171 		// TODO: check if velocities are pointing away from each other
1172 		// Narrow check required
1173 		return true;
1174 	},
1175 	/**Callback, triggered when this.later() timeout expires (and no callback was given).
1176 	 * @param data data object passed to this.later()
1177 	 * @event
1178 	 */
1179 	onTimeout: undefined,
1180 	/**Called when window is resized (and on start).
1181 	 * The default processing depends on the 'resizeMode' option.
1182 	 * @param {Int} width
1183 	 * @param {Int} height
1184 	 * @param {Event} e
1185 	 * @returns false to prevent default handling
1186 	 * @event
1187 	 */
1188 	onResize: undefined,
1189 	/**Called after window was resized.
1190 	 * @param {Event} e
1191 	 * @event
1192 	 */
1193 	afterResize: undefined,
1194 	/**Called on miscelaneous touch and gesture events.
1195 	 * @param {Event} event jQuery event
1196 	 * @param {OriginalEvent} originalEvent depends on mobile device
1197 	 * @event
1198 	 */
1199 	onTouchevent: undefined,
1200 	/**Called before object.step() is called on all game ojects.
1201 	 * @event
1202 	 */
1203 	preStep: undefined,
1204 	/**Called after object.step() was called on all game ojects.
1205 	 * @event
1206 	 */
1207 	postStep: undefined,
1208 	/**Called before object.render() is called on all game ojects.
1209 	 * object.step() calls have been executed and canvas was cleared.
1210 	 * @param ctx Canvas 2D context.
1211 	 * @event
1212 	 */
1213 	preDraw: undefined,
1214 	/**Called after object.render() was called on all game ojects.
1215 	 * @param ctx Canvas 2D context.
1216 	 * @event
1217 	 */
1218 	postDraw: undefined,
1219 	/**Called after all rendering happened and transformations are reset.
1220 	 * Allows accessing the full, untransformed canvas in Canvas Coordinates.
1221 	 * @param ctx Canvas 2D context.
1222 	 * @event
1223 	 */
1224 	postDrawCC: undefined,
1225 	// --- end of class
1226 	__lastentry: undefined
1227 });
1228 
1229 /**Return a string array from a space or comma separated string.
1230  * @param {string} s Space or comma separated string.
1231  */
1232 ArcadeJS.explode = function(s){
1233 	if($.isArray(s)){
1234 		return s;
1235 	}
1236 	return s.replace(",", " ").split(" ");
1237 };
1238 
1239 /**Copy selected dictionary members as object attributes.
1240  * @param {Class} object
1241  * @param dict
1242  * @param {string} attrNames comma seperated attribute names that will be
1243  * shallow-copied from dict to object.
1244  * @throws "Attribute 'x' not found."
1245  */
1246 ArcadeJS.extendAttributes = function(object, dict, attrNames){
1247 	attrNames = ArcadeJS.explode(attrNames);
1248 	for(var i=0; i<attrNames.length; i++){
1249 		var name = $.trim(attrNames[i]);
1250 		if(dict[name] === undefined){
1251 			throw("Attribute '" + name + "' not found in dictionary.");
1252 		}
1253 		object[name] = dict[name];
1254 	}
1255 };
1256 
1257 /**Copy all entries from `opts` as `target` properties, if there is a matching
1258  * entry in `template`.
1259  *
1260  * Note: this is different from `$.extend()`, because only values are copied that
1261  * are present in `template`. This prevents accidently overriding protected
1262  * members in `target`.
1263  * If `temnplate` members have a value of `undefined`, they are mandatory. An
1264  * exception will be raised, if they are not found in `opts`.
1265  *
1266  * @param {object} target Object that will receive the properties (typically `this`).
1267  * @param {object} template Defines names and default values of all members to copy.
1268  * @param {object} opts Object with methods and values that override `template` (typically passed to the target constructor).
1269  */
1270 ArcadeJS.guerrillaDerive = function(target, template, source){
1271 	var missing = [];
1272 	for(var name in template){
1273 		if(source.hasOwnProperty(name)){
1274 			target[name] = source[name];
1275 		}else if(template.hasOwnProperty(name)){
1276 			var val = template[name];
1277 			if(val === undefined){
1278 				missing.unshift(name);
1279 			}else{
1280 				target[name] = val;
1281 			}
1282 		}
1283 	}
1284 	if(missing.length){
1285 		alert("guerrillaDerive: Missing mandatory options '" + missing.join("', '") + "'");
1286 	}
1287 };
1288 
1289 /**Raise error if .
1290  * @param {object} object or dictionary
1291  * @param {string} attrNames comma seperated attribute names that will be
1292  * checked.
1293  * @throws "Attribute 'x' not found."
1294  */
1295 ArcadeJS.assertAttributes = function(object, attrNames){
1296 	attrNames = ArcadeJS.explode(attrNames);
1297 	for(var i=0; i<attrNames.length; i++){
1298 		var name = $.trim(attrNames[i]);
1299 		if(object[name] === undefined){
1300 			throw("Attribute '" + name + "' is undefined.");
1301 		}
1302 	}
1303 	return true;
1304 };
1305 /**Throw error, if expression is `false`.
1306  * @throws "Assert failed: '...'"
1307  */
1308 ArcadeJS.assert = function(expression, errorMsg){
1309 	if( !expression ){
1310 		throw("Assert failed: '" + errorMsg + "'.");
1311 	}
1312 };
1313 /**Global pointer to first created game object.*/
1314 ArcadeJS._firstGame = null;
1315 
1316 /**Used to generate unique object IDs.*/
1317 ArcadeJS._nextObjectId = 1;
1318 
1319 /**Used to generate unique timer IDs.*/
1320 ArcadeJS._nextTimeoutId = 1;
1321 
1322 /**Default options dictionary.*/
1323 ArcadeJS.defaultGameOptions = {
1324 	name: "Generic ArcadeJS application",
1325 	//activity: "idle",
1326 	backgroundColor: "black", // canvas background color
1327 	strokeStyle: "#ffffff", // default line color
1328 	fillStyle: "#c0c0c0", // default solid filll color
1329 	fullscreenMode: false, // Resize canvas to window extensions
1330 	fullscreenMargin: {top: 0, right: 0, bottom: 0, left: 0},
1331 	resizeMode: "adjust", // Adjust internal canvas width/height to match its outer dimensions
1332 	viewport: {x: 0, y: 0, width: 100, height: 100, mapMode: "stretch"},
1333 	fps: 30,
1334 	timeCorrection: true, // Adjust velocites for constant speed when frame rate drops
1335 	debug: {
1336 		level: 1,
1337 		logToCanvas: false,
1338 		logBufferLength: 30,
1339 		strokeStyle: "#80ff00",
1340 		showActivity: false,
1341 		showKeys: false,
1342 		showFps: true,
1343 		showObjects: false,
1344 		showMouse: false,
1345 		// globally override object debug settings:
1346 		showVelocity: undefined,
1347 		showBCircle: undefined
1348 	},
1349 	purgeRate: 0.5,
1350 	_lastEntry: undefined
1351 }
1352 
1353 
1354 /*----------------------------------------------------------------------------*/
1355 
1356 
1357 /**Augmented HTML 5 canvas.
1358  * This functions are added to a ArcadeJS canvas.
1359  * @class
1360  * @augments Canvas
1361  * @see <a href='http://www.w3.org/TR/html5/the-canvas-element.html#the-canvas-element'>The canvas element</a>
1362  * @see <a href='https://developer.mozilla.org/en/canvas_tutorial'>Canvas tutorial</a>
1363  */
1364 ArcadeCanvas =
1365 {
1366 	__drawCircle: function(arg1, arg2, arg3) {
1367 		this.beginPath();
1368 		if(arguments.length === 3){
1369 			this.arc(arg1, arg2, arg3, 0, 2 * Math.PI, true);
1370 		} else if(arguments.length === 2){
1371 			this.arc(arg1.x, arg1.y, arg2, 0, 2 * Math.PI, true);
1372 		} else {
1373 			this.arc(arg1.center.x, arg1.center.y, arg1.r, 0, 2 * Math.PI, true);
1374 		}
1375 	},
1376 	__drawScreenText: function(strokeMode, text, x, y){
1377 		var textWidth, textHeight;
1378 		this.save();
1379 		this.resetTransform();
1380 		if(x === 0){
1381 			textWidth = this.measureText(text).width;
1382 			x = 0.5 * (this.canvas.width - textWidth);
1383 		}else if( x < 0){
1384 			textWidth = this.measureText(text).width;
1385 			x = this.canvas.width - textWidth + x;
1386 		}
1387 		if(y === 0){
1388 			textHeight = this.measureText("M").width;
1389 			y = 0.5 * (this.canvas.height - textHeight);
1390 		}else if(y < 0){
1391 			textHeight = this.measureText("M").width;
1392 			y = this.canvas.height - 1 + y;
1393 		}
1394 		if(strokeMode){
1395 			this.strokeText(text, x, y);
1396 		}else{
1397 			this.fillText(text, x, y);
1398 		}
1399 		this.restore();
1400 	},
1401 	/**Render a circle outline to a canvas.
1402 	 *
1403 	 * @example
1404 	 * strokeCircle2(circle2)
1405 	 * strokeCircle2(point2, radius)
1406 	 * strokeCircle2(center.x, center.y, radius)
1407 	 */
1408 	strokeCircle2: function() {
1409 		this.__drawCircle.apply(this, arguments);
1410 		this.closePath();
1411 		this.stroke();
1412 	},
1413 	/**Render a filled circle to a canvas.
1414 	 * @see strokeCircle2
1415 	 */
1416 	fillCircle2: function() {
1417 		this.__drawCircle.apply(this, arguments);
1418 		this.fill();
1419 	},
1420 	/**Render a Polygon2 outline to a canvas.
1421 	 *
1422 	 * @param {Polygon2} pg
1423 	 * @param {Boolean} closed (optional) default: true
1424 	 */
1425 	strokePolygon2: function(pg, closed){
1426 		var xy = pg.xyList;
1427 		this.beginPath();
1428 		this.moveTo(xy[0], xy[1]);
1429 		for(var i=2; i<xy.length; i+=2){
1430 			this.lineTo(xy[i], xy[i+1]);
1431 		}
1432 		if(closed !== false){
1433 			this.closePath();
1434 		}
1435 		this.stroke();
1436 	},
1437 	/**Render a filled Polygon2 to a canvas.
1438 	 *
1439 	 * @param {Polygon2} pg
1440 	 */
1441 	fillPolygon2: function(pg){
1442 		var xy = pg.xyList;
1443 		this.beginPath();
1444 		this.moveTo(xy[0], xy[1]);
1445 		for(var i=2; i<xy.length; i+=2){
1446 			this.lineTo(xy[i], xy[i+1]);
1447 		}
1448 		this.fill();
1449 	},
1450 	/**Render a vector to the canvas.
1451 	 * @param {Vec2} vec
1452 	 * @param {Point2} origin (optional) default: (0/0)
1453 	 * @param {float} tipSize (optional) default: 5
1454 	 */
1455 	strokeVec2: function(vec, origin, tipSize) {
1456 		origin = origin || new Point2(0, 0);
1457 		tipSize = tipSize || 5;
1458 		this.beginPath();
1459 		this.moveTo(origin.x, origin.y);
1460 		var ptTip = origin.copy().translate(vec);
1461 		var pt = ptTip.copy();
1462 		this.lineTo(pt.x, pt.y);
1463 		this.closePath();
1464 		this.stroke();
1465 		if(vec.isNull()){
1466 			return;
1467 		}
1468 		this.beginPath();
1469 		var v = vec.copy().setLength(-tipSize);
1470 		var vPerp = v.copy().perp().scale(.5);
1471 		pt.translate(v).translate(vPerp);
1472 		this.lineTo(pt.x, pt.y);
1473 		pt.translate(vPerp.scale(-2));
1474 		this.lineTo(pt.x, pt.y);
1475 		this.lineTo(ptTip.x, ptTip.y);
1476 //			this.lineTo(origin.x, origin.y);
1477 		this.closePath();
1478 		this.stroke();
1479 	},
1480 	/**Render a Point2 to the canvas.
1481 	 * @param {Point2} pt
1482 	 * @param {float} size (optional) default: 4
1483 	 */
1484 	strokePoint2: function(){
1485 		if( pt.x ) {
1486 			var size = arguments[1] || 4;
1487 			this.rect(pt.x, pt.y, size, size);
1488 		} else {
1489 			var size = arguments[2] || 4;
1490 			this.rect(arguments[0], arguments[1], size, size);
1491 		}
1492 	},
1493 	/**Set context transformation to identity (so we can use pixel coordinates).
1494 	 */
1495 	resetTransform: function(){
1496 		this.setTransform(1, 0, 0, 1, 0, 0);
1497 	},
1498 	/**Apply transformation matrix.
1499 	 * This method takes care of transposing m, so it fits the canvas
1500 	 * representation. The matrix is treated as affine (last row being [0 0 1]).
1501 	 * @param {Matrix3} m
1502 	 */
1503 	transformMatrix3: function(m){
1504 		m = m.m;
1505 		this.transform(m[0], m[3], m[1], m[4], m[6], m[7]);
1506 	},
1507 	/**Set transformation matrix.
1508 	 * This method takes care of transposing m, so it fits the canvas
1509 	 * representation. The matrix is treated as affine (last row being [0 0 1]).
1510 	 * @param {Matrix3} m
1511 	 */
1512 	setTransformMatrix3: function(m){
1513 		m = m.m;
1514 		this.setTransform(m[0], m[3], m[1], m[4], m[6], m[7]);
1515 	},
1516 	/**Render a text field to the canvas using canvas coordinates.
1517 	 * Negative coordinates will align to opposite borders.
1518 	 * Pass x = 0 or y = 0 for centered output.
1519 	 * @param {string} text
1520 	 * @param {float} x Horizontal position in CC (use negative value to align at right border)
1521 	 * @param {float} y Vertical position in CC (use negative value to align at bottom)
1522 	 */
1523 	strokeScreenText: function(text, x, y){
1524 		this.__drawScreenText(true, text, x, y);
1525 	},
1526 	fillScreenText: function(text, x, y){
1527 		this.__drawScreenText(false, text, x, y);
1528 	},
1529 	__lastentry: undefined
1530 }
1531 
1532 
1533 /**
1534  * Return a nice string for a keyboard event. This function was inspired by
1535  * progressive.js.
1536  *
1537  * @param {Event} e A jQuery event object.
1538  * @returns {string} 'a' for the key 'a', 'A' for Shift+a, '^a' for Ctrl+a,
1539  *          '[shift]' for
1540  */
1541 ArcadeJS.keyCodeToString = function(e) {
1542 	var code = e.keyCode;
1543 	var shift = !!e.shiftKey;
1544 	var key = null;
1545 
1546 	// Map "shift + keyCode" to this code
1547 	var shiftMap = {
1548 		// Numbers
1549 		48: 41, // )
1550 		49: 33, // !
1551 		50: 64, // @
1552 		51: 35, // #
1553 		52: 36, // $
1554 		53: 37, // %
1555 		54: 94, // ^
1556 		55: 38, // &
1557 		56: 42, // *
1558 		57: 40, // (
1559 		// Symbols and their shift-symbols
1560 		107: 43,  // +
1561 		219: 123, // {
1562 		221: 125, // }
1563 		222: 34   // "
1564 	};
1565 	// Coded keys
1566 	var codeMap = {
1567 			188: 44, // ,
1568 			109: 45, // -
1569 			190: 46, // .
1570 			191: 47, // /
1571 			192: 96, // ~
1572 			219: 91, // [
1573 			220: 92, // \
1574 			221: 93, // ]
1575 			222: 39  // '
1576 		};
1577 	var specialMap = {
1578 		8: "backspace",
1579 		9: "tab",
1580 		10: "enter",
1581 		13: "return",
1582 		16: "shift",
1583 		17: "control",
1584 		18: "alt",
1585 		27: "esc",
1586 		37: "left",
1587 		38: "up",
1588 		39: "right",
1589 		40: "down",
1590 		127: "delete"
1591 		};
1592 
1593 	// Letters
1594 	if ( code >= 65 && code <= 90) { // A-Z
1595 		// Keys return ASCII for upcased letters.
1596 		// Convert to downcase if shiftKey is not pressed.
1597 		if ( !shift )
1598 			code = code + 32;
1599 		shift = false;
1600 		key = String.fromCharCode(code);
1601 	} else if (shiftMap[code]) {
1602 		code = shiftMap[code];
1603 		shift = false;
1604 		key = String.fromCharCode(code);
1605 	} else if (codeMap[code]) {
1606 		code = codeMap[code];
1607 		key = String.fromCharCode(code);
1608 	} else if (specialMap[code]) {
1609 		key = specialMap[code];
1610 	} else {
1611 		key = String.fromCharCode(code);
1612 	}
1613 	var prefix = "";
1614 	if(shift && code != 16){
1615 		prefix = "shift+" + prefix;
1616 	}
1617 	if(e.metaKey){
1618 		prefix = "meta+" + prefix;
1619 	}
1620 	if(e.ctrlKey && code != 17){
1621 		prefix = "ctrl+" + prefix;
1622 	}
1623 	if(e.altKey && code != 18){
1624 		prefix = "alt+" + prefix;
1625 	}
1626 	//window.console.log("keyCode:%s -> using %s, '%s'", e.keyCode,  code, prefix + key);
1627 
1628 	return prefix + key;
1629 }
1630 
1631 /* ****************************************************************************/
1632 
1633 var Movable = Class.extend(
1634 /** @lends Movable.prototype */
1635 {
1636 	/**Represents a game object with kinetic properties.
1637 	 * Used as base class for all game objects.
1638 	 *  
1639 	 * @constructs
1640 	 * @param {string} type Instance type identifier used to filter objects.
1641 	 * @param [opts] Additional options.
1642 	 * @param {string} [opts.id=random] Unique instance identifier.
1643 	 * @param {object} [opts.debug] Additional debug options.
1644 	 * @see Movable.defaultOptions
1645 	 */
1646 	init: function(type, opts) {
1647 		/**Type identifier used to filter objects.*/
1648 		this.type = type;
1649 		/**Unique ID (automatically assigned if ommited).*/
1650 		this.id = (opts && opts.id) ? opts.id : "#" + ArcadeJS._nextObjectId++;
1651 		/**Parent ArcadeJS object (set by game.addObject() method).*/
1652 		this.game = undefined;
1653 		/**True, if object is hidden and not tested for collisions*/
1654 		this.hidden = false;
1655 		this._dead = false;
1656 		this._activity = null;
1657 		// Set options
1658 		this.opts = $.extend(true, {}, Movable.defaultOptions, opts);
1659 		// TODO: required?
1660 		if(opts){
1661 			this.opts.debug = $.extend({}, Movable.defaultOptions.debug, opts.debug);
1662 		}
1663 		opts = this.opts;
1664 		// Copy some options as direct attributes
1665 		/**True, if this control uses native Canvas Coords instead of WC viewport.*/
1666 		this.useCC = !!opts.useCC;
1667 		/**Object position in World Coordinates (center of mass).*/
1668 		this.pos = opts.pos ? new Point2(opts.pos) : new Point2(0, 0);
1669 /*
1670 		if(this.useCC){
1671 			if( opts.pos ){ throw("'pos' is not allowed with useCC mode."); }
1672 			if( !opts.posCC ){ throw("Missing required option: 'posCC'."); }
1673 			this.pos = undefined;
1674 			this.posCC = opts.posCC;
1675 		}else{
1676 			if( opts.posCC ){ throw("'pos' is only allowed in useCC mode."); }
1677 			this.pos = opts.pos ? new Point2(opts.pos) : new Point2(0, 0);
1678 			this.posCC = undefined;
1679 		}
1680 */
1681 		/** Object scale.*/
1682 		this.scale = opts.scale ? +opts.scale : 1.0;
1683 		/** Object orientation in radians.*/
1684 		this.orientation = opts.orientation ? +opts.orientation : 0;
1685 		this.mc2wc = null;
1686 		this.wc2mc = null;
1687 		this._updateTransformations();
1688 
1689 		this.mass = opts.mass ? +opts.mass : 1;
1690 		this.velocity = opts.velocity ? new Vec2(opts.velocity) : new Vec2(0, 0);
1691 		this.translationStep = new Vec2(0, 0);
1692 		this.rotationalSpeed = opts.rotationalSpeed || null; //0.0 * LinaJS.DEG_TO_RAD;  // rad / tick
1693 		this.rotationStep = 0;
1694 		/**Defines, what happens when object leaves the viewport to the left or right.
1695 		 * Values: ('none', 'wrap', 'stop', 'bounce')*/
1696 		this.clipModeX = opts.clipModeX || "none";
1697 		/**Defines, what happens when object leaves the viewport to the left or right.
1698 		 * @See Movable#clipModeX
1699 		 */
1700 		this.clipModeY = opts.clipModeY || "none";
1701 		this._timeout = null; //+opts.timeout;
1702 
1703 //        this.tran = new BiTran2();.translate();
1704 	},
1705 	toString: function() {
1706 		var DEGS = String.fromCharCode(176);
1707 		return "Movable<"+this.type+"> '" + this.id + "' @ "
1708 			+ this.pos.toString(4) + " " + (this.orientation * LinaJS.R2D).toFixed(0) + DEGS
1709 			+ " acivity: '" + this._activity + "'";
1710 	},
1711 	/**Return current activity.
1712 	 * @returns {string}
1713 	 */
1714 	getActivity: function() {
1715 		return this._activity;
1716 	},
1717 	/**Set current activity and trigger onSetActivity events.
1718 	 * @param {string} activity
1719 	 * @returns {string} previous activity
1720 	 */
1721 	setActivity: function(activity) {
1722 		var prev = this._activity;
1723 		this._activity = activity;
1724 		for(var i=0; i<this.game.activityListeners.length; i++) {
1725 			var obj = this.game.activityListeners[i];
1726 			if(obj.onSetActivity)
1727 				obj.onSetActivity(this, activity, prev);
1728 		}
1729 		return prev;
1730 	},
1731 	/**Return true, if current activity is in the list.
1732 	 * @param {string | string array} activities (seperate multiple entries with space)
1733 	 * @returns {boolean}
1734 	 */
1735 	isActivity: function(activities) {
1736 //		if(typeof activities == "string"){
1737 //			activities = activities.replace(",", " ").split(" ");
1738 //		}
1739 		activities = ArcadeJS.explode(activities);
1740 		for(var i=0, l=activities.length; i<l; i++) {
1741 			if(activities[i] == this._activity){
1742 				return true;
1743 			}
1744 		}
1745 		return false;
1746 	},
1747 	/**Schedule a callback to be triggered after a number of seconds.
1748 	 * @param {float} seconds delay until callback is triggered
1749 	 * @param {function} [callback=this.onTimeout] Function to be called.
1750 	 * @param {Misc} [data] Additional data passed to callback
1751 	 */
1752 	later: function(seconds, callback, data) {
1753 		var timeout = {
1754 			id: ArcadeJS._nextTimeoutId++,
1755 			time: new Date().getTime() + 1000 * seconds,
1756 			// if later() is called in the constructor, 'game' may not be set
1757 			frame: (this.game ? this.game.fps : ArcadeJS._firstGame.fps) * seconds,
1758 			callback: callback || this.onTimeout,
1759 			data: (data === undefined ? null : data)
1760 		};
1761 		// TODO: append to a sorted list instead
1762 		this._timeout = timeout;
1763 		return timeout.id;
1764 	},
1765 
1766 	/**Set MC-to-WC transformation matrix and inverse from this.pos, .orientation and .scale.
1767 	 */
1768 	_updateTransformations: function() {
1769 		// TODO: Use negative orientation??
1770 		// Otherwise ctx.tran_redraw
1771 //		this.mc2wc = new Matrix3().scale(this.scale).rotate(this.orientation).translate(this.pos.x, this.pos.y);
1772 		this.mc2wc = new Matrix3().scale(this.scale).rotate(-this.orientation).translate(this.pos.x, this.pos.y);
1773 		this.wc2mc = this.mc2wc.copy().invert();
1774 	},
1775 
1776 	/**
1777 	 *
1778 	 */
1779 	_step: function() {
1780 		// Fire timeout event, if one was scheduled
1781 		var timeout = this._timeout;
1782 		if(timeout &&
1783 			((this.game.timeCorrection && this.game.time >= timeout.time)
1784 			 || (!this.game.timeCorrection && this.game.frameCount >= timeout.frame))
1785 			 ){
1786 			this._timeout = null;
1787 			this.game.debug(this.toString() + " timeout " + timeout);
1788 			timeout.callback.call(this, timeout.data);
1789 		}
1790 		// Kill this instance and fire 'die' event, if time-to-live has expired
1791 //		if( this.ttl > 0) {
1792 //			this.ttl--;
1793 //			if( this.ttl === 0) {
1794 //				this.die();
1795 //			}
1796 //		}
1797 		// Save previous values
1798 		this.prevPos = this.pos.copy();
1799 		this.prevOrientation = this.orientation;
1800 		this.prevVelocity = this.velocity.copy();
1801 		this.prevRotationalSpeed = this.rotationalSpeed;
1802 		// Update position in world coordinates
1803 		var factor = this.game.frameDuration;
1804 		this.translationStep = this.velocity.copy().scale(factor);
1805 		this.rotationStep = factor * this.rotationalSpeed;
1806 		this.orientation += this.rotationStep;
1807 		if(this.velocity && !this.velocity.isNull()) {
1808 			this.pos.translate(this.translationStep);
1809 			// wrap around at screen borders
1810 			var viewport = this.game.viewport;
1811 			switch(this.clipModeX){
1812 			case "wrap":
1813 				this.pos.x = (Math.abs(viewport.width) + this.pos.x) % viewport.width;
1814 				break;
1815 			case "stop":
1816 				this.pos.x = LinaJS.clamp(this.pos.x, viewport.x, viewport.x + viewport.width);
1817 				break;
1818 			case "die":
1819 				if(this.pos.x < viewport.x || this.pos.x > viewport.x + viewport.width){
1820 					this.die();
1821 				}
1822 				break;
1823 			}
1824 			switch(this.clipModeY){
1825 			case "wrap":
1826 				this.pos.y = (Math.abs(viewport.height) + this.pos.y) % viewport.height;
1827 				break;
1828 			case "stop":
1829 				this.pos.y = LinaJS.clamp(this.pos.y, viewport.y, viewport.y + viewport.height);
1830 				break;
1831 			case "die":
1832 				if(this.pos.y < viewport.y || this.pos.y > viewport.y + viewport.height){
1833 					this.die();
1834 				}
1835 				break;
1836 			}
1837 		}
1838 		// Update MC-to-WC transformation
1839 		this._updateTransformations();
1840 		// Let derived class change it
1841 		if(typeof this.step == "function"){
1842 			this.step();
1843 		}
1844 	},
1845 	_redraw: function(ctx) {
1846 		if( this.hidden ) {
1847 			return;
1848 		}
1849 		// Push current transformation and rendering context
1850 		ctx.save();
1851 		try{
1852 			// Render optional debug infos
1853 			ctx.save();
1854 			if(this.getBoundingCircle && (this.opts.debug.showBCircle || this.game.opts.debug.showBCircle)){
1855 				ctx.strokeStyle = this.game.opts.debug.strokeStyle;
1856 				ctx.strokeCircle2(this.getBoundingCircle());
1857 			}
1858 			if(this.velocity && !this.velocity.isNull() && (this.opts.debug.showVelocity || this.game.opts.debug.showVelocity)){
1859 				ctx.strokeStyle = this.game.opts.debug.strokeStyle;
1860 				var v = Vec2.scale(this.velocity, this.opts.debug.velocityScale);
1861 				ctx.strokeVec2(v, this.pos, 5 * this.game.onePixelWC);
1862 			}
1863 			ctx.restore();
1864 			// Apply object translation, rotation and scale
1865 			// TODO: this currently works, but only if we apply a *negative*
1866 			// orientation in _updateTransformations():
1867 			//   this.mc2wc = new Matrix3().scale(this.scale).rotate(-this.orientation).translate(this.pos.x, this.pos.y);
1868 			ctx.transformMatrix3(this.mc2wc);
1869 			// This also works (note that we apply a positive orientaion here,
1870 			// and the order is differnt from mc2wc:
1871 /*
1872 			ctx.translate(this.pos.x, this.pos.y);
1873 			if( this.scale && this.scale != 1.0 ){
1874 				ctx.scale(this.scale, this.scale);
1875 			}
1876 			if( this.orientation ){
1877 				ctx.rotate(this.orientation);
1878 			}
1879 */
1880 			// Let object render itself in its own modelling coordinates
1881 			this.render(ctx);
1882 		}finally{
1883 			// Restore previous transformation and rendering context
1884 			ctx.restore();
1885 		}
1886 	},
1887 	/**@function Return bounding circle for fast a-priory collision checking.
1888 	 * @returns {Circle2} bounding circle in world coordinates.
1889 	 * in modelling coordinates.
1890 	 */
1891 	getBoundingCircle: undefined,
1892 	/**@function Return bounding box for fast a-priory collision checking.
1893 	 * in modelling coordinates.
1894 	 */
1895 	getBoundingBox: undefined,
1896 	/**Remove this object from the game.
1897 	 */
1898 	die: function() {
1899 		if( this._dead ){
1900 			return;
1901 		}
1902 		this._dead = true;
1903 		this.hidden = true;
1904 		if( this.onDie ){
1905 			this.onDie();
1906 		}
1907 		if(this._dead){
1908 			this.game._deadCount++;
1909 			if(!this.game.purge(false)){
1910 				// If we did not purge, make sure it is at least removed from the type map
1911 				var typeMap = this.game.typeMap[this.type];
1912 				var idx = typeMap.indexOf(this);
1913 				typeMap.splice(idx, 1);
1914 			}
1915 		}
1916 	},
1917 	isDead: function() {
1918 		return !!this._dead;
1919 	},
1920 	/**Return true, if point hits this object.
1921 	 * @param {Point2} pt Point in world coordinates
1922 	 * @returns {boolean}
1923 	 */
1924 	contains: function(pt) {
1925 		if(this.getBoundingCircle) {
1926 			var boundsWC = this.getBoundingCircle();//.copy().transform(this.mc2wc);
1927 			return boundsWC.center.distanceTo(pt) <= boundsWC.r;
1928 		}
1929 		return undefined;
1930 	},
1931 	/**Return true, if object intersects with this object.
1932 	 * @param {Movable} otherObject
1933 	 * @returns {boolean}
1934 	 */
1935 	intersectsWith: function(otherObject) {
1936 		if( this.getBoundingCircle && otherObject.getBoundingCircle) {
1937 			var boundsWC = this.getBoundingCircle(),//.copy().transform(this.mc2wc),
1938 				boundsWC2 = otherObject.getBoundingCircle();//.copy().transform(otherObect.mc2wc);
1939 			return boundsWC.center.distanceTo(boundsWC2.center) <= (boundsWC.r + boundsWC2.r);
1940 		}
1941 		return undefined;
1942 	},
1943 	/**Override this to apply additional transformations.
1944 	 * @event
1945 	 */
1946 	step: undefined,
1947 	/**Draw the object to the canvas.
1948 	 * The objects transformation is already applied to the canvas when this
1949 	 * function is called. Therefore drawing commands should use modeling
1950 	 * coordinates.
1951 	 * @param ctx Canvas 2D context.
1952 	 * @event
1953 	 */
1954 	render: undefined,
1955 	/**Callback, triggered when document keydown event occurs.
1956 	 * @param {Event} e
1957 	 * @param {string} key stringified key, e.g. 'a', 'A', 'ctrl+a', or 'shift+enter'.
1958 	 * @event
1959 	 */
1960 	onKeydown: undefined,
1961 	/**Callback, triggered when document keyup event occurs.
1962 	 * @param {Event} e
1963 	 * @param {string} key stringified key, e.g. 'a', 'A', 'ctrl+a', or 'shift+enter'.
1964 	 * @event
1965 	 */
1966 	onKeyup: undefined,
1967 	/**Callback, triggered when document keypress event occurs.
1968 	 * Synchronous keys are supported
1969 	 * @param {Event} e
1970 	 * @see ArcadeJS.isKeyDown(keyCode)
1971 	 * @event
1972 	 */
1973 	onKeypress: undefined,
1974 	/**Callback, triggered when mouse wheel was used.
1975 	 * Note: this requires to include the jquery.mouseweheel.js plugin.
1976 	 * @param {Event} e
1977 	 * @param {int} delta +1 or -1
1978 	 * @event
1979 	 */
1980 	onMousewheel: undefined,
1981 	/**Callback, triggered when a mouse drag starts over this object.
1982 	 * @param {Point2} clickPos
1983 	 * @returns {boolean} must return true, if object wants to receive drag events
1984 	 * @event
1985 	 */
1986 	onDragstart: undefined,
1987 	/**Callback, triggered while this object is dragged.
1988 	 * @param {Vec2} dragOffset
1989 	 * @event
1990 	 */
1991 	onDrag: undefined,
1992 	/**Callback, triggered when a drag operation is cancelled.
1993 	 * @param {Vec2} dragOffset
1994 	 * @event
1995 	 */
1996 	onDragcancel: undefined,
1997 	/**Callback, triggered when a drag operation ends with mouseup.
1998 	 * @param {Vec2} dragOffset
1999 	 * @event
2000 	 */
2001 	onDrop: undefined,
2002 	/**Called on miscelaneous touch... and gesture... events.
2003 	 * @param {Event} event jQuery event
2004 	 * @param {OriginalEvent} originalEvent depends on mobile device
2005 	 * @event
2006 	 */
2007 	onTouchevent: undefined,
2008 	/**Callback, triggered when game or an object activity changes.
2009 	 * @param {Movable} target object that changed its activity (May be the ArcadeJS object too).
2010 	 * @param {string} activity new activity
2011 	 * @param {string} prevActivity previous activity
2012 	 * @event
2013 	 */
2014 	onSetActivity: undefined,
2015 	/**Callback, triggered when timeout expires (and no callback was given).
2016 	 * @param data data object passed to later()
2017 	 * @event
2018 	 */
2019 	onTimeout: undefined,
2020 	/**Callback, triggered when this object dies.
2021 	 * @event
2022 	 */
2023 	onDie: undefined,
2024 	/**Adjust velocity (by applying acceleration force) to move an object towards
2025 	 * a target position.
2026 	 * @param {float} stepTime
2027 	 * @param {Point2} targetPos
2028 	 * @param {float} eps
2029 	 * @param {float} maxSpeed
2030 	 * @param {float} turnRate
2031 	 * @param {float} maxAccel
2032 	 * @param {float} maxDecel
2033 	 * @returns {booelan} true, if target position is reached (+/- eps)
2034 	 */
2035 	driveToPosition: function(stepTime, targetPos, eps, maxSpeed, turnRate, maxAccel, maxDecel){
2036 		var vTarget = this.pos.vectorTo(targetPos),
2037 			dTarget = vTarget.length(),
2038 			aTarget = LinaJS.angleDiff(this.orientation + 90*LinaJS.D2R, vTarget.angle()),
2039 			curSpeed = this.velocity.length();
2040 
2041 		if(dTarget <= eps && curSpeed < eps){
2042 			this.velocity.setNull();
2043 			return true;
2044 		}
2045 		if(this.velocity.isNull()){
2046 //			this.velocity = vTarget.copy().setLength(stepTime * maxAccel).limit(maxSpeed);
2047 			this.velocity = LinaJS.polarToVec(this.orientation, LinaJS.EPS);
2048 			curSpeed = this.velocity.length();
2049 			maxAccel = 0;
2050 		}
2051 		// Turn to target (within 0.1� accuracy)
2052 		if(Math.abs(aTarget) > 0.1 * LinaJS.D2R){
2053 			if(aTarget > 0){
2054 				this.orientation += Math.min(aTarget, stepTime * turnRate);
2055 			}else{
2056 				this.orientation -= Math.min(-aTarget, stepTime * turnRate);
2057 			}
2058 			this.velocity.setAngle(this.orientation + 90*LinaJS.D2R);
2059 //			this.game.debug("driveToPosition: turning to " + this.orientation * LinaJS.R2D + "�");
2060 		}
2061 		// Decelerate, if desired and target is in reach
2062 		if(maxDecel > 0 && dTarget < curSpeed){
2063 			this.velocity.setLength(Math.max(LinaJS.EPS, curSpeed - stepTime * maxDecel));
2064 //			this.game.debug("driveToPosition: breaking to speed = " + this.velocity.length());
2065 		}else if(maxAccel > 0 && maxSpeed > 0 && Math.abs(curSpeed - maxSpeed) > LinaJS.EPS){
2066 			// otherwise accelerate to max speed, if this is desired
2067 			this.velocity.setLength(Math.min(maxSpeed, curSpeed + stepTime * maxAccel));
2068 //			this.game.debug("driveToPosition: accelerating to speed = " + this.velocity.length());
2069 		}
2070 		return false;
2071 	},
2072 ///**
2073 // * Adjust velocity (by applying acceleration force) to move an object towards
2074 // * a target position.
2075 // */
2076 //	floatToPosition: function(targetPos, maxAccel, maxSpeed, maxDecel){
2077 //		//TODO
2078 //		var vDest = this.pos.vectorTo(targetPos);
2079 //		this.velocity.accelerate(vDest.setLength(maxAccel), maxSpeed);
2080 //		// make sure we are heading to the moving direction
2081 //		this.orientation = this.velocity.angle() - 90*LinaJS.D2R;
2082 ////		this.game.debug("v: " + this.velocity);
2083 ////		if( this.attackMode && vTarget.length() < minFireDist
2084 ////				&& Math.abs(vTarget.angle() - this.orientation - 90*LinaJS.D2R) < 25*LinaJS.D2R){
2085 ////			this.fire();
2086 ////		}
2087 //	},
2088 
2089 
2090 	/**Turn game object to direction or target point.
2091 	 * @param {float} stepTime
2092 	 * @param {float | Vec2 | Point2} target angle, vector or position
2093 	 * @param {float} turnRate
2094 	 * @returns {booelan} true, if target angle is reached
2095 	 */
2096 	turnToDirection: function(stepTime, target, turnRate){
2097 		var angle = target;
2098 		if(target.x !== undefined){
2099 			// target is a point: calc angle from current pos top this point
2100 			angle = this.pos.vectorTo(target).angle();
2101 		}else if(target.dx !== undefined){
2102 			// target is a vector
2103 			angle = target.angle();
2104 		}
2105 		// now calc the delta-angle
2106 		angle = LinaJS.angleDiff(this.orientation + 90*LinaJS.D2R, angle);
2107 		// Turn to target (within 0.1� accuracy)
2108 		if(Math.abs(angle) <= 0.1 * LinaJS.D2R){
2109 			return true;
2110 		}
2111 		if(angle > 0){
2112 			this.orientation += Math.min(angle, stepTime * turnRate);
2113 		}else{
2114 			this.orientation -= Math.min(-angle, stepTime * turnRate);
2115 		}
2116 		this.velocity.setAngle(this.orientation + 90*LinaJS.D2R);
2117 		// return true, if destination orientation was reached
2118 		return Math.abs(angle) <= stepTime * turnRate;
2119 	},
2120 
2121 	// --- end of class
2122 	__lastentry: undefined
2123 });
2124 
2125 /**Default options used when a Movable or derived object is constructed. */
2126 Movable.defaultOptions = {
2127 	pos: null,
2128 	clipModeX: "wrap",
2129 	clipModeY: "wrap",
2130 	debug: {
2131 		level: 1,
2132 		showLabel: false,
2133 		showBBox: false,
2134 		showBCircle: false,
2135 		showVelocity: false,
2136 		velocityScale: 1.0
2137 	},
2138 	__lastentry: undefined
2139 }
2140