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