1 /** 2 * A screen object. 3 * @class 4 * @param {object} args Construction values for public properties. 5 */ 6 7 punx.Sprite = function() { 8 9 /** 10 * Sprite sheet to draw from. 11 * @public 12 * @type Image 13 */ 14 this.sheet = null; 15 16 /** 17 * Sprite's X crop offset in the sprite sheet. 18 * @public 19 * @type int 20 * @defaults 0 21 */ 22 this.x = 0; 23 24 /** 25 * Sprite's Y crop offset in the sprite sheet. 26 * @public 27 * @type int 28 * @defaults 0 29 */ 30 this.y = 0; 31 32 /** 33 * Sprite's crop width in the sprite sheet. 34 * @public 35 * @type int 36 * @defaults 64 37 */ 38 this.w = 64; 39 40 /** 41 * Sprite's crop height in the sprite sheet. 42 * @public 43 * @type int 44 * @defaults 64 45 */ 46 this.h = 64; 47 48 /** 49 * Sprite draws itself to the context and position supplied. 50 * If the sheet isn't set, a red rectangle is drawn. 51 * @public 52 * @param ctx Canvas context. 53 */ 54 this.draw = function(ctx,x,y,w,h) { 55 if (this.sheet !== null) { 56 ctx.drawImage(this.sheet,this.x,this.y,this.w,this.h,x,y,w,h); 57 } 58 else { 59 ctx.fillStyle = '#f00'; 60 ctx.drawRect(x,y,w,h); 61 } 62 }; 63 64 }; 65 66 Punx.Sprite.sort = function(a,b) { 67 // sorting function that prioritizes sprites with more depth 68 // this is mainly used when figuring out the drawing order. 69 if (a instanceof Punx.Sprite.Wall ^ b instanceof Punx.Sprite.Wall) return (a instanceof Punx.Sprite.Wall ? -1 : 1); // walls always behind 70 else if (a.z != b.z) return (a.z < b.z ? -1 : 1); 71 else return (a.y + a.h < b.y + b.h ? -1 : 1); // sprite's bottom is further away 72 }; 73 74 // under construction 75 76 Audio.prototype.fadeTo = function(fadeTo,callback) { 77 if (fadeTo < 0) fadeTo = 0; 78 else if (fadeTo > 1) fadeTo = 1; 79 var that = this; 80 var setVolume = function(setTo) { 81 return function() { 82 that.volume = setTo; 83 if (setTo == fadeTo && callback) callback(); 84 }; 85 }; 86 var delay = 0; 87 if (this.volume < fadeTo) { 88 for (var v=this.volume; v<=fadeTo; v+=0.1) { 89 setTimeout(setVolume(v),delay); 90 delay+=100; 91 } 92 } 93 else if (volume > fadeTo) { 94 for (var v=this.volume; v>=fadeTo; v-=0.1) { 95 setTimeout(setVolume(v),delay); 96 delay+=100; 97 } 98 } 99 else if (callback) callback(); 100 }; 101 102 punx.Sound = new function() { 103 104 this.music = new Audio(); // swap out for one in Library 105 106 this.muted = false; 107 this.mute = function() { 108 this.muted ^= 1; 109 this.music.muted = this.muted; 110 }; 111 112 }; 113 114 /** 115 * Represents a collection of sprites to be drawn to the canvas. Only the active screen is drawn. 116 * @class 117 * @param {object} args Construction values for public properties. 118 */ 119 120 punx.Screen = function() { 121 122 /** 123 * List of {punx.Entity} instances. Should be pre-sorted by the draw order (z-index). 124 * @type Punx.Sprites[] 125 * @default [] 126 */ 127 this.bgs = []; 128 129 this.actors = []; 130 131 this.overlays = []; 132 133 var updated = false; 134 135 /** 136 * Update all sprites and remember if updated. 137 * Called by the {Punx} update loop. 138 */ 139 this.update = function() { 140 for (var i in this.bgs) { 141 updated |= this.bgs[i].update(); 142 } 143 for (var i in this.actors) { 144 updated |= this.actors[i].update(); 145 } 146 for (var i in this.overlays) { 147 updated |= this.overlays[i].update(); 148 } 149 }; 150 151 /** 152 * Stage back. 153 */ 154 this.back = 0; 155 156 /** 157 * Stage front. 158 */ 159 this.front = 0; 160 161 /** 162 * Camera tracking X offset. 163 */ 164 this.track = 0; 165 166 /** 167 * Draws only if updated. 168 * Draws all entities in order. 169 * @public 170 */ 171 this.draw = function() { 172 if (!updated) return; 173 updated = false; 174 Punx.clear(); 175 for (var i in this.bgs) { 176 this.bgs[i].draw(); 177 } 178 for (var i in this.actors) { 179 this.actors.draw(); 180 } 181 for (var i in this.overlays) { 182 this.overlays[i].draw(); 183 } 184 }; 185 186 this.move = function(a,x,y) { 187 // TODO account for clipping and collision 188 a.x = x; 189 a.y = y; 190 } 191 192 }; 193 194 /** 195 * Keyboard interface. 196 */ 197 punx.key = new function() { 198 199 /** 200 * Object keyed by button name and its current press-state, so that sprites et al. can poll the keyboard. 201 * @private 202 * @type object<string,boolean> 203 */ 204 var state = {}; 205 206 /** 207 * Internal map between keyCode and button name. 208 * @private 209 * @type object<integer,string> 210 */ 211 var map = { 212 9:'tab', 213 13:'enter', 214 27:'escape', 215 32:'space', 216 37:'left', 217 38:'up', 218 39:'right', 219 40:'down' 220 }; 221 for (var i=48;i<=57;i++) { map[i] = String.fromCharCode(i); } // numbers 222 for (var i=97;i<=122;i++) { map[i] = String.fromCharCode(i); } // letters 223 224 this.pressed = function(name) { 225 return name in state && state[name]; 226 }; 227 228 /** 229 * Document's onkeydown listener, attached to the document by the grid during create(). 230 * Sets the key's state to true. Keypresses that are combined with special keys like CTRL, ALT, Shift, Meta/Windows, Menu, etc are ignored. 231 * @public 232 * @param {event} [e=window.event] 233 */ 234 this.onkeydown = function(e) { 235 e=e||window.event; 236 if (!(e.keyCode in map) || e.altGraphKey || e.metaKey || e.altKey || e.shiftKey || e.ctrlKey) return; 237 e.preventDefault(); 238 e.stopPropagation(); 239 state[map[e.keyCode]] = true; 240 }.bind(this); 241 242 /** 243 * Document's keyup listener, attached to the document by the grid during create(). 244 * Sets the key's state to false. 245 * @public 246 * @param {event} [e=window.event] 247 */ 248 this.onkeyup = function(e) { 249 e=e||window.event; 250 if (!(e.keyCode in map)) return; 251 e.preventDefault(); 252 e.stopPropagation(); 253 state[map[e.keyCode]] = false; 254 }.bind(this); 255 256 }; 257 258 /** 259 * @author magicmarkers 260 * @description A derpy HTML5/JS engine for side-scrolling brawler / beat 'em up / punchy-fighty games. 261 * @see https://github.com/magicmarkers/Punx 262 */ 263 264 window.requestAnimationFrame = window.requestAnimationFrame || window.msRequestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.oRequestAnimationFrame || function(cb){ 265 setTimeout(cb,1000/60); 266 }; 267 268 var punx = {}: 269 270 /** 271 * Determines if two boxes intersect. 272 * @public 273 * @param {Punx.Box} a 274 * @param {Punx.Box} b 275 * @returns boolean 276 */ 277 punx.aabb = function(a,b) { 278 return (!( 279 a.x + a.w < b.x || // a too left 280 a.x > b.x + b.w || // a too right 281 a.y + a.h < b.y || // a too bg (y) 282 a.y > b.y + b.h // a too fg (y) 283 )); 284 }; 285 286 /** 287 * Returns a box describing the intersection of two boxes. 288 * This may return strange things if they aren't actually intersecting. Use AABB() first. 289 * @public 290 * @param {Punx.Box} a 291 * @param {Punx.Box} b 292 * @returns Punx.Box 293 */ 294 punx.aabbc = function(a,b) { 295 var c = { 296 x : (a.x < b.x ? b.x : a.x), 297 y : (a.y < b.y ? b.y : a.y) 298 }; 299 c.w = (a.x + a.w < b.x + b.w ? a.x + a.w : b.x + b.w) - c.x; 300 c.h = (a.y + a.h < b.y + b.h ? a.y + a.h : b.y + b.h) - c.y; 301 return new Punx.Box(c); 302 }; 303 304 /** 305 * Returns an array of sprites, sorted according to their proximity to the origin x. 306 * @param {number} x Integer x origin point 307 * @param {Punx.Sprite[]} sprites Sprites to sort 308 * @returns {Punx.Sprite[]} Sorted array 309 */ 310 punx.xSort = function(x,sprites) { 311 return sprites.sort(function(a,b) { 312 var A = (a.x + a.w < x ? x - (a.x + a.w) : a.x - x); 313 var B = (b.x + b.w < x ? x - (b.x + b.w) : b.x - x); 314 return (A < B ? -1 : (A > B ? 1 : 0)); 315 }); 316 }; 317 318 /** 319 * Returns an array of sprites, sorted according to their proximity to the origin y. 320 * @param {number} y Integer y origin point 321 * @param {Punx.Sprite[]} sprites Sprites to sort 322 * @returns {Punx.Sprite[]} Sorted array 323 */ 324 punx.ySort = function(y,sprites) { 325 return sprites.sort(function(a,b) { 326 var A = (a.y + a.h < y ? y - (a.y + a.h) : a.y - y); 327 var B = (b.y + b.h < y ? y - (b.y + b.h) : b.y - y); 328 return (A < B ? -1 : (A > B ? 1 : 0)); 329 }); 330 }; 331 332 punx.zSort = function(a,b) { 333 return a.z < b.z ? -1 : (b.z > a.z ? 1 : 0); 334 }; 335 336 punx.Game = function() { 337 338 /** 339 * Integer canvas width. 340 * @public 341 * @type number 342 * @default 960 343 */ 344 this.width = 960; 345 346 /** 347 * Integer canvas height. 348 * @public 349 * @type number 350 * @default 540 351 */ 352 this.height = 540; 353 354 this.screen = null; 355 356 var loop = null; 357 358 this.ctx = null; 359 this.ictx = null; 360 361 this.frame = 0; 362 363 this.draw = function() { 364 this.screen.draw(); 365 window.requestAnimationFrame(this.draw); 366 }.bind(this); 367 368 var update = function() { 369 try { 370 this.screen.update(); 371 this.frame = ++this.frame % 60; 372 } 373 catch (x) { 374 console.log(x.stack); 375 window.clearInterval(loop); 376 } 377 }.bind(this); 378 379 this.init = function(containerElement) { 380 var canvas = document.createElement('canvas'); 381 canvas.setAttribute('width',this.width); 382 canvas.setAttribute('height',this.height); 383 canvas.setAttribute('tabindex',0); 384 canvas.addEventListener('keydown',Punx.Key.onkeydown,true); 385 canvas.addEventListener('keyup',Punx.Key.onkeyup,true); 386 this.ctx = canvas.getContext('2d'); 387 this.ctx.imageSmoothingEnabled = false; 388 this.ctx.mozImageSmoothingEnabled = false; 389 this.ctx.webkitImageSmoothingEnabled = false; 390 (containerElement||document.body).appendChild(canvas); 391 canvas.focus(); 392 393 // imaginary canvas 394 var icanvas = document.createElement('canvas'); 395 icanvas.setAttribute('width',this.width); 396 icanvas.setAttribute('height',this.height); 397 this.ictx = icanvas.getContext('2d'); 398 this.ictx.imageSmoothingEnabled = false; 399 this.ictx.mozImageSmoothingEnabled = false; 400 this.ictx.webkitImageSmoothingEnabled = false; 401 402 loop = window.setInterval(update,1000/60); 403 this.draw(); 404 } 405 406 this.clear = function() { 407 this.ctx.clearRect(0,0,this.width,this.height); 408 }; 409 410 }; 411 punx.entity = {}; 412 413 /** 414 * Entity can be collided by another entity. 415 * @interface 416 */ 417 punx.entity.Collidable = function() {}: 418 punx.entity.Collidable.prototype.collide = function(entity) {}; 419 420 /** 421 * Entity is drawable. 422 * @interface 423 */ 424 punx.entity.Drawable = function() {}; 425 punx.entity.Drawable.prototype.draw = function(ctx,track) {}; 426 427 /** 428 * Screen inhabitants. Includes backgrounds, NPC, baddies, player, text, anything drawn or collidable. 429 * @class 430 */ 431 punx.Entity = function(x,y) { 432 433 this.x = x; 434 this.y = y; 435 436 this.update = function() { 437 var updated = false; 438 for (var i in this.elements) { 439 updated |= this.elements[i].update(); 440 } 441 return updated; 442 }; 443 444 }; 445 // actors are any non-inert stage element. these include baddies, powerups, and props. 446 // actors interact, such as via collision. 447 448 Punx.Sprite.Actor = function(args) { 449 Punx.Sprite.call(this,args); 450 451 this.name = args['name']||'Actor'; 452 453 this.collidable = true; 454 455 this.busy = false; // set this to disable user input or to denote that a multi-cycle process is taking place and you dont want to be interrupted 456 457 // get collided by another actor, react to it. 458 // this is meant to be overloaded. 459 // dont modify the other actor, as notification order is undefined. it's only provided for context so you can react appropriately. 460 // chances are if its the initial collision then there will be a large swath of calls to this function within a short period of time, as 461 // collisions are checked every screen update. 462 // set flags etc accordingly to "debounce" collision notifications. 463 // for example, when the player is collided by a baddy, inflict self-damage and set a flag and timeout for recover-time invulnerability. 464 this.collide = function(actor) { 465 }; 466 467 this.move = function(stage,direction,amount) { 468 amount = amount || 5; 469 this.setGroup(direction); 470 var x = this.x; 471 var y = this.y; 472 switch (direction) { 473 case 'left' : { x -= amount; break; } 474 case 'right' : { x += amount; break; } 475 case 'up' : { y -= amount; break; } 476 case 'down' : { y += amount; break; } 477 } 478 if (!stage.clipping(this,x,y)) { 479 this.x = x; 480 this.y = y; 481 } 482 return true; 483 } 484 485 }; Punx.Sprite.Actor.prototype = new Punx.Sprite(); 486 487