1 /*
  2  ___               
  3 | _ \_  _ _ _ __ __
  4 |  _/ || | ' \\ \ /
  5 |_|  \_,_|_||_/_\_\
  6                    
  7 */
  8 /**
  9  * @author magicmarkers
 10  * @description A derpy HTML5/JS engine for side-scrolling brawler / beat 'em up / punchy-fighty games.
 11  * @see https://github.com/magicmarkers/Punx
 12  */
 13 
 14 // TODO: change collision engine, make pixel perfect. keep depth in mind.
 15 // TODO: get audio stuff working
 16 // TODO: parallax backgrounds
 17 // TODO: boilerplate baddy AI
 18 // TODO: boilerplate post-hit temporary invulnerability/blinkage for player
 19 // TODO: boilerplate talk bubbles
 20 
 21 /**
 22  * @namespace Punx
 23  */
 24 var Punx = {};
 25 
 26 /*
 27  ___                      ___     _    _ 
 28 | _ \_  _ _ _ __ __      / __|_ _(_)__| |
 29 |  _/ || | ' \\ \ /  _  | (_ | '_| / _` |
 30 |_|  \_,_|_||_/_\_\ (_)  \___|_| |_\__,_|
 31                                          
 32 */
 33 /**
 34  * @class Represents the game engine. Houses the main loop and canvas DOM node reference.
 35  * @param {object} args Public field construction values.
 36  */
 37 var Punx.Grid = function(args) {
 38 
 39     /**
 40      * Required: Object holding all screens, keyed by name.
 41      * @type object.<string,Punx.Screen>
 42      * @public
 43      */
 44     this.screens = args.screens;
 45 
 46     /**
 47      * Required: Key of the initial screen.
 48      * @public
 49      * @type string
 50      */
 51     this.screen = args.screen
 52 
 53     /**
 54      * Integer canvas width.
 55      * @public
 56      * @type number
 57      * @default 960
 58      */
 59     this.w = args.w || 960;
 60 
 61     /**
 62      * Integer canvas height.
 63      * @public
 64      * @type number
 65      * @default 540
 66      */
 67     this.h = args.h || 540;
 68 
 69     /**
 70      * Integer frames-per-second.
 71      * @public
 72      * @type number
 73      * @default 60
 74      */
 75     this.fps = args.fps || 60;
 76 
 77     /**
 78      * For debugging, causes the main loop to idle when true.
 79      * @public
 80      * @type boolean
 81      * @default false
 82      */
 83     this.idle = false;
 84 
 85     /**
 86      * Internal frame counter, increments once after every update cycle, wraps at fps. This should be treated as read-only.
 87      * @public
 88      * @type number
 89      * @default 0
 90      */
 91     this.frame = 0;
 92 
 93     /**
 94      * Viewport canvas context.
 95      * @public
 96      */
 97     this.ctx = null;
 98 
 99     /**
100      * Imaginary canvas context.
101      * @public
102      */
103     this.ictx = null;
104 
105     /**
106      * Viewport canvas DOM element. This is displayed.
107      * @private
108      */
109     this.canvas = null;
110 
111     /**
112      * Imaginary canvas DOM element used as a frame-buffer for collision detection and other graphics analysis. Scratch-space. Never shown.
113      * @private
114      */
115     this.icanvas = null;
116 
117     /**
118      * The game loop, an interval that calls update() on the current screen, timed according to fps.
119      * @private
120      */
121     this.loop = null;
122 
123     /**
124      * Creates the canvases and injects the main canvas into the viewport.
125      * @public
126      * @param [parentElement="document.body"] DOM element to make the canvas a child of.
127      */
128     this.create = function(parentElement) {
129         // viewport canvas
130         this.canvas = document.createElement('canvas');
131         this.canvas.setAttribute('width',this.w);
132         this.canvas.setAttribute('height',this.h);
133         this.canvas.setAttribute('tabindex',0);
134         this.canvas.addEventListener('keydown',Punx.Key.onkeydown,true);
135         this.canvas.addEventListener('keyup',Punx.Key.onkeyup,true);
136         this.ctx = this.canvas.getContext('2d');
137         this.ctx.imageSmoothingEnabled = false;
138         this.ctx.mozImageSmoothingEnabled = false;
139         this.ctx.webkitImageSmoothingEnabled = false;
140         (parentElement||document.body).appendChild(this.canvas);
141         this.canvas.focus();
142 
143         // imaginary canvas
144         this.icanvas = document.createElement('canvas');
145         this.icanvas.setAttribute('width',this.w);
146         this.icanvas.setAttribute('height',this.h);
147         this.ictx = this.icanvas.getContext('2d');
148         this.ictx.imageSmoothingEnabled = false;
149         this.ictx.mozImageSmoothingEnabled = false;
150         this.ictx.webkitImageSmoothingEnabled = false;
151     };
152 
153     /**
154      * Starts the main loop.
155      * @public
156      */
157     this.start = function() {
158         this.loop = window.setInterval(this.update.bind(this),1000/this.fps);
159     };
160 
161     /**
162      * The main loop calls this once every fps. The loop is killed if any errors occur. Does nothing if idle is true.
163      * @private
164      */
165     // the main loop calls this once every FPS.
166     // kills the loop upon error. does nothing if idle is true
167     this._update = function() {
168         if (this.idle) return;
169         try {
170             this.screen.update();
171             this.frame = ++this.frame % this.fps;
172         }
173         catch (e) {
174             console.log(e.stack);
175             window.clearInterval(this._loop);
176         }
177     };
178 
179 };
180 
181 /*
182  ___                     _   _ _   _ _ 
183 | _ \_  _ _ _ __ __     | | | | |_(_) |
184 |  _/ || | ' \\ \ /  _  | |_| |  _| | |
185 |_|  \_,_|_||_/_\_\ (_)  \___/ \__|_|_|
186                                        
187 */
188 
189 Punx.Util = new function() {
190 
191     this.PHI = (1+Math.sqrt(5))/2; // golden ratio
192     this.PHI_A = 1 - this.PHI;
193     this.PHI_B = 1 - this.PHI_A;
194 
195     // determines if two boxes, a and b, are intersecting
196     this.AABB = function(a,b) {
197         return (!(
198             a.x + a.w < b.x || // a too left
199             a.x > b.x + b.w || // a too right
200             a.y + a.h < b.y || // a too bg (y)
201             a.y > b.y + b.h    // a too fg (y)
202         ));
203     };
204 
205     // returns a box for the intersection of boxes a and b
206     // this may return strange things if they aren't actually intersecting. use AABB first.
207     this.AABBC = function(a,b) {
208         return new function() {
209             this.x = a.x < b.x ? b.x : a.x;
210             this.y = a.y < b.y ? b.y : a.y;
211             this.w = (a.x + a.w < b.x + b.w ? a.x + a.w : b.x + b.w) - this.x;
212             this.h = (a.y + a.h < b.y + b.h ? a.y + a.h : b.y + b.h) - this.y;
213         };
214     };
215 
216     // imagines a box and returns a list of sprites it AABB intersects.
217     // use this for instantaneous projectiles or other things from afar.
218     // sprites is a list of sprite instances, and box is an object with x,y,w,h
219     this.scan = function(box,sprites) {
220         var intersections = [];
221         for (var i in sprites) {
222             if (Punx.Util.AABB(box,sprites[i])) intersections.append(sprites[i]);
223         }
224         return intersections;
225     };
226 
227     // returns a sorted list of sprites according to their proximity to the origin x
228     this.xSort = function(sprites,x) {
229         x=x||0;
230         return sprites.sort(function(a,b) {
231             if (a.x + a.w < x) { // a is left
232                 if (b.x + b.w < x) { // b is left
233                     return (a.x + a.w > b.x + b.w ? -1 : 1);
234                 }
235                 else { // b is right
236                     return (x - (a.x + a.w) < b.x - x ? -1 : 1);
237                 }
238             }
239             else { // a is right
240                 if (b.x > x) { // b is right
241                     return (a.x < b.x ? -1 : 1);
242                 }
243                 else { // b is left
244                     return (a.x - x < x - (b.x + b.w) ? -1 : 1);
245                 }
246             }
247         });
248     };
249 
250     // returns a sorted list of sprites according to their proximity to the origin y
251     this.ySort = function(sprites,y) {
252         y=y||0;
253         return sprites.sort(function(a,b) {
254             if (a.y + a.h < y) { // a is above
255                 if (b.y + b.h < y) { // b is above
256                     return (a.y + a.h > b.y + b.h ? -1 : 1);
257                 }
258                 else { // b is below
259                     return (y - (a.y + a.h) < b.y - y ? -1 : 1);
260                 }
261             }
262             else { // a is below
263                 if (b.y > y) { // b is below
264                     return (a.y < b.y ? -1 : 1);
265                 }
266                 else { // b is above
267                     return (a.y - y < y - (b.y + b.h) ? -1 : 1);
268                 }
269             }
270         });
271     };
272 
273 };
274 
275 /*
276   ___     _    _       _  __         
277  / __|_ _(_)__| |     | |/ /___ _  _ 
278 | (_ | '_| / _` |  _  | ' </ -_) || |
279  \___|_| |_\__,_| (_) |_|\_\___|\_, |
280                                 |__/ 
281 */
282 // keyboard stuff
283 
284 Punx.Key = new function() {
285 
286     // key name => bool state
287     // sprites should poll this during update() and act accordingly.
288     // sprites should use internal flags to determine if they can be interrupted.
289     // example: if (Punx.Key.state['left'] && !this.busy) ...
290     this.state = {};
291 
292     // keyCode => key name
293     this._keys = {
294         '9':'tab',
295         '13':'enter',
296         '27':'escape',
297         '32':'space',
298         '37':'left',
299         '38':'up',
300         '39':'right',
301         '40':'down'
302     };
303     for (var i=48;i<=57;i++) { this._keys[i] = String.fromCharCode(i); } // numbers
304     for (var i=65;i<=90;i++) { this._keys[i] = String.fromCharCode(i).toLowerCase(); } // letters
305 
306     // document's keydown listener
307     this._keydown = function(e) {
308         e=e||window.event;
309         if (e.altGraphKey || e.metaKey || e.altKey || e.shiftKey || e.ctrlKey) return;
310         var key = this._keys[e.keyCode];
311         if (!key) return;
312         e.preventDefault();
313         e.stopPropagation();
314         this.state[key] = true;
315     }.bind(this); // event handlers are wacky and need "this" explicitly bound
316 
317     // document's keyup listener
318     this._keyup = function(e) {
319         e=e||window.event;
320         var key = this._keys[e.keyCode];
321         if (!key) return;
322         e.preventDefault();
323         e.stopPropagation();
324         this.state[key] = false;
325     }.bind(this); // event handlers are wacky and need "this" explicitly bound
326 
327 };
328 
329 /*
330   ___     _    _       ___                   _ 
331  / __|_ _(_)__| |     / __| ___ _  _ _ _  __| |
332 | (_ | '_| / _` |  _  \__ \/ _ \ || | ' \/ _` |
333  \___|_| |_\__,_| (_) |___/\___/\_,_|_||_\__,_|
334                                                
335 */
336 // in progress
337 Audio.prototype.fadeTo = function(fadeTo,callback) {
338     if (fadeTo < 0) fadeTo = 0;
339     else if (fadeTo > 1) fadeTo = 1;
340     var that = this;
341     var setVolume = function(setTo) {
342         return function() {
343             that.volume = setTo;
344             if (setTo == fadeTo && callback) callback();
345         };
346     };
347     var delay = 0;
348     if (this.volume < fadeTo) {
349         for (var v=this.volume; v<=fadeTo; v+=0.1) {
350             setTimeout(setVolume(v),delay);
351             delay+=100;
352         }
353     }
354     else if (volume > fadeTo) {
355         for (var v=this.volume; v>=fadeTo; v-=0.1) {
356             setTimeout(setVolume(v),delay);
357             delay+=100;
358         }
359     }
360     else if (callback) callback();
361 };
362 
363 Punx.Sound = new function() {
364 
365     this.music = new Audio(); // swap out for one in Library
366 
367     this.muted = false;
368     this.mute = function() {
369         this.muted ^= 1;
370         this.music.muted = this.muted;
371     };
372 
373 };
374 
375 /*
376   ___     _    _       ___                      
377  / __|_ _(_)__| |     / __| __ _ _ ___ ___ _ _  
378 | (_ | '_| / _` |  _  \__ \/ _| '_/ -_) -_) ' \ 
379  \___|_| |_\__,_| (_) |___/\__|_| \___\___|_||_|
380                                                 
381 */
382 // a screen is a collection of sprites, such as a menu. levels are stages and extend this.
383 // only one screen is drawn to the canvas.
384 
385 Punx.Screen = function(args) {
386 
387     // required
388     this.grid = args['grid']; // grid reference
389 
390     // optional
391     this.sprites = args['sprites']||[]; // list of Punx.Sprite instances
392     this.bg = args['bg']; // Image
393 
394     // called by the grid. screens should draw themselves to the grid canvas context if they need to.
395     // this notifies sprites that they should "step" or otherwise incrementally change themselves if needed.
396     // sprites should return boolean of whether or not they changed
397     this.update = function() {
398         var updated = false;
399         for (var i in this.sprites) {
400             updated |= this.sprites[i].update();
401         }
402         if (updated) this._draw();
403     };
404 
405     // draw the screen
406     // sprites are drawn from "back to front", that is, sprites with more "depth" are drawn first as to mimic layering.
407     this._draw = function() {
408         this.grid.ctx.clearRect(0,0,this.grid.w,this.grid.h);
409         if (this.bg) this.grix.ctx.drawImage(this.bg,0,0);
410         for (var i in this.sprites.sort(Punx.Sprite.sort)) {
411             this.sprites[i].draw(this.grid.ctx);
412         }
413     };
414 };
415 
416 /*
417   ___     _    _       ___                            ___ _                 
418  / __|_ _(_)__| |     / __| __ _ _ ___ ___ _ _       / __| |_ __ _ __ _ ___ 
419 | (_ | '_| / _` |  _  \__ \/ _| '_/ -_) -_) ' \   _  \__ \  _/ _` / _` / -_)
420  \___|_| |_\__,_| (_) |___/\__|_| \___\___|_||_| (_) |___/\__\__,_\__, \___|
421                                                                   |___/     
422 */
423 // stages are screens that are game levels. they have "curtains" and a "floor".
424 // imagine looking upon a theater stage from an ideal balcony seat.
425 // curtains are used for backgrounds and parallax.
426 // the floor is the "depth" where actors move away from and toward the "front" of the stage.
427 // the defaults define the curtains and floor in terms of the golden ratio:
428 // http://en.wikipedia.org/wiki/Golden_ratio
429 // where the longer portion represents the curtains and the shorter portion is the floor.
430 
431 Punx.Screen.Stage = function(args) {
432     Punx.Screen.call(this,args);
433 
434     // required
435     this.player = args['player']; // Punx.Sprite.Actor.Player instance
436 
437     // optional
438     this.w = args['w'] || 960; // width of the stage (can go beyond viewport for panning)
439     this.y = args['y'] || 334; // height of curtains
440     this.h = args['h'] || 206; // height ("depth") of floor.
441     this.x = args['x'] || 0; // starting pan amount from stage left
442     this.panMargin = 200; // player distance from frame edge to start panning
443     this.pannable = args['pannable']||true; // whether to pan
444 
445     // private stuff temporary to update()
446     this._sprites = []; // all in-frame sprites
447     this._actors = []; // in-frame actors
448     this._walls = []; // in-frame walls
449 
450     this.update = function() {
451         var updated = false;
452         this._sprites = [];
453         this._walls = [];
454         this._actors = [];
455 
456         // pan
457         if (this.pannable) with (this) {
458             // pan left if: frame left > stage left && player left < frame left margin
459             if (x > 0 && player.x < x + panMargin) {
460                 // amount: margin - (player left - frame left)
461                 x -= panMargin - (player.x - x);
462                 if (x < 0) x = 0; // stop at min left
463             }
464             // pan right if: frame right < stage right && player right > frame right margin
465             else if (x + grid.w < w && player.x + player.w > x + grid.w - panMargin) {
466                 // amount: margin - ((frame right) - (player right))
467                 x += panMargin - ((x + grid.w) - (player.x + player.w));
468                 if (x > w - grid.w) x = w - grid.w; // stop at max right
469             }
470         }
471 
472         // determine active sprites, in-frame and frame-edged
473         for (var i in this.sprites) {
474             var s = this.sprites[i];
475             if (s.x + s.w < this.x || s.x > this.x + this.grid.w) continue; // sprite is out of frame
476             this._sprites.push(s);
477             if (s instanceof Punx.Sprite.Wall) this._walls.push(s);
478             else if (s instanceof Punx.Sprite.Actor) this._actors.push(s);
479         }
480 
481         // update active sprites. sprites should return boolean of whether they updated
482         for (var i in this._sprites.sort(Punx.Sprite.sort)) {
483             updated |= this._sprites[i].update();
484         }
485 
486         if (updated) {
487             this._draw(); // draw the screen
488             this._collisions(); // detect collisions and notify sprites after drawing so "reactions" occur next update cycle
489         }
490 
491     };
492 
493     // returns boolean. actors should call this during their update() to determine if they can move to a given position
494     this.clipping = function(actor,x,y) {
495         // ensure actor stays on stage floor vertically. their "depth" is determined by the PHI_B of their height
496         if (y < this.y - (actor.h * Punx.Const.PHI_B) || y + actor.h > this.y + this.h) return true;
497 
498         // ensure actor can only walk into frame horizontally, not out of frame
499         if (x < this.x || x + actor.w > this.x + this.grid.w) return true;
500 
501         // ensure actor doesn't walk through walls
502         for (var i in this._walls) {
503             if (Punx.Util.AABB(this._walls[i],actor.w,actor.h)) return true;
504         }
505     };
506 
507     // draws the screen
508     this._draw = function() {
509         this.grid.ctx.clearRect(0,0,this.grid.w,this.grid.h);
510         for (var i in this._sprites.sort(Punx.Sprite.sort)) {
511             this._sprites[i].draw(this.grid.ctx);
512         }
513     };
514 
515     this._collisions = function() {
516         // check in-frame actors for collision
517         // this performs n(n-1)/2 AABB checks, subchecking pixels in overlapping areas
518         // a pixel is only considered collidable if its opacity is >=127
519         // when two actors collide, both are notified via actor.collide(stage,other actor)
520         // the order of notification is undefined. actors should only "be collided", not do things to effect the other actor as it might not be notified yet.
521         // TODO "z" axis
522         var _actorsLength = this._actors.length;
523         for (var i=0;i<_actorsLength;i++) {
524             var a = this._actors[i];
525             for (var j=i+1;j<_actorsLength;j++) {
526                 var b = this._actors[j];
527                 if (!Punx.Util.AABB(a,b)) continue; // no box collision, move on.
528 
529                 // triggers don't need pixel detection, bounding box is enough
530                 if (a instanceof Punx.Sprite.Actor.Trigger || b instanceof Punx.Sprite.Actor.Trigger) {
531                     a.collide(b);
532                     b.collide(a);
533                     continue;
534                 }
535 
536                 // moving on to pixel detection,
537                 // determine intersection
538                 var c = Punx.Util.AABBC(a,b);
539 
540                 // get separate slices of a and b at the intersection using the scratch canvas
541                 this.grid.ictx.clearRect(a.x-this.x,a.y,a.w,a.h); // clear scratch space
542                 this.grid.draw(a,this.grid.ictx); // draw scratch a
543                 c.a = this.grid.ictx.getImageData(c.x-this.x,c.y,c.w,c.h); // slice of a @ intersect
544                 this.grid.ictx.clearRect(b.x-this.x,b.y,b.w,b.h); // clear scratch space
545                 this.grid.draw(b,this.grid.ictx); // draw scratch b
546                 c.b = this.grid.ictx.getImageData(c.x-this.x,c.y,c.w,c.h); // slice of b @ intersect
547 
548                 // compare slices on a pixel-by-pixel basis according to a resolution
549                 var resolution = 5; // jump this many pixels for each check in order to speed it up.
550                                     // if set to 1, all pixels are checked.
551                 resolution *= 4;    // each pixel actually takes up 4 elements in the data.
552 
553                 var sliceLength = c.a.data.length;
554                 for (var p=0;p<sliceLength;p+=resolution) {
555                     // notify and break when two pixels collide
556                     // pixels are collidable when their opacity is >= 127
557                     if (c.a.data[p+3] >= 127 && c.b.data[p+3] >= 127) {
558                         a.collide(b);
559                         b.collide(a);
560                         break;
561                     }
562                 }
563             }
564         }
565     };
566 
567 }; Punx.Screen.Stage.prototype = new Punx.Screen();
568 
569 /*
570   ___     _    _       ___          _ _       
571  / __|_ _(_)__| |     / __|_ __ _ _(_) |_ ___ 
572 | (_ | '_| / _` |  _  \__ \ '_ \ '_| |  _/ -_)
573  \___|_| |_\__,_| (_) |___/ .__/_| |_|\__\___|
574                           |_|                 
575 */
576 // sprites are stage elements that have a drawable face
577 
578 Punx.Sprite = function(args) {
579 
580     // required
581     this.screen = args['screen']; // Punx.Screen reference
582     this.sheet = new Image();
583     this.sheet.src = args['sheet']; // sprite sheet url relative to html
584     this.frames = args['frames']; // { group name : [ [ x,y,w,h ] , ... ] , ... } where x,y,w,h is sheet cropping origin point
585     this.group = args['group']; // current frame group name
586 
587     // optional
588     this.rate = v['rate']||10; // updates per animation step
589     this.name = args['name']||'Sprite';
590     this.x = args['x']||0;
591     this.y = args['y']||0;
592     this.z = args['z']||1; // 0 rug, 1 standing, 2 in air, 3 sticky
593     this.w = args['w']||64;
594     this.h = args['h']||64;
595 
596     this._step = 0; // animation step index, increments once per rated update prior to step() being called, wraps at length of current group
597 
598     this.setGroup = function(name) {
599         this.group = name;
600         this._step %= this.frames[this.group].length; // wrap animation step index
601     };
602 
603     this.update = function() {
604         var updated = false;
605 
606         // issue a tick. this is the sprite's custom logic every grid fps.
607         updated |= this.tick();
608 
609         // rate-limited steps
610         if (this.screen.grid.frame % this.rate == 0) {
611             if (this.frames[this.group].length > 1) {
612                 updated = true;
613                 this._step = ++this._step % this.frames[this.group].length; // increment and wrap animation step index
614             }
615             updated |= this.step();
616         }
617 
618         return updated;
619     };
620 
621     // called whenever the screen calls update() on the sprite.
622     // use this to perform logic per grid frame, return boolean if the sprite is updated
623     this.tick = function() {
624     };
625 
626     // called every rated amount of grid frames
627     // use this to perform rated logic. it may be called whether or not an animation step was made, as groups can have a single frame.
628     // return boolean of whether the sprite is updated
629     this.step = function() {
630     };
631 
632     this.draw = function(ctx) {
633         var frame = this.frames[this.group][this._step];
634         ctx.drawImage(this.sheet,this.frame[0],this.frame[1],this.frame[2],this.frame[3],this.x - (this.screen.x||0),this.y,this.w,this.h);
635     }
636 };
637 
638 Punx.Sprite.sort = function(a,b) {
639     // sorting function that prioritizes sprites with more depth
640     // this is mainly used when figuring out the drawing order.
641     if (a instanceof Punx.Sprite.Wall ^ b instanceof Punx.Sprite.Wall) return (a instanceof Punx.Sprite.Wall ? -1 : 1); // walls always behind
642     else if (a.z != b.z) return (a.z < b.z ? -1 : 1);
643     else return (a.y + a.h < b.y + b.h ? -1 : 1); // sprite's bottom is further away
644 };
645 
646 /*
647   ___     _    _       ___          _ _            __      __    _ _ 
648  / __|_ _(_)__| |     / __|_ __ _ _(_) |_ ___      \ \    / /_ _| | |
649 | (_ | '_| / _` |  _  \__ \ '_ \ '_| |  _/ -_)  _   \ \/\/ / _` | | |
650  \___|_| |_\__,_| (_) |___/ .__/_| |_|\__\___| (_)   \_/\_/\__,_|_|_|
651                           |_|                                        
652 */
653 // a wall is a stage element that actors cannot clip.
654 // walls are not actors, they are inert.
655 
656 Punx.Sprite.Wall = function(args) {
657     Punx.Sprite.call(this,args);
658     
659 }; Punx.Sprite.Wall.prototype = new Punx.Sprite();
660 
661 /*
662   ___     _    _       ___          _ _               _      _           
663  / __|_ _(_)__| |     / __|_ __ _ _(_) |_ ___        /_\  __| |_ ___ _ _ 
664 | (_ | '_| / _` |  _  \__ \ '_ \ '_| |  _/ -_)  _   / _ \/ _|  _/ _ \ '_|
665  \___|_| |_\__,_| (_) |___/ .__/_| |_|\__\___| (_) /_/ \_\__|\__\___/_|  
666                           |_|                                            
667 */
668 // actors are any non-inert stage element. these include baddies, powerups, and props.
669 // actors interact, such as via collision.
670 
671 Punx.Sprite.Actor = function(args) {
672     Punx.Sprite.call(this,args);
673 
674     this.name = args['name']||'Actor';
675 
676     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
677 
678     // get collided by another actor, react to it.
679     // this is meant to be overloaded.
680     // dont modify the other actor, as notification order is undefined. it's only provided for context so you can react appropriately.
681     // 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
682     // collisions are checked every screen update.
683     // set flags etc accordingly to "debounce" collision notifications.
684     // for example, when the player is collided by a baddy, inflict self-damage and set a flag and timeout for recover-time invulnerability.
685     this.collide = function(actor) {
686     };
687 
688     this.move = function(stage,direction,amount) {
689         amount = amount || 5;
690         this.setGroup(direction);
691         var x = this.x;
692         var y = this.y;
693         switch (direction) {
694             case 'left' : { x -= amount; break; }
695             case 'right' : { x += amount; break; }
696             case 'up' : { y -= amount; break; }
697             case 'down' : { y += amount; break; }
698         }
699         if (!stage.clipping(this,x,y)) {
700             this.x = x;
701             this.y = y;
702         }
703         return true;
704     }
705 
706 }; Punx.Sprite.Actor.prototype = new Punx.Sprite();
707 
708 /*
709   ___     _    _       ___          _ _               _      _                 ___ _                   
710  / __|_ _(_)__| |     / __|_ __ _ _(_) |_ ___        /_\  __| |_ ___ _ _      | _ \ |__ _ _  _ ___ _ _ 
711 | (_ | '_| / _` |  _  \__ \ '_ \ '_| |  _/ -_)  _   / _ \/ _|  _/ _ \ '_|  _  |  _/ / _` | || / -_) '_|
712  \___|_| |_\__,_| (_) |___/ .__/_| |_|\__\___| (_) /_/ \_\__|\__\___/_|   (_) |_| |_\__,_|\_, \___|_|  
713                           |_|                                                             |__/         
714 */
715 // the user
716 
717 Punx.Sprite.Actor.Player = function(v) { 
718     Punx.Sprite.Actor.call(this,v=v||{});
719     this.name = 'Player';
720     this.update = function(stage) {
721         if (!this.busy) {
722             with (Punx.Key) {
723                 if (key['left'] || key['a']) this.move(stage,'left');
724                 else if (key['right'] || key['d']) this.move(stage,'right');
725                 if (key['down'] || key['s']) this.move(stage,'down');
726                 else if (key['up'] || key['w']) this.move(stage,'up');
727             }
728         }
729         Punx.Sprite.Actor.Player.prototype.update.call(this,stage);
730     };
731 }; Punx.Sprite.Actor.Player.prototype = new Punx.Sprite.Actor();
732 
733 /*
734   ___     _    _       ___          _ _               _      _                 _____    _                   
735  / __|_ _(_)__| |     / __|_ __ _ _(_) |_ ___        /_\  __| |_ ___ _ _      |_   _| _(_)__ _ __ _ ___ _ _ 
736 | (_ | '_| / _` |  _  \__ \ '_ \ '_| |  _/ -_)  _   / _ \/ _|  _/ _ \ '_|  _    | || '_| / _` / _` / -_) '_|
737  \___|_| |_\__,_| (_) |___/ .__/_| |_|\__\___| (_) /_/ \_\__|\__\___/_|   (_)   |_||_| |_\__, \__, \___|_|  
738                           |_|                                                            |___/|___/         
739 */
740 // triggers are actors that collide with just bounding box
741 
742 Punx.Sprite.Actor.Trigger = function(v) {
743     Punx.Sprite.Actor.call(this,v=v||{});
744     this.name = 'Trigger';
745 }; Punx.Sprite.Actor.Trigger.prototype = new Punx.Sprite.Actor();
746