//============================================================================= // // We need some ECMAScript 5 methods but we need to implement them ourselves // for older browsers (compatibility: http://kangax.github.com/es5-compat-table/) // // Function.bind: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind // Object.create: http://javascript.crockford.com/prototypal.html // Object.extend: (defacto standard like jquery $.extend or prototype's Object.extend) // // Object.construct: our own wrapper around Object.create that ALSO calls // an initialize constructor method if one exists // //============================================================================= if (!Function.prototype.bind) { Function.prototype.bind = function(obj) { var slice = [].slice, args = slice.call(arguments, 1), self = this, nop = function () {}, bound = function () { return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments))); }; nop.prototype = self.prototype; bound.prototype = new nop(); return bound; }; } if (!Object.create) { Object.create = function(base) { function F() {}; F.prototype = base; return new F(); } } if (!Object.construct) { Object.construct = function(base) { var instance = Object.create(base); if (instance.initialize) instance.initialize.apply(instance, [].slice.call(arguments, 1)); return instance; } } if (!Object.extend) { Object.extend = function(destination, source) { for (var property in source) { if (source.hasOwnProperty(property)) destination[property] = source[property]; } return destination; }; } /* NOT READY FOR PRIME TIME if (!window.requestAnimationFrame) {// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ window.requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback, element) { window.setTimeout(callback, 1000 / 60); } } */ //============================================================================= // Minimal DOM Library ($) //============================================================================= Element = function() { var instance = { _extended: true, showIf: function(on) { if (on) this.show(); else this.hide(); }, show: function() { this.style.display = ''; }, hide: function() { this.style.display = 'none'; }, update: function(content) { this.innerHTML = content; }, hasClassName: function(name) { return (new RegExp("(^|\s*)" + name + "(\s*|$)")).test(this.className) }, addClassName: function(name) { this.toggleClassName(name, true); }, removeClassName: function(name) { this.toggleClassName(name, false); }, toggleClassName: function(name, on) { var classes = this.className.split(' '); var n = classes.indexOf(name); on = (typeof on == 'undefined') ? (n < 0) : on; if (on && (n < 0)) classes.push(name); else if (!on && (n >= 0)) classes.splice(n, 1); this.className = classes.join(' '); } }; var get = function(ele) { if (typeof ele == 'string') ele = document.getElementById(ele); if (!ele._extended) Object.extend(ele, instance); return ele; }; return get; }(); $ = Element; //============================================================================= // State Machine //============================================================================= StateMachine = { //--------------------------------------------------------------------------- create: function(cfg) { var target = cfg.target || {}; var events = cfg.events; var n, event, name, can = {}; for(n = 0 ; n < events.length ; n++) { event = events[n]; name = event.name; can[name] = (can[name] || []).concat(event.from); target[name] = this.buildEvent(name, event.from, event.to, target); } target.current = 'none'; target.is = function(state) { return this.current == state; }; target.can = function(event) { return can[event].indexOf(this.current) >= 0; }; target.cannot = function(event) { return !this.can(event); }; if (cfg.initial) { // see "initial" qunit tests for examples var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow single string to represent initial state, or complex object to configure { state: 'first', event: 'init', defer: true|false } name = initial.event || 'startup'; can[name] = ['none']; event = this.buildEvent(name, 'none', initial.state, target); if (initial.defer) target[name] = event; // allow caller to trigger initial transition event else event.call(target); } return target; }, //--------------------------------------------------------------------------- buildEvent: function(name, from, to, target) { return function() { if (this.cannot(name)) throw "event " + name + " innapropriate in current state " + this.current; var beforeEvent = this['onbefore' + name]; if (beforeEvent && (false === beforeEvent.apply(this, arguments))) return; if (this.current != to) { var exitState = this['onleave' + this.current]; if (exitState) exitState.apply(this, arguments); this.current = to; var enterState = this['onenter' + to] || this['on' + to]; if (enterState) enterState.apply(this, arguments); } var afterEvent = this['onafter' + name] || this['on' + name]; if (afterEvent) afterEvent.apply(this, arguments); } } //--------------------------------------------------------------------------- }; //============================================================================= // GAME //============================================================================= Game = { compatible: function() { return Object.create && Object.extend && Function.bind && document.addEventListener && // HTML5 standard, all modern browsers that support canvas should also support add/removeEventListener Game.ua.hasCanvas }, start: function(id, game, cfg) { if (Game.compatible()) return Game.current = Object.construct(Game.Runner, id, game, cfg).game; // return the game instance, not the runner (caller can always get at the runner via game.runner) }, ua: function() { // should avoid user agent sniffing... but sometimes you just gotta do what you gotta do var ua = navigator.userAgent.toLowerCase(); var key = ((ua.indexOf("opera") > -1) ? "opera" : null); key = key || ((ua.indexOf("firefox") > -1) ? "firefox" : null); key = key || ((ua.indexOf("chrome") > -1) ? "chrome" : null); key = key || ((ua.indexOf("safari") > -1) ? "safari" : null); key = key || ((ua.indexOf("msie") > -1) ? "ie" : null); try { var re = (key == "ie") ? "msie (\\d)" : key + "\\/(\\d\\.\\d)" var matches = ua.match(new RegExp(re, "i")); var version = matches ? parseFloat(matches[1]) : null; } catch (e) {} return { full: ua, name: key + (version ? " " + version.toString() : ""), version: version, isFirefox: (key == "firefox"), isChrome: (key == "chrome"), isSafari: (key == "safari"), isOpera: (key == "opera"), isIE: (key == "ie"), hasCanvas: (document.createElement('canvas').getContext), hasAudio: (typeof(Audio) != 'undefined'), hasTouch: ('ontouchstart' in window) } }(), addEvent: function(obj, type, fn) { $(obj).addEventListener(type, fn, false); }, removeEvent: function(obj, type, fn) { $(obj).removeEventListener(type, fn, false); }, windowWidth: function() { return window.innerWidth || /* ie */ document.documentElement.offsetWidth; }, windowHeight: function() { return window.innerHeight || /* ie */ document.documentElement.offsetHeight; }, ready: function(fn) { if (Game.compatible()) Game.addEvent(document, 'DOMContentLoaded', fn); }, renderToCanvas: function(width, height, render, canvas) { // http://kaioa.com/node/103 canvas = canvas || document.createElement('canvas'); canvas.width = width; canvas.height = height; render(canvas.getContext('2d')); return canvas; }, loadScript: function(src, cb) { var head = document.getElementsByTagName('head')[0]; var s = document.createElement('script'); head.appendChild(s); if (Game.ua.isIE) { s.onreadystatechange = function(e) { if (e.currentTarget.readyState == 'loaded') cb(e.currentTarget); } } else { s.onload = function(e) { cb(e.currentTarget); } } s.type = 'text/javascript'; s.src = src; }, loadImages: function(sources, callback) { /* load multiple images and callback when ALL have finished loading */ var images = {}; var count = sources ? sources.length : 0; if (count == 0) { callback(images); } else { for(var n = 0 ; n < sources.length ; n++) { var source = sources[n]; var image = document.createElement('img'); images[source] = image; Game.addEvent(image, 'load', function() { if (--count == 0) callback(images); }); image.src = source; } } }, loadSounds: function(cfg) { cfg = cfg || {}; if (typeof soundManager == 'undefined') { var path = cfg.path || 'sound/soundmanager2-nodebug-jsmin.js'; var swf = cfg.swf || 'sound/swf'; window.SM2_DEFER = true; Game.loadScript(path, function() { window.soundManager = new SoundManager(); soundManager.useHighPerformance = true; soundManager.useFastPolling = true; soundManager.url = swf; soundManager.defaultOptions.volume = 50; // shhh! soundManager.onready(function() { Game.loadSounds(cfg); }); soundManager.beginDelayedInit(); }); } else { var sounds = []; for(var id in cfg.sounds) { sounds.push(soundManager.createSound({id: id, url: cfg.sounds[id]})); } if (cfg.onload) cfg.onload(sounds); } }, random: function(min, max) { return (min + (Math.random() * (max - min))); }, randomChoice: function(choices) { return choices[Math.round(Game.random(0, choices.length-1))]; }, randomBool: function() { return Game.randomChoice([true, false]); }, timestamp: function() { return new Date().getTime(); }, THREESIXTY: Math.PI * 2, KEY: { BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, DELETE: 46, HOME: 36, END: 35, PAGEUP: 33, PAGEDOWN: 34, INSERT: 45, ZERO: 48, ONE: 49, TWO: 50, A: 65, D: 68, L: 76, P: 80, Q: 81, TILDA: 192 }, //----------------------------------------------------------------------------- Math: { bound: function(box) { if (box.radius) { box.w = 2 * box.radius; box.h = 2 * box.radius; box.left = box.x - box.radius; box.right = box.x + box.radius; box.top = box.y - box.radius; box.bottom = box.y + box.radius; } else { box.left = box.x; box.right = box.x + box.w; box.top = box.y; box.bottom = box.y + box.h; } return box; }, overlap: function(box1, box2, returnOverlap) { if ((box1.right < box2.left) || (box1.left > box2.right) || (box1.top > box2.bottom) || (box1.bottom < box2.top)) { return false; } else { if (returnOverlap) { var left = Math.max(box1.left, box2.left); var right = Math.min(box1.right, box2.right); var top = Math.max(box1.top, box2.top); var bottom = Math.min(box1.bottom, box2.bottom); return {x: left, y: top, w: right-left, h: bottom-top, left: left, right: right, top: top, bottom: bottom }; } else { return true; } } }, normalize: function(vec, m) { vec.m = this.magnitude(vec.x, vec.y); if (vec.m == 0) { vec.x = vec.y = vec.m = 0; } else { vec.m = vec.m / (m || 1); vec.x = vec.x / vec.m; vec.y = vec.y / vec.m; vec.m = vec.m / vec.m; } return vec; }, magnitude: function(x, y) { return Math.sqrt(x*x + y*y); }, move: function(x, y, dx, dy, dt) { var nx = dx * dt; var ny = dy * dt; return { x: x + nx, y: y + ny, dx: dx, dy: dy, nx: nx, ny: ny }; }, accelerate: function(x, y, dx, dy, accel, dt) { var x2 = x + (dt * dx) + (accel * dt * dt * 0.5); var y2 = y + (dt * dy) + (accel * dt * dt * 0.5); var dx2 = dx + (accel * dt) * (dx > 0 ? 1 : -1); var dy2 = dy + (accel * dt) * (dy > 0 ? 1 : -1); return { nx: (x2-x), ny: (y2-y), x: x2, y: y2, dx: dx2, dy: dy2 }; }, intercept: function(x1, y1, x2, y2, x3, y3, x4, y4, d) { var denom = ((y4-y3) * (x2-x1)) - ((x4-x3) * (y2-y1)); if (denom != 0) { var ua = (((x4-x3) * (y1-y3)) - ((y4-y3) * (x1-x3))) / denom; if ((ua >= 0) && (ua <= 1)) { var ub = (((x2-x1) * (y1-y3)) - ((y2-y1) * (x1-x3))) / denom; if ((ub >= 0) && (ub <= 1)) { var x = x1 + (ua * (x2-x1)); var y = y1 + (ua * (y2-y1)); return { x: x, y: y, d: d}; } } } return null; }, ballIntercept: function(ball, rect, nx, ny) { var pt; if (nx < 0) { pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, rect.right + ball.radius, rect.top - ball.radius, rect.right + ball.radius, rect.bottom + ball.radius, "right"); } else if (nx > 0) { pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, rect.left - ball.radius, rect.top - ball.radius, rect.left - ball.radius, rect.bottom + ball.radius, "left"); } if (!pt) { if (ny < 0) { pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, rect.left - ball.radius, rect.bottom + ball.radius, rect.right + ball.radius, rect.bottom + ball.radius, "bottom"); } else if (ny > 0) { pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, rect.left - ball.radius, rect.top - ball.radius, rect.right + ball.radius, rect.top - ball.radius, "top"); } } return pt; } }, //----------------------------------------------------------------------------- Runner: { initialize: function(id, game, cfg) { this.cfg = Object.extend(game.Defaults || {}, cfg || {}); // use game defaults (if any) and extend with custom cfg (if any) this.fps = this.cfg.fps || 60; this.interval = 1000.0 / this.fps; this.canvas = $(id); this.bounds = this.canvas.getBoundingClientRect(); this.width = this.cfg.width || this.canvas.offsetWidth; this.height = this.cfg.height || this.canvas.offsetHeight; this.front = this.canvas; this.front.width = this.width; this.front.height = this.height; this.front2d = this.front.getContext('2d'); this.addEvents(); this.resetStats(); this.resize(); this.game = Object.construct(game, this, this.cfg); // finally construct the game object itself if (this.cfg.state) StateMachine.create(Object.extend({target: this.game}, this.cfg.state)); this.initCanvas(); }, start: function() { // game instance should call runner.start() when its finished initializing and is ready to start the game loop this.lastFrame = Game.timestamp(); this.timer = setInterval(this.loop.bind(this), this.interval); }, stop: function() { clearInterval(this.timer); }, loop: function() { this._start = Game.timestamp(); this.update((this._start - this.lastFrame)/1000.0); // send dt as seconds this._middle = Game.timestamp(); this.draw(); this._end = Game.timestamp(); this.updateStats(this._middle - this._start, this._end - this._middle); this.lastFrame = this._start; }, initCanvas: function() { if (this.game && this.game.initCanvas) this.game.initCanvas(this.front2d); }, update: function(dt) { this.game.update(dt); }, draw: function() { this.game.draw(this.front2d); this.drawStats(this.front2d); }, resetStats: function() { this.stats = { count: 0, fps: 0, update: 0, draw: 0, frame: 0 // update + draw }; }, updateStats: function(update, draw) { if (this.cfg.stats) { this.stats.update = Math.max(1, update); this.stats.draw = Math.max(1, draw); this.stats.frame = this.stats.update + this.stats.draw; this.stats.count = this.stats.count == this.fps ? 0 : this.stats.count + 1; this.stats.fps = Math.min(this.fps, 1000 / this.stats.frame); } }, strings: { frame: "frame: ", fps: "fps: ", update: "update: ", draw: "draw: ", ms: "ms" }, drawStats: function(ctx) { if (this.cfg.stats) { ctx.fillText(this.strings.frame + Math.round(this.stats.count), this.width - 100, this.height - 60); ctx.fillText(this.strings.fps + Math.round(this.stats.fps), this.width - 100, this.height - 50); ctx.fillText(this.strings.update + Math.round(this.stats.update) + this.strings.ms, this.width - 100, this.height - 40); ctx.fillText(this.strings.draw + Math.round(this.stats.draw) + this.strings.ms, this.width - 100, this.height - 30); } }, addEvents: function() { Game.addEvent(document, 'keydown', this.onkeydown.bind(this)); Game.addEvent(document, 'keyup', this.onkeyup.bind(this)); Game.addEvent(window, 'resize', this.onresize.bind(this)); }, onresize: function() { this.stop(); if (this.onresizeTimer) clearTimeout(this.onresizeTimer); this.onresizeTimer = setTimeout(this.onresizeend.bind(this), 50); // dont fire resize event until 50ms after user has stopped resizing (avoid flickering) }, onresizeend: function() { this.resize(); this.start(); }, resize: function() { if ((this.width != this.canvas.offsetWidth) || (this.height != this.front.offsetHeight)) { // console.log("CANVAS RESIZED " + this.front.offsetWidth + ", " + this.front.offsetHeight); this.width = this.front.width = this.front.offsetWidth; this.height = this.front.height = this.front.offsetHeight; if (this.game && this.game.onresize) this.game.onresize(this.width, this.height); this.initCanvas(); // when canvas is really resized, its state is reset so we need to re-initialize } }, onkeydown: function(ev) { if (this.game.onkeydown) return this.game.onkeydown(ev.keyCode); else if (this.cfg.keys) return this.onkey(ev.keyCode, 'down'); }, onkeyup: function(ev) { if (this.game.onkeyup) return this.game.onkeyup(ev.keyCode); else if (this.cfg.keys) return this.onkey(ev.keyCode, 'up'); }, onkey: function(keyCode, mode) { var n, k, i, state = this.game.current; // avoid same key event triggering in 2 different states by remembering current state so that even if an earlier keyhandler changes state, the later keyhandler wont kick in. for(n = 0 ; n < this.cfg.keys.length ; n++) { k = this.cfg.keys[n]; k.mode = k.mode || 'up'; if ((k.key == keyCode) || (k.keys && (k.keys.indexOf(keyCode) >= 0))) { if (!k.state || (k.state == state)) { if (k.mode == mode) { k.action.call(this.game); } } } } }, storage: function() { try { return this.localStorage = this.localStorage || window.localStorage || {}; } catch(e) { // IE localStorage throws exceptions when using non-standard port (e.g. during development) return this.localStorage = {}; } }, alert: function(msg) { this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update result = window.alert(msg); this.start(); return result; }, confirm: function(msg) { this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update result = window.confirm(msg); this.start(); return result; } //------------------------------------------------------------------------- } // Game.Runner } // Game //============================================================================= // Breakout //============================================================================= Breakout = { Defaults: { fps: 60, stats: false, score: { lives: { initial: 3, max: 5 } }, court: { xchunks: 30, ychunks: 25 }, ball: { radius: 0.3, speed: 15, labels: { 3: { text: 'pronti...', fill: '#D82800', stroke: 'black', font: 'bold 28pt arial' }, 2: { text: 'a posto..', fill: '#FC9838', stroke: 'black', font: 'bold 28pt arial' }, 1: { text: 'via!', fill: '#80D010', stroke: 'black', font: 'bold 28pt arial' } } }, paddle: { width: 6, height: 1, speed: 20 }, color: { background: 'rgba(200, 200, 200, 0.5)', foreground: 'green', border: '#222', wall: '#333', ball: 'black', paddle: 'rgb(245,111,37)', score: "#EFD279", highscore: "#AFD775" }, state: { initial: 'menu', events: [ { name: 'play', from: 'menu', to: 'game' }, { name: 'abandon', from: 'game', to: 'menu' }, { name: 'lose', from: 'game', to: 'menu' } ]}, keys: [ { keys: [Game.KEY.LEFT, Game.KEY.A], mode: 'down', action: function() { this.paddle.moveLeft(); } }, { keys: [Game.KEY.RIGHT, Game.KEY.D], mode: 'down', action: function() { this.paddle.moveRight(); } }, { keys: [Game.KEY.LEFT, Game.KEY.A], action: function() { this.paddle.stopMovingLeft(); } }, { keys: [Game.KEY.RIGHT, Game.KEY.D], action: function() { this.paddle.stopMovingRight(); } }, { keys: [Game.KEY.SPACE, Game.KEY.RETURN], state: 'menu', action: function() { this.play(); } }, { keys: [Game.KEY.SPACE, Game.KEY.RETURN], state: 'game', action: function() { this.ball.launchNow(); } }, { key: Game.KEY.ESC, state: 'game', action: function() { this.abandon(); } }, { key: Game.KEY.UP, state: 'menu', action: function() { this.nextLevel(); } }, { key: Game.KEY.DOWN, state: 'menu', action: function() { this.prevLevel(); } } ], sounds: { brick: '/sound/breakout/brick.mp3', paddle: '/sound/breakout/paddle.mp3', go: '/sound/breakout/go.mp3', levelup: '/sound/breakout/levelup.mp3', loselife: '/sound/breakout/loselife.mp3', gameover: '/sound/breakout/gameover.mp3' } }, //----------------------------------------------------------------------------- initialize: function(runner, cfg) { this.cfg = cfg; this.runner = runner; this.width = runner.width; this.height = runner.height; this.storage = runner.storage(); this.color = cfg.color; this.sound = false; this.court = Object.construct(Breakout.Court, this, cfg.court); this.paddle = Object.construct(Breakout.Paddle, this, cfg.paddle); this.ball = Object.construct(Breakout.Ball, this, cfg.ball); this.score = Object.construct(Breakout.Score, this, cfg.score); Game.loadSounds({sounds: cfg.sounds}); }, onstartup: function() { // the event that fires the initial state transition occurs when Game.Runner constructs our StateMachine this.addEvents(); this.runner.start(); // start the 60fps update/draw game loop }, addEvents: function() { Game.addEvent('prev', 'click', this.prevLevel.bind(this, false)); Game.addEvent('next', 'click', this.nextLevel.bind(this, false)); Game.addEvent('sound', 'change', this.toggleSound.bind(this, false)); Game.addEvent('instructions', 'touchstart', this.play.bind(this)); Game.addEvent(this.runner.canvas, 'touchmove', this.ontouchmove.bind(this)); Game.addEvent(document.body, 'touchmove', function(event) { event.preventDefault(); }); // prevent ipad bouncing up and down when finger scrolled }, toggleSound: function() { this.storage.sound = this.sound = !this.sound; }, update: function(dt) { this.court.update(dt); this.paddle.update(dt); this.ball.update(dt); this.score.update(dt); }, draw: function(ctx) { ctx.save(); ctx.clearRect(0, 0, this.width, this.height); ctx.fillStyle = this.color.background; ctx.fillRect(0, 0, this.width, this.height); this.court.draw(ctx); this.paddle.draw(ctx); this.ball.draw(ctx); this.score.draw(ctx); ctx.restore(); }, onresize: function(width, height) { this.width = width; this.height = height; this.court.resize(); this.paddle.reset(); this.ball.reset(); }, onmenu: function() { this.resetLevel(); this.paddle.reset(); this.ball.reset(); this.refreshDOM(); }, ongame: function() { this.refreshDOM(); this.score.reset(); this.ball.reset({launch: true}); }, onlose: function() { this.playSound('gameover'); }, onleavegame: function() { this.score.save(); this.score.resetLives(); }, onbeforeabandon: function() { return this.runner.confirm("Abandon game?") }, loseBall: function() { this.playSound('loselife'); if (this.score.loseLife()) this.lose(); else { this.ball.reset({launch: true}); } }, winLevel: function() { this.playSound('levelup'); this.score.gainLife(); this.nextLevel(true); this.ball.reset({launch: true}); }, hitBrick: function(brick) { this.playSound('brick'); this.court.remove(brick); this.score.increase(brick.score); this.ball.speed += 10 * (1 - (this.ball.speed / this.ball.maxspeed)); // decay curve - speed increases less the faster the ball is (otherwise game becomes impossible) if (this.court.empty()) this.winLevel(); }, resetLevel: function() { this.setLevel(); }, setLevel: function(level) { level = (typeof level == 'undefined') ? (this.storage.level ? parseInt(this.storage.level) : 0) : level; level = level < Breakout.Levels.length ? level : 0; this.court.reset(level); this.storage.level = this.level = level; this.refreshDOM(); }, canPrevLevel: function() { return this.is('menu') && (this.level > 0); }, canNextLevel: function() { return this.is('menu') && (this.level < (Breakout.Levels.length-1)); }, prevLevel: function(force) { if (force || this.canPrevLevel()) this.setLevel(this.level - 1); }, nextLevel: function(force) { if (force || this.canNextLevel()) this.setLevel(this.level + 1); }, initCanvas: function(ctx) { // called by Game.Runner whenever the canvas is reset (on init and on resize) ctx.fillStyle = this.color.foreground; ctx.strokeStyle = this.color.foreground; ctx.lineWidth = 1; this.score.measure(ctx); // score needs to measure itself }, refreshDOM: function() { $('instructions').className = Game.ua.hasTouch ? 'touch' : 'keyboard'; $('instructions').showIf(this.is('menu')); $('prev').toggleClassName('disabled', !this.canPrevLevel()); $('next').toggleClassName('disabled', !this.canNextLevel()); $('level').update(this.level + 1); $('sound').checked = this.sound; }, playSound: function(id) { if (soundManager && this.sound) { soundManager.play(id); } }, ontouchmove: function(ev) { if (ev.targetTouches.length == 1) { this.paddle.place(ev.targetTouches[0].pageX - this.runner.bounds.left - this.paddle.w/2); // clientX only works in ios, not on android - must use pageX - yuck } }, //============================================================================= Score: { initialize: function(game, cfg) { this.game = game; this.cfg = cfg; this.load(); this.reset(); }, reset: function() { this.set(0); this.resetLives(); }, set: function(n) { this.score = this.vscore = n; this.rerender = true; }, increase: function(n) { this.score = this.score + n; this.rerender = true; }, format: function(n) { return ("0000000" + n).slice(-7); }, load: function() { this.highscore = this.game.storage.highscore ? parseInt(this.game.storage.highscore) : 1000; }, save: function() { if (this.score > this.highscore) this.game.storage.highscore = this.highscore = this.score; }, resetLives: function() { this.setLives(this.cfg.lives.initial); }, setLives: function(n) { this.lives = n; this.rerender = true; }, gainLife: function() { this.setLives(Math.min(this.cfg.lives.max, this.lives + 1)); }, loseLife: function() { this.setLives(this.lives-1); return (this.lives == 0); }, update: function(dt) { if (this.vscore < this.score) { this.vscore = Math.min(this.score, this.vscore + 10); this.rerender = true; } }, measure: function(ctx) { this.left = this.game.court.left; this.top = this.game.court.top - this.game.court.wall.size*2; this.width = this.game.court.width; this.height = this.game.court.wall.size*2; this.scorefont = "bold " + Math.max(9, this.game.court.wall.size - 2) + "pt arial"; this.highfont = "" + Math.max(9, this.game.court.wall.size - 8) + "pt arial"; ctx.save(); ctx.font = this.scorefont; this.scorewidth = ctx.measureText(this.format(0)).width; ctx.font = this.highfont; this.highwidth = ctx.measureText("HIGH SCORE: " + this.format(0)).width; ctx.restore(); this.rerender = true; }, draw: function(ctx) { if (this.rerender) { this.canvas = Game.renderToCanvas(this.width, this.height, this.render.bind(this), this.canvas); this.rerender = false; } ctx.drawImage(this.canvas, this.left, this.top); }, render: function(ctx) { var text, width, paddle; var ishigh = this.game.is('game') && (this.score > this.highscore); ctx.textBaseline = "middle"; ctx.fillStyle = this.game.color.score; ctx.font = this.scorefont; text = this.format(this.vscore); ctx.fillText(text, 0, this.height/2); ctx.fillStyle = ishigh ? this.game.color.score : this.game.color.highscore; text = " Punteggio Massimo: " + this.format(ishigh ? this.score : this.highscore); ctx.font = this.highfont; width = ctx.measureText(text).width; ctx.fillText(text, this.width - width, this.height/2); paddle = { game: this.game, w: this.game.court.chunk * 1.5, h: this.game.court.chunk * 2/3 } ctx.translate(this.scorewidth + 20, (this.height-paddle.h) / 2); for(var n = 0 ; n < this.lives ; n++) { this.game.paddle.render.call(paddle, ctx); ctx.translate(paddle.w + 5, 0); } } }, //============================================================================= Court: { initialize: function(game, cfg) { this.game = game; this.cfg = cfg; }, reset: function(level) { var layout = Breakout.Levels[level]; var line, brick, score, c, n, x, y, nx, ny = Math.min(layout.bricks.length, this.cfg.ychunks); this.bricks = []; for(y = 0 ; y < ny ; y++) { score = (this.cfg.ychunks - y) * 5; line = layout.bricks[y] + " "; // extra space simplifies loop brick = null; nx = Math.min(line.length, this.cfg.xchunks + 1); for(x = 0 ; x < nx ; x++) { c = line[x]; if (brick && (brick.c == c)) { brick.pos.x2 = x; } else if (brick && (brick.c != c)) { this.bricks.push(brick); brick = null; } if (!brick && (c != ' ')) brick = { isbrick: true, hit: false, c: c, pos: { x1: x, x2: x, y: y }, score: score, color: layout.colors[c.toLowerCase()] }; } } this.numbricks = this.bricks.length; this.numhits = 0; this.resize(); }, resize: function() { this.chunk = Math.floor(Math.min(this.game.width, this.game.height) / (Math.max(this.cfg.xchunks, this.cfg.ychunks) + 4)); // room for court plus 2 chunk wall either side this.width = this.cfg.xchunks * this.chunk; this.height = this.cfg.ychunks * this.chunk; this.left = Math.floor((this.game.width - this.width) / 2); this.top = Math.floor((this.game.height - this.height) / 2); this.right = this.left + this.width; this.bottom = this.top + this.height; this.wall = {} this.wall.size = this.chunk; this.wall.top = Game.Math.bound({x: this.left - this.wall.size, y: this.top - this.wall.size*2, w: this.width + this.wall.size*2, h: this.wall.size*2 }); this.wall.left = Game.Math.bound({x: this.left - this.wall.size, y: this.top - this.wall.size*2, w: this.wall.size, h: this.wall.size*2 + this.height }); this.wall.right = Game.Math.bound({x: this.right, y: this.top - this.wall.size*2, w: this.wall.size, h: this.wall.size*2 + this.height }); for(n = 0 ; n < this.numbricks ; n++) { brick = this.bricks[n]; brick.x = this.left + (brick.pos.x1 * this.chunk); brick.y = this.top + (brick.pos.y * this.chunk); brick.w = (brick.pos.x2 - brick.pos.x1 + 1) * this.chunk; brick.h = this.chunk; Game.Math.bound(brick); } this.rerender = true; }, update: function(dt) { }, draw: function(ctx) { if (this.rerender) { this.canvas = Game.renderToCanvas(this.game.width, this.game.height, this.render.bind(this), this.canvas); this.rerender = false; } ctx.drawImage(this.canvas, 0, 0); }, render: function(ctx) { var n, brick; ctx.translate(0.5, 0.5); // crisp 1px lines for the brick borders ctx.strokeStyle = this.game.color.border; ctx.lineWidth = 1; for(n = 0 ; n < this.numbricks ; n++) { brick = this.bricks[n]; if (!brick.hit) { ctx.fillStyle = brick.color; ctx.fillRect(brick.x, brick.y, brick.w, brick.h); ctx.strokeRect(brick.x, brick.y, brick.w, brick.h); } } ctx.fillStyle = this.game.color.wall; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(this.wall.top.left, this.wall.top.top); ctx.lineTo(this.wall.top.right, this.wall.top.top); ctx.lineTo(this.wall.top.right, this.wall.right.bottom); ctx.lineTo(this.wall.right.left, this.wall.right.bottom); ctx.lineTo(this.wall.right.left, this.wall.top.bottom); ctx.lineTo(this.wall.left.right, this.wall.top.bottom); ctx.lineTo(this.wall.left.right, this.wall.left.bottom); ctx.lineTo(this.wall.left.left, this.wall.left.bottom); ctx.lineTo(this.wall.top.left, this.wall.top.top); ctx.fill(); ctx.stroke(); ctx.closePath(); }, remove: function(brick) { brick.hit = true; this.numhits++; this.rerender = true; }, empty: function() { return (this.numhits == this.numbricks); } }, //============================================================================= Ball: { initialize: function(game, cfg) { this.game = game; this.cfg = cfg; }, reset: function(options) { this.radius = this.cfg.radius * this.game.court.chunk; this.speed = this.cfg.speed * this.game.court.chunk; this.maxspeed = this.speed * 1.5; this.color = this.game.color.ball; this.moveToPaddle(); this.setdir(0, 0); this.clearLaunch(); this.hitTargets = [ this.game.paddle, this.game.court.wall.top, this.game.court.wall.left, this.game.court.wall.right, ].concat(this.game.court.bricks); if (options && options.launch) this.launch(); }, moveToPaddle: function() { this.setpos(this.game.paddle.left + (this.game.paddle.w/2), this.game.court.bottom - this.game.paddle.h - this.radius); }, setpos: function(x, y) { this.x = x; this.y = y; Game.Math.bound(this); }, setdir: function(dx, dy) { var dir = Game.Math.normalize({ x: dx, y: dy }); this.dx = dir.x; this.dy = dir.y; this.moving = dir.m != 0; }, launch: function() { if (!this.moving || this.countdown) { this.countdown = (typeof this.countdown == 'undefined') || (this.countdown == null) ? 3 : this.countdown - 1; if (this.countdown > 0) { this.label = this.launchLabel(this.countdown); this.delayTimer = setTimeout(this.launch.bind(this), 1000); if (this.countdown == 1) this.setdir(1, -1); // launch on 'go' } else { this.clearLaunch(); } } }, launchNow: function() { // <space> key can override countdown launch if (!this.moving) { this.clearLaunch(); this.setdir(1, -1); } }, launchLabel: function(count) { var label = this.cfg.labels[count]; var ctx = this.game.runner.front2d; // dodgy getting the context this way, should probably have a Game.Runner.ctx() method ? ctx.save(); ctx.font = label.font; ctx.fillStyle = label.fill; ctx.strokeStyle = label.stroke; ctx.lineWidth = 0.5; var width = ctx.measureText(label.text).width; ctx.restore(); label.x = this.game.court.left + (this.game.court.width - width)/2; label.y = this.game.paddle.top - 60; return label; }, clearLaunch: function() { if (this.delayTimer) { clearTimeout(this.delayTimer); this.delayTimer = this.label = this.countdown = null; } }, update: function(dt) { if (!this.moving) return this.moveToPaddle(); var p2 = Game.Math.move(this.x, this.y, this.dx * this.speed, this.dy * this.speed, dt); var mCurrent, mClosest = Infinity, point, item, closest = null; for (var n = 0 ; n < this.hitTargets.length ; n++) { item = this.hitTargets[n]; if (!item.hit) { point = Game.Math.ballIntercept(this, item, p2.nx, p2.ny); if (point) { mCurrent = Game.Math.magnitude(point.x - this.x, point.y - this.y); if (mCurrent < mClosest) { mClosest = mCurrent; closest = {item: item, point: point}; } } } } if (closest) { if (closest.item.isbrick) { this.game.hitBrick(closest.item); if (!this.moving) // if hitBrick caused game to end we dont want to continue updating our state return; } if ((closest.item == this.game.paddle) && (closest.point.d == 'top')) { p2.dx = this.speed * (closest.point.x - (this.game.paddle.left + this.game.paddle.w/2)) / (this.game.paddle.w/2); this.game.playSound('paddle'); } this.setpos(closest.point.x, closest.point.y); switch(closest.point.d) { case 'left': case 'right': this.setdir(-p2.dx, p2.dy); break; case 'top': case 'bottom': this.setdir(p2.dx, -p2.dy); break; } var udt = dt * (mClosest / Game.Math.magnitude(p2.nx, p2.ny)); // how far along did we get before intercept ? return this.update(dt - udt); // so we can update for time remaining } if ((p2.x < 0) || (p2.y < 0) || (p2.x > this.game.width) || (p2.y > this.game.height)) { this.game.loseBall(); } else { this.setpos(p2.x, p2.y); this.setdir(p2.dx, p2.dy); } }, draw: function(ctx) { ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Game.THREESIXTY, true); ctx.fill(); ctx.stroke(); ctx.closePath(); if (this.label) { ctx.font = this.label.font; ctx.fillStyle = this.label.fill; ctx.strokeStyle = this.label.stroke; ctx.lineWidth = 0.5; ctx.fillText(this.label.text, this.label.x, this.label.y); ctx.strokeText(this.label.text, this.label.x, this.label.y); } } }, //============================================================================= Paddle: { initialize: function(game, cfg) { this.game = game; this.cfg = cfg; }, reset: function() { this.speed = this.cfg.speed * this.game.court.chunk; this.w = this.cfg.width * this.game.court.chunk; this.h = this.cfg.height * this.game.court.chunk; this.minX = this.game.court.left; this.maxX = this.game.court.right - this.w; this.setpos(Game.random(this.minX, this.maxX), this.game.court.bottom - this.h); this.setdir(0); this.rerender = true; }, setpos: function(x, y) { this.x = x; this.y = y; Game.Math.bound(this); }, setdir: function(dx) { this.dleft = (dx < 0 ? -dx : 0); this.dright = (dx > 0 ? dx : 0); }, place: function(x) { this.setpos(Math.min(this.maxX, Math.max(this.minX, x)), this.y); }, update: function(dt) { var amount = this.dright - this.dleft; if (amount != 0) this.place(this.x + (amount * dt * this.speed)); }, draw: function(ctx) { if (this.rerender) { this.canvas = Game.renderToCanvas(this.w, this.h, this.render.bind(this)); this.rerender = false; } ctx.drawImage(this.canvas, this.x, this.y); }, render: function(ctx) { var gradient = ctx.createLinearGradient(0, this.h, 0, 0); gradient.addColorStop(0.36, 'rgb(245,111,37)'); gradient.addColorStop(0.68, 'rgb(255,145,63)'); gradient.addColorStop(0.84, 'rgb(255,174,95)'); var r = this.h/2; ctx.fillStyle = gradient; ctx.strokeStyle = this.game.color.border; ctx.beginPath(); ctx.moveTo(r, 0); ctx.lineTo(this.w - r, 0); ctx.arcTo(this.w, 0, this.w, r, r); ctx.lineTo(this.w, this.h - r); ctx.arcTo(this.w, this.h, this.w - r, this.h, r); ctx.lineTo(r, this.h); ctx.arcTo(0, this.h, 0, this.h - r, r); ctx.lineTo(0, r); ctx.arcTo(0, 0, r, 0, r); ctx.fill(); ctx.stroke(); ctx.closePath(); }, moveLeft: function() { this.dleft = 1; }, moveRight: function() { this.dright = 1; }, stopMovingLeft: function() { this.dleft = 0; }, stopMovingRight: function() { this.dright = 0; } } //============================================================================= }; // Breakout Breakout.Colors = { arkanoid: { w: "#FCFCFC", // white o: "#FC7460", // orange l: "#3CBCFC", // light blue g: "#80D010", // green r: "#D82800", // red b: "#0070EC", // blue p: "#FC74B4", // pink y: "#FC9838", // yellow s: "#BCBCBC", // silver d: "#F0BC3C" // gold }, pastel: { y: "#FFF7A5", // yellow p: "#FFA5E0", // pink b: "#A5B3FF", // blue g: "#BFFFA5", // green o: "#FFCBA5" // orange }, vintage: { a: "#EFD279", // yellow b: "#95CBE9", // light blue c: "#024769", // dark blue d: "#AFD775", // light green e: "#2C5700", // grass f: "#DE9D7F", // red g: "#7F9DDE", // purple h: "#00572C", // dark green i: "#75D7AF", // mint j: "#694702", // brown k: "#E9CB95", // peach l: "#79D2EF" // blue }, liquidplanner: { a: '#62C4E7', // light blue b: '#00A5DE', // dark blue x: '#969699', // light gray y: '#7B797E' // dark gray }, }; Breakout.Levels = [ { colors: Breakout.Colors.pastel, bricks: [ "", "", "", "", "", "", "yyyyyYYYYYyyyyyYYYYYyyyyyYYYYY", "pppppPPPPPpppppPPPPPpppppPPPPP", "bbbbbBBBBBbbbbbBBBBBbbbbbBBBBB", "gggggGGGGGgggggGGGGGgggggGGGGG", "oooooOOOOOoooooOOOOOoooooOOOOO" ] }, { colors: Breakout.Colors.arkanoid, bricks: [ "", "", " yy yy ", " yy yy ", " yy yy ", " ssSSssSSss ", " ssSSssSSss ", " SSsswwsswwssSS ", " SSsswwsswwssSS ", " ssSSssSSssSSssSSss ", " ssSSssSSssSSssSSss ", " ss ssSSssSSss ss ", " ss ss ss ss ", " ss ss ss ss ", " ss ss ", " ss ss ", ] }, { colors: Breakout.Colors.arkanoid, bricks: [ "", "oo", "ooll", "oollgg", "oollggbb", "oollggbbrr", "oollggbbrroo", "oollggbbrrooll", "oollggbbrroollgg", "oollggbbrroollggbb", "oollggbbrroollggbbrr", "oollggbbrroollggbbrroo", "oollggbbrroollggbbrrooll", "oollggbbrroollggbbrroollgg", "oollggbbrroollggbbrroollggbb", "ssSSssSSssSSssSSssSSssSSssSSrr" ] }, { colors: Breakout.Colors.arkanoid, bricks: [ "", "", " ss ", " bbBBssggGG ", " BBbbWWwwWWGGgg ", " bbBBwwWWwwWWwwggGG ", " bbBBwwWWwwWWwwggGG ", " bbBBwwWWwwWWwwggGG ", " ss ss ss ss ss ", " ss ", " ss ", " oo oo ", " ooOOoo ", " OO " ] }, { colors: Breakout.Colors.pastel, bricks: [ "", "", " yyYYyyYYyyYY YYyyYYyyYYyy ", " bbBBbbBBbbBB BBbbBBbbBBbb ", " ggGGggGGggGG GGggGGggGGgg ", " ooOOooOOooOO OOooOOooOOoo ", "", "", " yyYYyyYYyyYY YYyyYYyyYYyy ", " bbBBbbBBbbBB BBbbBBbbBBbb ", " ggGGggGGggGG GGggGGggGGgg ", " ooOOooOOooOO OOooOOooOOoo ", "", "", " yyYYyyYYyyYY YYyyYYyyYYyy ", " bbBBbbBBbbBB BBbbBBbbBBbb ", " ggGGggGGggGG GGggGGggGGgg ", " ooOOooOOooOO OOooOOooOOoo " ] }, { colors: Breakout.Colors.vintage, bricks: [ "", "", "", " AAaaAAaaAAaaAAaaAAaaAAaa ", " BBbbBBbbBBbbBBbbBBbbBB ", " CCccCCccCCccCCccCCcc ", " DDddDDddDDddDDddDD ", " EEeeEEeeEEeeEEee ", " FFffFFffFFffFF ", " GGggGGggGGgg ", " HHhhHHhhHH ", " IIiiIIii ", " JJjjJJ ", " KKkk ", " LL " ] }, { colors: Breakout.Colors.vintage, bricks: [ "", "", " aabbccddeeffggFFEEDDCCBBAA ", " aabbccddeeffFFEEDDCCBBAA ", " aabbccddeeffEEDDCCBBAA ", " aabbccddeeEEDDCCBBAA ", " aabbccddeeDDCCBBAA ", " aabbccddDDCCBBAA ", " aabbccddCCBBAA ", " aabbccCCBBAA ", " aabbccBBAA ", " hh aabbCCAA hh ", " hhHH aabbAA hhHH ", " hhiiHH aaAA hhiiHH ", " hhiiIIHH aa hhiiIIHH ", " hhiijjIIHH hhiijjIIHH ", " hhiijjJJIIHH hhiijjJJIIHH " ] }, { colors: Breakout.Colors.pastel, bricks: [ " ", " ", " bbBBbbBBbbBBbbBBbbBBbbBBbb ", " ooggGGggGGggGGggGGggGGggoo ", " ooggGGggGGggGGggGGggGGggoo ", " ooppPPppPPppPPppPPppPPppoo ", " ooppPPppPPppBBppPPppPPppoo ", " ooppPPppPPbbBBbbPPppPPppoo ", " ooppPPppBBbbOObbBBppPPppoo ", " ooppPPbbBBooOOooBBbbPPppoo ", " ooppBBbbOOooYYooOObbBBppoo ", " oobbBBOOooyyYYyyooOOBBbboo ", " oobbooOOYYyyYYyyYYOOoobboo ", " ooOOooyyYYyyYYyyYYyyooOOoo ", " ooOOYYyyYYyyYYyyYYyyYYOOoo ", " ooyyYYyyYYyyYYyyYYyyYYyyoo ", " ooyyYYyyYYyyYYyyYYyyYYyyoo ", " bbBBbbBBbbBBbbBBbbBBbbBBbb " ] }, { colors: { b: '#111111', // black, w: '#EEEEEE', // white, c: '#EC7150', // cherry, s: '#B33A2F' // shadow, }, bricks: [ "", " bBb ", " BcCcB ", " bCwCcsb b ", " bCcCcsb b ", " BcCsB B ", " BbBsSsBbB bBb ", " bcCcbBbcCcb BcCcB ", " bcwcCsbcwcCsb bCwCcsb b ", " bcCcCsbcCcCsb bCcCcsb b ", " bcCcsSbcCcsSb BcCsB B ", " bsSsb bsSsb BbBsSsBbB ", " bBb bBb bcCcbBbcCcb ", " bcwcCsbcwcCsb ", " bcCcCsbcCcCsb ", " bcCcsSbcCcsSb ", " bsSsb bsSsb ", " bBb bBb ", " ", " ", " ", " ", ] }, { colors: { r: '#D80000', // red b: '#706800', // brown o: '#F8AB00', // orange f: '#F83800', // fire w: '#FFFFFF', // white e: '#FFE0A8' // beige }, bricks: [ "", " rRrRr ", " RrRrRrRrR ", " BbBoObo ", " boboOoboOo F f f ", " bobBoOoboOo f e ", " bBoOoObBbB F f e ", " oOoOoOo Ff E ", " bBrbBb E f fF F f ", " bBbrbBrbBb FfFfFf F ", " bBbBrRrRbBbB fFeFeFfFf ", " oObrorRorboO FfEeEeEfF ", " oOorRrRrRoOo FeEeWwEeFf ", " oOrRrRrRrRoO fFeFwWfEeFf ", " rRr RrR fFeFwWfEeFf ", " bBb bBb fFeEwWeEeFf ", " bBbB bBbB fFfEeEeEfF ", " FfFfFfFfF ", " FfFfF " ] } ];