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