(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= minRow && r < maxRow && c >= minCol && c < maxCol; if (animateSpec.after && viewable) { //Bouncing the squares. var offsetBounce = getBounceOffset(animateSpec.frame); y -= offsetBounce; shadowColor = animateSpec.after.shadowColor; baseColor = animateSpec.after.lightBaseColor.deriveLumination(-(offsetBounce / DROP_HEIGHT) * .1); ctx.fillStyle = shadowColor.rgbString(); ctx.fillRect(x, y + CELL_WIDTH, CELL_WIDTH, SHADOW_OFFSET); ctx.fillStyle = baseColor.rgbString(); ctx.fillRect(x, y, CELL_WIDTH + 1, CELL_WIDTH + 1); } animateSpec.frame++; if (animateSpec.frame >= ANIMATE_FRAMES) animateGrid.set(r, c, null); } } } } function paintUIBar(ctx) { //UI Bar background ctx.fillStyle = "#24422c"; ctx.fillRect(0, 0, canvasWidth, BAR_HEIGHT); var barOffset; ctx.fillStyle = "white"; ctx.font = "24px Changa"; barOffset = (user && user.name) ? (ctx.measureText(user.name).width + 20) : 0; ctx.fillText(user ? user.name : "", 5, CELL_WIDTH - 5); //Draw filled bar. ctx.fillStyle = "rgba(180, 180, 180, .3)"; ctx.fillRect(barOffset, 0, BAR_WIDTH, BAR_HEIGHT); var userPortions = portionsRolling[user.num] ? portionsRolling[user.num].lag : 0; var barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * userPortions + MIN_BAR_WIDTH); ctx.fillStyle = user ? user.baseColor.rgbString() : ""; ctx.fillRect(barOffset, 0, barSize, CELL_WIDTH); ctx.fillStyle = user ? user.shadowColor.rgbString() : ""; ctx.fillRect(barOffset, CELL_WIDTH, barSize, SHADOW_OFFSET); //TODO: dont reset kill count and zoom when we request frames. //Percentage ctx.fillStyle = "white"; ctx.font = "18px Changa"; ctx.fillText((userPortions * 100).toFixed(3) + "%", 5 + barOffset, CELL_WIDTH - 5); //Number of kills var killsText = "Kills: " + client.kills; var killsOffset = 20 + BAR_WIDTH + barOffset; ctx.fillText(killsText, killsOffset, CELL_WIDTH - 5); //Calcuate rank var sorted = []; client.getPlayers().forEach(function(val) { sorted.push({player: val, portion: playerPortion[val.num]}); }); sorted.sort(function(a, b) { if (a.portion === b.portion) return a.player.num - b.player.num; else return b.portion - a.portion; }); var rank = sorted.findIndex(function(val) {return val.player === user}); ctx.fillText("Rank: " + (rank === -1 ? "--" : rank + 1) + " of " + sorted.length, ctx.measureText(killsText).width + killsOffset + 20, CELL_WIDTH - 5); //Rolling the leaderboard bars. if (sorted.length > 0) { var maxPortion = sorted[0].portion; client.getPlayers().forEach(function(player) { var rolling = barProportionRolling[player.num]; rolling.value = playerPortion[player.num] / maxPortion; rolling.update(); }); } //Show leaderboard. var leaderboardNum = Math.min(5, sorted.length); for (var i = 0; i < leaderboardNum; i++) { var player = sorted[i].player; var name = player.name || "Unnamed"; var portion = barProportionRolling[player.num].lag; var nameWidth = ctx.measureText(name).width; barSize = Math.ceil((BAR_WIDTH - MIN_BAR_WIDTH) * portion + MIN_BAR_WIDTH); var barX = canvasWidth - barSize; var barY = BAR_HEIGHT * (i + 1); var offset = i == 0 ? 10 : 0; ctx.fillStyle = 'rgba(10, 10, 10, .3)'; ctx.fillRect(barX - 10, barY + 10 - offset, barSize + 10, BAR_HEIGHT + offset); ctx.fillStyle = player.baseColor.rgbString(); ctx.fillRect(barX, barY, barSize, CELL_WIDTH); ctx.fillStyle = player.shadowColor.rgbString(); ctx.fillRect(barX, barY + CELL_WIDTH, barSize, SHADOW_OFFSET); ctx.fillStyle = "black"; ctx.fillText(name, barX - nameWidth - 15, barY + 27); var percentage = (portionsRolling[player.num].lag * 100).toFixed(3) + "%"; ctx.fillStyle = "white"; ctx.fillText(percentage, barX + 5, barY + CELL_WIDTH - 5); } } function paint(ctx) { ctx.fillStyle = '#e2ebf3'; //'whitesmoke'; ctx.fillRect(0, 0, canvasWidth, canvasHeight); //Move grid to viewport as said with the offsets, below the stats ctx.save(); ctx.translate(0, BAR_HEIGHT); ctx.beginPath(); ctx.rect(0, 0, gameWidth, gameHeight); ctx.clip(); //Zoom in/out based on player stats. ctx.scale(zoom, zoom); ctx.translate(-offset[0] + BORDER_WIDTH, -offset[1] + BORDER_WIDTH); paintGrid(ctx); client.getPlayers().forEach(function (p) { var fr = p.waitLag; if (fr < ANIMATE_FRAMES) p.render(ctx, fr / ANIMATE_FRAMES); else p.render(ctx); }); //Reset transform to paint fixed UI elements ctx.restore(); paintUIBar(ctx); if ((!user || user.dead) && !showedDead) { showedDead = true; console.log("You died!"); //return; } } function paintDoubleBuff() { paint(offctx); ctx.drawImage(offscreenCanvas, 0, 0); } function update() { updateSize(); //Change grid offsets. for (var i = 0; i <= 1; i++) { if (animateTo[i] !== offset[i]) { if (client.allowAnimation) { var delta = animateTo[i] - offset[i]; var dir = Math.sign(delta); var mag = Math.min(SPEED, Math.abs(delta)); offset[i] += dir * mag; } else offset[i] = animateTo[i]; } } //Calculate player portions. client.getPlayers().forEach(function(player) { var roll = portionsRolling[player.num]; roll.value = playerPortion[player.num] / GRID_SIZE / GRID_SIZE; roll.update(); }); //Zoom goes from 1 to .5, decreasing as portion goes up. TODO: maybe can modify this? if (portionsRolling[user.num]) zoom = 1 / (portionsRolling[user.num].lag + 1); //TODO: animate player is dead. (maybe explosion?), and tail rewinds itself. if (user) centerOnPlayer(user, animateTo); } //Helper methods. function centerOnPlayer(player, pos) { var xOff = Math.floor(player.posX - (gameWidth / zoom - CELL_WIDTH) / 2); var yOff = Math.floor(player.posY - (gameHeight / zoom - CELL_WIDTH) / 2); var gridWidth = grid.size * CELL_WIDTH + BORDER_WIDTH * 2; pos[0] = xOff; //Math.max(Math.min(xOff, gridWidth + (BAR_WIDTH + 100) / zoom - gameWidth / zoom), 0); pos[1] = yOff; //Math.max(Math.min(yOff, gridWidth - gameHeight / zoom), 0); } function getBounceOffset(frame) { var offsetBounce = ANIMATE_FRAMES; var bounceNum = BOUNCE_FRAMES.length - 1; while (bounceNum >= 0 && frame < offsetBounce - BOUNCE_FRAMES[bounceNum]) { offsetBounce -= BOUNCE_FRAMES[bounceNum]; bounceNum--; } if (bounceNum === -1) { return (offsetBounce - frame) * DROP_SPEED; } else { offsetBounce -= BOUNCE_FRAMES[bounceNum]; frame = frame - offsetBounce; var midFrame = BOUNCE_FRAMES[bounceNum] / 2; if (frame >= midFrame) return (BOUNCE_FRAMES[bounceNum] - frame) * DROP_SPEED; else return frame * DROP_SPEED; } } function showStats() { //TODO: Show score stats. $("#begin").removeClass("hidden"); $("#begin").animate({ opacity: .9999 }, 1000, function() { $("#stats").addClass("hidden").css("opacity", 0); }); } module.exports = exports = { addPlayer: function(player) { playerPortion[player.num] = 0; portionsRolling[player.num] = new Rolling(9 / GRID_SIZE / GRID_SIZE, ANIMATE_FRAMES); barProportionRolling[player.num] = new Rolling(0, ANIMATE_FRAMES); }, disconnect: function() { //Show score stats. $("#stats").removeClass("hidden"); $("#stats").animate({ opacity: .9999 }, 3000, function() { showStats(); //Then fade back into the login screen. }); }, removePlayer: function(player) { delete playerPortion[player.num]; delete portionsRolling[player.num]; delete barProportionRolling[player.num]; }, setUser: function(player) { user = player; centerOnPlayer(user, offset); }, reset: reset, updateGrid: function(row, col, before, after) { //Keep track of areas. if (before) playerPortion[before.num]--; if (after) playerPortion[after.num]++; //Queue animation if (before === after || !client.allowAnimation) return; animateGrid.set(row, col, { before: before, after: after, frame: 0 }); }, paint: paintDoubleBuff, update: update }; },{"../client":3,"../game-core":9,"./rolling":1}],3:[function(require,module,exports){ var core = require('./game-core'); var Player = core.Player; var io = require('socket.io-client'); var GRID_SIZE = core.GRID_SIZE; var CELL_WIDTH = core.CELL_WIDTH; var running = false; var user, socket, frame; var players, allPlayers; var kills; var timeout = undefined; var dirty = false; var deadFrames = 0; var requesting = -1; //frame that we are requesting at. var frameCache = []; //Frames after our request. var allowAnimation = true; var grid = new core.Grid(core.GRID_SIZE, function(row, col, before, after) { invokeRenderer('updateGrid', [row, col, before, after]); }); /** * Provides requestAnimationFrame in a cross browser way. (edited so that this is also compatible with node.) * @author paulirish / http://paulirish.com/ */ // window.requestAnimationFrame = function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { // window.setTimeout( callback, 1000 / 60 ); // }; var hasWindow; try { window.document; hasWindow = true; } catch (e) { hasWindow = false; } var requestAnimationFrame; if ( !requestAnimationFrame ) { requestAnimationFrame = ( function() { if (hasWindow) { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { setTimeout( callback, 1000 / 60 ); }; } else { return function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { setTimeout( callback, 1000 / 60 ); }; } })(); } //Public API function connectGame(url, name, callback) { if (running) return; //Prevent multiple runs. running = true; user = null; deadFrames = 0; //Socket connection. io.j = []; io.sockets = []; socket = io(url, { 'forceNew': true, upgrade: false, transports: ['websocket'] }); socket.on('connect', function(){ console.info('Connected to server.'); }); socket.on('game', function(data) { if (timeout != undefined) clearTimeout(timeout); //Initialize game. //TODO: display data.gameid --- game id # frame = data.frame; reset(); //Load players. data.players.forEach(function(p) { var pl = new Player(grid, p); addPlayer(pl); }); user = allPlayers[data.num]; if (!user) throw new Error(); setUser(user); //Load grid. var gridData = new Uint8Array(data.grid); for (var r = 0; r < grid.size; r++) for (var c = 0; c < grid.size; c++) { var ind = gridData[r * grid.size + c] - 1; grid.set(r, c, ind === -1 ? null : players[ind]); } invokeRenderer('paint', []); frame = data.frame; if (requesting !== -1) { //Update those cache frames after we updated game. var minFrame = requesting; requesting = -1; while (frameCache.length > frame - minFrame) processFrame(frameCache[frame - minFrame]); frameCache = []; } }); socket.on('notifyFrame', processFrame); socket.on('dead', function() { socket.disconnect(); //In case we didn't get the disconnect call. }); socket.on('disconnect', function(){ if (!user) return; console.info('Server has disconnected. Creating new game.'); socket.disconnect(); user.die(); dirty = true; paintLoop(); running = false; invokeRenderer('disconnect', []); }); socket.emit('hello', { name: name, type: 0, //Free-for-all gameid: -1 //Requested game-id, or -1 for anyone. }, function(success, msg) { if (success) console.info('Connected to game!'); else { console.error('Unable to connect to game: ' + msg); running = false; } if (callback) callback(success, msg); }); } function changeHeading(newHeading) { if (!user || user.dead) return; if (newHeading === user.currentHeading || ((newHeading % 2 === 0) ^ (user.currentHeading % 2 === 0))) { //user.heading = newHeading; if (socket) { socket.emit('frame', { frame: frame, heading: newHeading }, function(success, msg) { if (!success) console.error(msg); }); } } } function getUser() { return user; } function getOthers() { var ret = []; for (var p of players) { if (p !== user) { ret.push(p); } } return ret; } function getPlayers() { return players.slice(); } //Private API function addPlayer(player) { if (allPlayers[player.num]) return; //Already added. allPlayers[player.num] = players[players.length] = player; invokeRenderer('addPlayer', [player]); return players.length - 1; } function invokeRenderer(name, args) { var renderer = exports.renderer; if (renderer && typeof renderer[name] === 'function') renderer[name].apply(exports, args); } function processFrame(data) { if (timeout != undefined) clearTimeout(timeout); if (requesting !== -1 && requesting < data.frame) { frameCache.push(data); return; } if (data.frame - 1 !== frame) { console.error('Frames don\'t match up!'); socket.emit('requestFrame'); //Restore data. requesting = data.frame; frameCache.push(data); return; } frame++; if (data.newPlayers) { data.newPlayers.forEach(function(p) { if (p.num === user.num) return; var pl = new Player(grid, p); addPlayer(pl); core.initPlayer(grid, pl); }); } var found = new Array(players.length); data.moves.forEach(function(val, i) { var player = allPlayers[val.num]; if (!player) return; if (val.left) player.die(); found[i] = true; player.heading = val.heading; }); for (var i = 0; i < players.length; i++) { //Implicitly leaving game. if (!found[i]) { var player = players[i]; player && player.die(); } } update(); var locs = {}; for (var i = 0; i < players.length; i++) { var p = players[i]; locs[p.num] = [p.posX, p.posY, p.waitLag]; } /* socket.emit('verify', { frame: frame, locs: locs }, function(frame, success, adviceFix, msg) { if (!success && requesting === -1) { console.error(frame + ': ' + msg); if (adviceFix) socket.emit('requestFrame'); } }.bind(this, frame)); */ dirty = true; requestAnimationFrame(function() { paintLoop(); }); timeout = setTimeout(function() { console.warn('Server has timed-out. Disconnecting.'); socket.disconnect(); }, 3000); } function paintLoop() { if (!dirty) return; invokeRenderer('paint', []); dirty = false; if (user && user.dead) { if (timeout) clearTimeout(timeout); if (deadFrames === 60) //One second of frame { var before =allowAnimation; allowAnimation = false; update(); invokeRenderer('paint', []); allowAnimation = before; user = null; deadFrames = 0; return; } socket.disconnect(); deadFrames++; dirty = true; update(); requestAnimationFrame(paintLoop); } } function reset() { user = null; grid.reset(); players = []; allPlayers = []; kills = 0; invokeRenderer('reset'); } function setUser(player) { user = player; invokeRenderer('setUser', [player]); } function update() { var dead = []; core.updateFrame(grid, players, dead, function addKill(killer, other) { if (players[killer] === user && killer !== other) kills++; }); dead.forEach(function(val) { console.log((val.name || 'Unnamed') + ' is dead'); delete allPlayers[val.num]; invokeRenderer('removePlayer', [val]); }); invokeRenderer('update', [frame]); } //Export stuff var funcs = [connectGame, changeHeading, getOthers, getPlayers, getUser]; funcs.forEach(function (f) { exports[f.name] = f; }); exports.renderer = null; Object.defineProperties(exports, { allowAnimation: { get: function() { return allowAnimation; }, set: function(val) { allowAnimation = !!val; }, enumerable: true }, grid: { get: function() { return grid; }, enumerable: true }, kills: { get: function() { return kills; }, enumerable: true } }); },{"./game-core":9,"socket.io-client":44}],4:[function(require,module,exports){ /* global $ */ var client = require("./client"); var core = require("./game-core"); var io = require('socket.io-client'); var GRID_SIZE = core.GRID_SIZE; var CELL_WIDTH = core.CELL_WIDTH; client.allowAnimation = true; client.renderer = require("./client-modes/user-mode"); /** * Provides requestAnimationFrame in a cross browser way. (edited so that this is also compatible with node.) * @author paulirish / http://paulirish.com/ */ // window.requestAnimationFrame = function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { // window.setTimeout( callback, 1000 / 60 ); // }; var hasWindow; try { window.document; hasWindow = true; } catch (e) { hasWindow = false; } var requestAnimationFrame; if ( !requestAnimationFrame ) { requestAnimationFrame = ( function() { if (hasWindow) { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { setTimeout( callback, 1000 / 60 ); }; } else { return function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) { setTimeout( callback, 1000 / 60 ); }; } })(); } function run() { client.connectGame('//' + window.location.hostname + ':8081', $('#name').val(), function(success, msg) { if (success) { $("#begin").addClass("hidden"); $("#begin").animate({ opacity: 0 }, 1000); } else { var error = $("#error"); error.text(msg); } }); } $(function() { var error = $("#error"); if (!window.WebSocket) { error.text("Your browser does not support WebSockets!"); return; } error.text("Loading..."); //TODO: show loading screen. var success = false; var socket = io('http://' + window.location.hostname + ':8081', { 'forceNew': true, upgrade: false, transports: ['websocket'] }); socket.on('connect_error', function() { if (!success) error.text("Cannot connect with server. This probably is due to misconfigured proxy server. (Try using a different browser)"); }); socket.emit("checkConn", function() { success = true; socket.disconnect(); }); setTimeout(function() { if (!success) error.text("Cannot connect with server. This probably is due to misconfigured proxy server. (Try using a different browser)"); else { error.text(""); $("input").keypress(function(evt) { if (evt.which === 13) requestAnimationFrame(run); }); $("button").click(function(evt) { requestAnimationFrame(run); }); } }, 2000); }); //Event listeners $(document).keydown(function(e) { var newHeading = -1; switch (e.which) { case 37: newHeading = 3; break; //LEFT case 38: newHeading = 0; break; //UP case 39: newHeading = 1; break; //RIGHT case 40: newHeading = 2; break; //DOWN default: return; //exit handler for other keys. } client.changeHeading(newHeading); e.preventDefault(); }); },{"./client":3,"./client-modes/user-mode":2,"./game-core":9,"socket.io-client":44}],5:[function(require,module,exports){ function Color(h, s, l, a) { verifyRange(h, s, l); if (a === undefined) a = 1; else verifyRange(a); Object.defineProperties(this, { "hue": {value: h, enumerable: true}, "sat": {value: s, enumerable: true}, "lum": {value: l, enumerable: true}, "alpha": {value: a, enumerable: true}, }); } Color.fromData = function(data) { return new Color(data.hue, data.sat, data.lum, data.alpha); }; function verifyRange() { for (var i = 0; i < arguments.length; i++) { if (arguments[i] < 0 || arguments[i] > 1) throw new RangeError("H, S, L, and A parameters must be between the range [0, 1]"); } } Color.prototype.interpolateToString = function(color, amount) { var rgbThis = hslToRgb(this.hue, this.sat, this.lum); var rgbThat = hslToRgb(color.hue, color.sat, color.lum); var rgb = []; for (var i = 0; i < 3; i++) rgb[i] = Math.floor((rgbThat[i] - rgbThis[i]) * amount + rgbThis[i]); return {rgbString: function() {return 'rgb(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ')'}}; } Color.prototype.deriveLumination = function(amount) { var lum = this.lum + amount; lum = Math.min(Math.max(lum, 0), 1); return new Color(this.hue, this.sat, lum, this.alpha); }; Color.prototype.deriveHue = function(amount) { var hue = this.hue - amount; return new Color(hue - Math.floor(hue), this.sat, this.lum, this.alpha); }; Color.prototype.deriveSaturation = function(amount) { var sat = this.sat + amount; sat = Math.min(Math.max(sat, 0), 1); return new Color(this.hue, sat, this.lum, this.alpha); }; Color.prototype.deriveAlpha = function(newAlpha) { verifyRange(newAlpha); return new Color(this.hue, this.sat, this.lum, newAlpha); }; Color.prototype.rgbString = function() { var rgb = hslToRgb(this.hue, this.sat, this.lum); rgb[3] = this.a; return 'rgba(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ', ' + this.alpha + ')'; }; //http://stackoverflow.com/a/9493060/7344257 function hslToRgb(h, s, l){ var r, g, b; if(s == 0){ r = g = b = l; // achromatic }else{ var hue2rgb = function hue2rgb(p, q, t){ if(t < 0) t += 1; if(t > 1) t -= 1; if(t < 1/6) return p + (q - p) * 6 * t; if(t < 1/2) return q; if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; }; var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } module.exports = Color; },{}],6:[function(require,module,exports){ function constant(val) { return { value: val, enumerable: true }; } var consts = { GRID_SIZE: constant(80), CELL_WIDTH: constant(40), SPEED: constant(5), BORDER_WIDTH: constant(20), MAX_PLAYERS: constant(81) }; Object.defineProperties(module.exports, consts); },{}],7:[function(require,module,exports){ var ANIMATE_FRAMES = 24; var CELL_WIDTH = 40; //TODO: remove constants. exports.initPlayer = function(grid, player) { for (var dr = -1; dr <= 1; dr++) for (var dc = -1; dc <= 1; dc++) if (!grid.isOutOfBounds(dr + player.row, dc + player.col)) grid.set(dr + player.row, dc + player.col, player); }; exports.updateFrame = function(grid, players, dead, notifyKill) { var adead = []; if (dead instanceof Array) adead = dead; var kill; if (!notifyKill) kill = function() {}; else kill = function(killer, other) {if (!removing[other]) notifyKill(killer, other);}; //Move players. var tmp = players.filter(function(val) { val.move(); if (val.dead) adead.push(val); return !val.dead; }); //Remove players with collisions. var removing = new Array(players.length); for (var i = 0; i < players.length; i++) { for (var j = i; j < players.length; j++) { //Remove those players when other players have hit their tail. if (!removing[j] && players[j].tail.hitsTail(players[i])) { kill(i, j); removing[j] = true; //console.log("TAIL"); } if (!removing[i] && players[i].tail.hitsTail(players[j])) { kill(j, i); removing[i] = true; //console.log("TAIL"); } //Remove players with collisons... if (i !== j && squaresIntersect(players[i].posX, players[j].posX) && squaresIntersect(players[i].posY, players[j].posY)) { //...if one player is own his own territory, the other is out. if (grid.get(players[i].row, players[i].col) === players[i]) { kill(i, j); removing[j] = true; } else if (grid.get(players[j].row, players[j].col) === players[j]) { kill(j, i); removing[i] = true; } else { //...otherwise, the one that sustains most of the collision will be removed. var areaI = area(players[i]); var areaJ = area(players[j]); if (areaI === areaJ) { kill(i, j); kill(j, i); removing[i] = removing[j] = true; } else if (areaI > areaJ) { kill(j, i); removing[i] = true; } else { kill(i, j); removing[j] = true; } } } } } tmp = tmp.filter(function(val, i) { if (removing[i]) { adead.push(val); val.die(); } return !removing[i]; }); players.length = tmp.length; for (var i = 0; i < tmp.length; i++) players[i] = tmp[i]; //Remove dead squares. for (var r = 0; r < grid.size; r++) { for (var c = 0; c < grid.size; c++) { if (adead.indexOf(grid.get(r, c)) !== -1) grid.set(r, c, null); } } }; function squaresIntersect(a, b) { if (a < b) return b < a + CELL_WIDTH; else return a < b + CELL_WIDTH; } function area(player) { var xDest = player.col * CELL_WIDTH; var yDest = player.row * CELL_WIDTH; if (player.posX === xDest) return Math.abs(player.posY - yDest); else return Math.abs(player.posX - xDest); } },{}],8:[function(require,module,exports){ function Grid(size, changeCallback) { var grid = new Array(size); var modified = false; var data = { grid: grid, size: size }; this.get = function(row, col) { if (isOutOfBounds(data, row, col)) throw new RangeError("Row or Column value out of bounds"); return grid[row] && grid[row][col]; } this.set = function(row, col, value) { if (isOutOfBounds(data, row, col)) throw new RangeError("Row or Column value out of bounds"); if (!grid[row]) grid[row] = new Array(size); var before = grid[row][col]; grid[row][col] = value; if (typeof changeCallback === "function") changeCallback(row, col, before, value); modified = true; return before; } this.reset = function() { if (modified) { grid = new Array(size); modified = false; } } this.isOutOfBounds = isOutOfBounds.bind(this, data); Object.defineProperty(this, "size", { get: function() {return size; }, enumerable: true }); } function isOutOfBounds(data, row, col) { return row < 0 || row >= data.size || col < 0 || col >= data.size; } module.exports = Grid; },{}],9:[function(require,module,exports){ var core = require('./game-core'); var consts = require('./game-consts'); exports.Color = require('./color'); exports.Grid = require('./grid'); exports.Player = require('./player'); exports.initPlayer = core.initPlayer; exports.updateFrame = core.updateFrame; for (var prop in consts) { Object.defineProperty(exports, prop, { enumerable: true, value: consts[prop] }); } },{"./color":5,"./game-consts":6,"./game-core":7,"./grid":8,"./player":10}],10:[function(require,module,exports){ var Stack = require("./stack"); var Color = require("./color"); var Grid = require("./grid.js"); var consts = require("./game-consts.js"); var GRID_SIZE = consts.GRID_SIZE; var CELL_WIDTH = consts.CELL_WIDTH; var NEW_PLAYER_LAG = 60; //wait for a second at least. function defineGetter(getter) { return { get: getter, enumerable: true }; } function defineInstanceMethods(thisobj, data /*, methods...*/) { for (var i = 2; i < arguments.length; i++) thisobj[arguments[i].name] = arguments[i].bind(this, data); } function defineAccessorProperties(thisobj, data /*, names...*/) { var descript = {}; function getAt(name) { return function() {return data[name] } } for (var i = 2; i < arguments.length; i++) descript[arguments[i]] = defineGetter(getAt(arguments[i])); Object.defineProperties(thisobj, descript); } function TailMove(orientation) { this.move = 1; Object.defineProperty(this, "orientation", { value: orientation, enumerable: true }); } function Tail(player, sdata) { var data = { tail: [], tailGrid: [], prev: null, startRow: 0, startCol: 0, prevRow: 0, prevCol: 0, player: player }; if (sdata) { data.startRow = data.prevRow = sdata.startRow || 0; data.startCol = data.prevCol = sdata.startCol || 0; sdata.tail.forEach(function(val) { addTail(data, val.orientation, val.move); }); } data.grid = player.grid; defineInstanceMethods(this, data, addTail, hitsTail, fillTail, renderTail, reposition, serialData); Object.defineProperty(this, "moves", { get: function() {return data.tail.slice(0);}, enumerable: true }); } //Instance methods. function serialData(data) { return { tail: data.tail, startRow: data.startRow, startCol: data.startCol }; } function setTailGrid(data, tailGrid, r, c) { if (!tailGrid[r]) tailGrid[r] = []; tailGrid[r][c] = true; } function addTail(data, orientation, count) { if (count === undefined) count = 1; if (!count || count < 0) return; var prev = data.prev; var r = data.prevRow, c = data.prevCol; if (data.tail.length === 0) setTailGrid(data, data.tailGrid, r, c); if (!prev || prev.orientation !== orientation) { prev = data.prev = new TailMove(orientation); data.tail.push(prev); prev.move += count - 1; } else prev.move += count; for (var i = 0; i < count; i++) { var pos = walk([data.prevRow, data.prevCol], null, orientation, 1); data.prevRow = pos[0]; data.prevCol = pos[1]; setTailGrid(data, data.tailGrid, pos[0], pos[1]); } } function reposition(data, row, col) { data.prevRow = data.startRow = row; data.prevCol = data.startCol = col; data.prev = null; if (data.tail.length === 0) return; else { var ret = data.tail; data.tail = []; data.tailGrid = []; return ret; } } /* function render2(data, ctx) { ctx.fillStyle = data.player.tailColor.rgbString(); for (var r = 0; r < data.tailGrid.length; r++) { if (!data.tailGrid[r]) continue; for (var c = 0; c < data.tailGrid[r].length; c++) if (data.tailGrid[r][c]) ctx.fillRect(c * CELL_WIDTH, r * CELL_WIDTH, CELL_WIDTH, CELL_WIDTH); } } */ //Helper methods. function renderTail(data, ctx) { if (data.tail.length === 0) return; ctx.fillStyle = data.player.tailColor.rgbString(); var prevOrient = -1; var start = [data.startRow, data.startCol]; //fillTailRect(ctx, start, start); data.tail.forEach(function(tail) { var negDir = tail.orientation === 0 || tail.orientation === 3; var back = start; if (!negDir) start = walk(start, null, tail.orientation, 1); var finish = walk(start, null, tail.orientation, tail.move - 1); if (tail.move > 1) fillTailRect(ctx, start, finish); if (prevOrient !== -1) //Draw folding triangle. renderCorner(ctx, back, prevOrient, tail.orientation); start = finish; if (negDir) walk(start, start, tail.orientation, 1); prevOrient = tail.orientation; }); var curOrient = data.player.currentHeading; if (prevOrient === curOrient) { fillTailRect(ctx, start, start); } else renderCorner(ctx, start, prevOrient, curOrient); } function renderCorner(ctx, cornerStart, dir1, dir2) { if (dir1 === 0 || dir2 === 0) walk(cornerStart, cornerStart, 2, 1); if (dir1 === 3 || dir2 === 3) walk(cornerStart, cornerStart, 1, 1); var a = walk(cornerStart, null, dir2, 1); var b = walk(a, null, dir1, 1); var triangle = new Path2D(); triangle.moveTo(cornerStart[1] * CELL_WIDTH, cornerStart[0] * CELL_WIDTH); triangle.lineTo(a[1] * CELL_WIDTH, a[0] * CELL_WIDTH); triangle.lineTo(b[1] * CELL_WIDTH, b[0] * CELL_WIDTH); triangle.closePath(); for (var i = 0; i < 2; i++) ctx.fill(triangle); } function walk(from, ret, orient, dist) { ret = ret || []; ret[0] = from[0]; ret[1] = from[1]; switch (orient) { case 0: ret[0] -= dist; break; //UP case 1: ret[1] += dist; break; //RIGHT case 2: ret[0] += dist; break; //DOWN case 3: ret[1] -= dist; break; //LEFT } return ret; } function fillTailRect(ctx, start, end) { var x = start[1] * CELL_WIDTH; var y = start[0] * CELL_WIDTH; var width = (end[1] - start[1]) * CELL_WIDTH; var height = (end[0] - start[0]) * CELL_WIDTH; if (width === 0) width += CELL_WIDTH; if (height === 0) height += CELL_WIDTH; if (width < 0) { x += width; width = -width; } if (height < 0) { y += height; height = -height; } ctx.fillRect(x, y, width, height); } function fillTail(data) { if (data.tail.length === 0) return; function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; } var grid = data.grid; var start = [data.startRow, data.startCol]; var been = new Grid(grid.size); var coords = []; coords.push(start); while (coords.length > 0) //BFS for all tail spaces. { var coord = coords.shift(); var r = coord[0]; var c = coord[1]; if (grid.isOutOfBounds(r, c)) continue; if (been.get(r, c)) continue; if (onTail(coord)) //on the tail. { been.set(r, c, true); grid.set(r, c, data.player); //Find all spots that this tail encloses. floodFill(data, grid, r + 1, c, been); floodFill(data, grid, r - 1, c, been); floodFill(data, grid, r, c + 1, been); floodFill(data, grid, r, c - 1, been); coords.push([r + 1, c]); coords.push([r - 1, c]); coords.push([r, c + 1]); coords.push([r, c - 1]); } } } function floodFill(data, grid, row, col, been) { function onTail(c) { return data.tailGrid[c[0]] && data.tailGrid[c[0]][c[1]]; } var start = [row, col]; if (grid.isOutOfBounds(row, col) || been.get(row, col) || onTail(start) || grid.get(row, col) === data.player) return; //Avoid allocating too many resources. var coords = []; var filled = new Stack(GRID_SIZE * GRID_SIZE + 1); var surrounded = true; coords.push(start); while (coords.length > 0) { var coord = coords.shift(); var r = coord[0]; var c = coord[1]; if (grid.isOutOfBounds(r, c)) { surrounded = false; continue; } //End this traverse on boundaries (where we been, on the tail, and when we enter our territory) if (been.get(r, c) || onTail(coord) || grid.get(r, c) === data.player) continue; been.set(r, c, true); if (surrounded) filled.push(coord); coords.push([r + 1, c]); coords.push([r - 1, c]); coords.push([r, c + 1]); coords.push([r, c - 1]); } if (surrounded) { while (!filled.isEmpty()) { coord = filled.pop(); grid.set(coord[0], coord[1], data.player); } } return surrounded; } function hitsTail(data, other) { return (data.prevRow !== other.row || data.prevCol !== other.col) && (data.startRow !== other.row || data.startCol !== other.col) && !!(data.tailGrid[other.row] && data.tailGrid[other.row][other.col]); } var SPEED = 5; var SHADOW_OFFSET = 10; function Player(grid, sdata) { var data = {}; //Parameters data.num = sdata.num; data.name = sdata.name || ""; //|| "Player " + (data.num + 1); data.grid = grid; data.posX = sdata.posX; data.posY = sdata.posY; this.heading = data.currentHeading = sdata.currentHeading; //0 is up, 1 is right, 2 is down, 3 is left. data.waitLag = sdata.waitLag || 0; data.dead = false; //Only need colors for client side. var base; if (sdata.base) base = this.baseColor = sdata.base instanceof Color ? sdata.base : Color.fromData(sdata.base); else { var hue = Math.random(); this.baseColor = base = new Color(hue, .8, .5); } this.lightBaseColor = base.deriveLumination(.1); this.shadowColor = base.deriveLumination(-.3); this.tailColor = base.deriveLumination(.3).deriveAlpha(.5); //Tail requires special handling. this.grid = grid; //Temporary if (sdata.tail) data.tail = new Tail(this, sdata.tail); else { data.tail = new Tail(this); data.tail.reposition(calcRow(data), calcCol(data)); } //Instance methods. this.move = move.bind(this, data); this.die = function() { data.dead = true;}; this.serialData = function() { return { base: this.baseColor, num: data.num, name: data.name, posX: data.posX, posY: data.posY, currentHeading: data.currentHeading, tail: data.tail.serialData(), waitLag: data.waitLag }; }; //Read-only Properties. defineAccessorProperties(this, data, "currentHeading", "dead", "name", "num", "posX", "posY", "grid", "tail", "waitLag"); Object.defineProperties(this, { row: defineGetter(function() { return calcRow(data); }), col: defineGetter(function() { return calcCol(data); }) }); } //Gets the next integer in positive or negative direction. function nearestInteger(positive, val) { return positive ? Math.ceil(val) : Math.floor(val); } function calcRow(data) { return nearestInteger(data.currentHeading === 2 /*DOWN*/, data.posY / CELL_WIDTH); } function calcCol(data) { return nearestInteger(data.currentHeading === 1 /*RIGHT*/, data.posX / CELL_WIDTH); } //Instance methods Player.prototype.render = function(ctx, fade) { //Render tail. this.tail.renderTail(ctx); //Render player. fade = fade || 1; ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString(); ctx.fillRect(this.posX, this.posY, CELL_WIDTH, CELL_WIDTH); var mid = CELL_WIDTH / 2; var grd = ctx.createRadialGradient(this.posX + mid, this.posY + mid - SHADOW_OFFSET, 1, this.posX + mid, this.posY + mid - SHADOW_OFFSET, CELL_WIDTH); grd.addColorStop(0, this.baseColor.deriveAlpha(fade).rgbString()); grd.addColorStop(1, new Color(0, 0, 1, fade).rgbString()); ctx.fillStyle = grd; ctx.fillRect(this.posX - 1, this.posY - SHADOW_OFFSET, CELL_WIDTH + 2, CELL_WIDTH); //Render name ctx.fillStyle = this.shadowColor.deriveAlpha(fade).rgbString(); ctx.textAlign = "center"; var yoff = -SHADOW_OFFSET * 2; if (this.row === 0) yoff = SHADOW_OFFSET * 2 + CELL_WIDTH; ctx.font = "18px Changa"; ctx.fillText(this.name, this.posX + CELL_WIDTH / 2, this.posY + yoff); }; function move(data) { if (data.waitLag < NEW_PLAYER_LAG) { data.waitLag++; return; } //Move to new position. var heading = this.heading; if (this.posX % CELL_WIDTH !== 0 || this.posY % CELL_WIDTH !== 0) heading = data.currentHeading; else data.currentHeading = heading; switch (heading) { case 0: data.posY -= SPEED; break; //UP case 1: data.posX += SPEED; break; //RIGHT case 2: data.posY += SPEED; break; //DOWN case 3: data.posX -= SPEED; break; //LEFT } //Check for out of bounds. var row = this.row, col = this.col; if (data.grid.isOutOfBounds(row, col)) { data.dead = true; return; } //Update tail position. if (data.grid.get(row, col) === this) { //Safe zone! this.tail.fillTail(); this.tail.reposition(row, col); } //If we are completely in a new cell (not in our safe zone), we add to the tail. else if (this.posX % CELL_WIDTH === 0 && this.posY % CELL_WIDTH === 0) this.tail.addTail(heading); } module.exports = Player; },{"./color":5,"./game-consts.js":6,"./grid.js":8,"./stack":11}],11:[function(require,module,exports){ function Stack(initSize) { var len = 0; var arr = []; this.ensureCapacity = function(size) { arr.length = Math.max(arr.length, size || 0); }; this.push = function(ele) { this[len] = ele; len++; }; this.pop = function() { if (len === 0) return; len--; var tmp = this[len]; this[len] = undefined; return tmp; }; this.isEmpty = function() { return len === 0; } this.ensureCapacity(initSize); Object.defineProperty(this, "length", { get: function() {return len;} }); } module.exports = Stack; },{}],12:[function(require,module,exports){ module.exports = after function after(count, callback, err_cb) { var bail = false err_cb = err_cb || noop proxy.count = count return (count === 0) ? callback() : proxy function proxy(err, result) { if (proxy.count <= 0) { throw new Error('after called too many times') } --proxy.count // after first error, rest are passed to err_cb if (err) { bail = true callback(err) // future error callbacks will go to error handler callback = err_cb } else if (proxy.count === 0 && !bail) { callback(null, result) } } } function noop() {} },{}],13:[function(require,module,exports){ /** * An abstraction for slicing an arraybuffer even when * ArrayBuffer.prototype.slice is not supported * * @api public */ module.exports = function(arraybuffer, start, end) { var bytes = arraybuffer.byteLength; start = start || 0; end = end || bytes; if (arraybuffer.slice) { return arraybuffer.slice(start, end); } if (start < 0) { start += bytes; } if (end < 0) { end += bytes; } if (end > bytes) { end = bytes; } if (start >= bytes || start >= end || bytes === 0) { return new ArrayBuffer(0); } var abv = new Uint8Array(arraybuffer); var result = new Uint8Array(end - start); for (var i = start, ii = 0; i < end; i++, ii++) { result[ii] = abv[i]; } return result.buffer; }; },{}],14:[function(require,module,exports){ /** * Expose `Backoff`. */ module.exports = Backoff; /** * Initialize backoff timer with `opts`. * * - `min` initial timeout in milliseconds [100] * - `max` max timeout [10000] * - `jitter` [0] * - `factor` [2] * * @param {Object} opts * @api public */ function Backoff(opts) { opts = opts || {}; this.ms = opts.min || 100; this.max = opts.max || 10000; this.factor = opts.factor || 2; this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0; this.attempts = 0; } /** * Return the backoff duration. * * @return {Number} * @api public */ Backoff.prototype.duration = function(){ var ms = this.ms * Math.pow(this.factor, this.attempts++); if (this.jitter) { var rand = Math.random(); var deviation = Math.floor(rand * this.jitter * ms); ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation; } return Math.min(ms, this.max) | 0; }; /** * Reset the number of attempts. * * @api public */ Backoff.prototype.reset = function(){ this.attempts = 0; }; /** * Set the minimum duration * * @api public */ Backoff.prototype.setMin = function(min){ this.ms = min; }; /** * Set the maximum duration * * @api public */ Backoff.prototype.setMax = function(max){ this.max = max; }; /** * Set the jitter * * @api public */ Backoff.prototype.setJitter = function(jitter){ this.jitter = jitter; }; },{}],15:[function(require,module,exports){ /* * base64-arraybuffer * https://github.com/niklasvh/base64-arraybuffer * * Copyright (c) 2012 Niklas von Hertzen * Licensed under the MIT license. */ (function(){ "use strict"; var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // Use a lookup table to find the index. var lookup = new Uint8Array(256); for (var i = 0; i < chars.length; i++) { lookup[chars.charCodeAt(i)] = i; } exports.encode = function(arraybuffer) { var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ""; for (i = 0; i < len; i+=3) { base64 += chars[bytes[i] >> 2]; base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; base64 += chars[bytes[i + 2] & 63]; } if ((len % 3) === 2) { base64 = base64.substring(0, base64.length - 1) + "="; } else if (len % 3 === 1) { base64 = base64.substring(0, base64.length - 2) + "=="; } return base64; }; exports.decode = function(base64) { var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4; if (base64[base64.length - 1] === "=") { bufferLength--; if (base64[base64.length - 2] === "=") { bufferLength--; } } var arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer); for (i = 0; i < len; i+=4) { encoded1 = lookup[base64.charCodeAt(i)]; encoded2 = lookup[base64.charCodeAt(i+1)]; encoded3 = lookup[base64.charCodeAt(i+2)]; encoded4 = lookup[base64.charCodeAt(i+3)]; bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); } return arraybuffer; }; })(); },{}],16:[function(require,module,exports){ (function (global){ /** * Create a blob builder even when vendor prefixes exist */ var BlobBuilder = global.BlobBuilder || global.WebKitBlobBuilder || global.MSBlobBuilder || global.MozBlobBuilder; /** * Check if Blob constructor is supported */ var blobSupported = (function() { try { var a = new Blob(['hi']); return a.size === 2; } catch(e) { return false; } })(); /** * Check if Blob constructor supports ArrayBufferViews * Fails in Safari 6, so we need to map to ArrayBuffers there. */ var blobSupportsArrayBufferView = blobSupported && (function() { try { var b = new Blob([new Uint8Array([1,2])]); return b.size === 2; } catch(e) { return false; } })(); /** * Check if BlobBuilder is supported */ var blobBuilderSupported = BlobBuilder && BlobBuilder.prototype.append && BlobBuilder.prototype.getBlob; /** * Helper function that maps ArrayBufferViews to ArrayBuffers * Used by BlobBuilder constructor and old browsers that didn't * support it in the Blob constructor. */ function mapArrayBufferViews(ary) { for (var i = 0; i < ary.length; i++) { var chunk = ary[i]; if (chunk.buffer instanceof ArrayBuffer) { var buf = chunk.buffer; // if this is a subarray, make a copy so we only // include the subarray region from the underlying buffer if (chunk.byteLength !== buf.byteLength) { var copy = new Uint8Array(chunk.byteLength); copy.set(new Uint8Array(buf, chunk.byteOffset, chunk.byteLength)); buf = copy.buffer; } ary[i] = buf; } } } function BlobBuilderConstructor(ary, options) { options = options || {}; var bb = new BlobBuilder(); mapArrayBufferViews(ary); for (var i = 0; i < ary.length; i++) { bb.append(ary[i]); } return (options.type) ? bb.getBlob(options.type) : bb.getBlob(); }; function BlobConstructor(ary, options) { mapArrayBufferViews(ary); return new Blob(ary, options || {}); }; module.exports = (function() { if (blobSupported) { return blobSupportsArrayBufferView ? global.Blob : BlobConstructor; } else if (blobBuilderSupported) { return BlobBuilderConstructor; } else { return undefined; } })(); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],17:[function(require,module,exports){ /** * Slice reference. */ var slice = [].slice; /** * Bind `obj` to `fn`. * * @param {Object} obj * @param {Function|String} fn or string * @return {Function} * @api public */ module.exports = function(obj, fn){ if ('string' == typeof fn) fn = obj[fn]; if ('function' != typeof fn) throw new Error('bind() requires a function'); var args = slice.call(arguments, 2); return function(){ return fn.apply(obj, args.concat(slice.call(arguments))); } }; },{}],18:[function(require,module,exports){ /** * Expose `Emitter`. */ module.exports = Emitter; /** * Initialize a new `Emitter`. * * @api public */ function Emitter(obj) { if (obj) return mixin(obj); }; /** * Mixin the emitter properties. * * @param {Object} obj * @return {Object} * @api private */ function mixin(obj) { for (var key in Emitter.prototype) { obj[key] = Emitter.prototype[key]; } return obj; } /** * Listen on the given `event` with `fn`. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.on = Emitter.prototype.addEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; (this._callbacks[event] = this._callbacks[event] || []) .push(fn); return this; }; /** * Adds an `event` listener that will be invoked a single * time then automatically removed. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.once = function(event, fn){ var self = this; this._callbacks = this._callbacks || {}; function on() { self.off(event, on); fn.apply(this, arguments); } on.fn = fn; this.on(event, on); return this; }; /** * Remove the given callback for `event` or all * registered callbacks. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.off = Emitter.prototype.removeListener = Emitter.prototype.removeAllListeners = Emitter.prototype.removeEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; // all if (0 == arguments.length) { this._callbacks = {}; return this; } // specific event var callbacks = this._callbacks[event]; if (!callbacks) return this; // remove all handlers if (1 == arguments.length) { delete this._callbacks[event]; return this; } // remove specific handler var cb; for (var i = 0; i < callbacks.length; i++) { cb = callbacks[i]; if (cb === fn || cb.fn === fn) { callbacks.splice(i, 1); break; } } return this; }; /** * Emit `event` with the given args. * * @param {String} event * @param {Mixed} ... * @return {Emitter} */ Emitter.prototype.emit = function(event){ this._callbacks = this._callbacks || {}; var args = [].slice.call(arguments, 1) , callbacks = this._callbacks[event]; if (callbacks) { callbacks = callbacks.slice(0); for (var i = 0, len = callbacks.length; i < len; ++i) { callbacks[i].apply(this, args); } } return this; }; /** * Return array of callbacks for `event`. * * @param {String} event * @return {Array} * @api public */ Emitter.prototype.listeners = function(event){ this._callbacks = this._callbacks || {}; return this._callbacks[event] || []; }; /** * Check if this emitter has `event` handlers. * * @param {String} event * @return {Boolean} * @api public */ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; },{}],19:[function(require,module,exports){ module.exports = function(a, b){ var fn = function(){}; fn.prototype = b.prototype; a.prototype = new fn; a.prototype.constructor = a; }; },{}],20:[function(require,module,exports){ module.exports = require('./lib/index'); },{"./lib/index":21}],21:[function(require,module,exports){ module.exports = require('./socket'); /** * Exports parser * * @api public * */ module.exports.parser = require('engine.io-parser'); },{"./socket":22,"engine.io-parser":34}],22:[function(require,module,exports){ (function (global){ /** * Module dependencies. */ var transports = require('./transports/index'); var Emitter = require('component-emitter'); var debug = require('debug')('engine.io-client:socket'); var index = require('indexof'); var parser = require('engine.io-parser'); var parseuri = require('parseuri'); var parsejson = require('parsejson'); var parseqs = require('parseqs'); /** * Module exports. */ module.exports = Socket; /** * Socket constructor. * * @param {String|Object} uri or options * @param {Object} options * @api public */ function Socket (uri, opts) { if (!(this instanceof Socket)) return new Socket(uri, opts); opts = opts || {}; if (uri && 'object' === typeof uri) { opts = uri; uri = null; } if (uri) { uri = parseuri(uri); opts.hostname = uri.host; opts.secure = uri.protocol === 'https' || uri.protocol === 'wss'; opts.port = uri.port; if (uri.query) opts.query = uri.query; } else if (opts.host) { opts.hostname = parseuri(opts.host).host; } this.secure = null != opts.secure ? opts.secure : (global.location && 'https:' === location.protocol); if (opts.hostname && !opts.port) { // if no port is specified manually, use the protocol default opts.port = this.secure ? '443' : '80'; } this.agent = opts.agent || false; this.hostname = opts.hostname || (global.location ? location.hostname : 'localhost'); this.port = opts.port || (global.location && location.port ? location.port : (this.secure ? 443 : 80)); this.query = opts.query || {}; if ('string' === typeof this.query) this.query = parseqs.decode(this.query); this.upgrade = false !== opts.upgrade; this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/'; this.forceJSONP = !!opts.forceJSONP; this.jsonp = false !== opts.jsonp; this.forceBase64 = !!opts.forceBase64; this.enablesXDR = !!opts.enablesXDR; this.timestampParam = opts.timestampParam || 't'; this.timestampRequests = opts.timestampRequests; this.transports = opts.transports || ['polling', 'websocket']; this.readyState = ''; this.writeBuffer = []; this.prevBufferLen = 0; this.policyPort = opts.policyPort || 843; this.rememberUpgrade = opts.rememberUpgrade || false; this.binaryType = null; this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades; this.perMessageDeflate = false !== opts.perMessageDeflate ? (opts.perMessageDeflate || {}) : false; if (true === this.perMessageDeflate) this.perMessageDeflate = {}; if (this.perMessageDeflate && null == this.perMessageDeflate.threshold) { this.perMessageDeflate.threshold = 1024; } // SSL options for Node.js client this.pfx = opts.pfx || null; this.key = opts.key || null; this.passphrase = opts.passphrase || null; this.cert = opts.cert || null; this.ca = opts.ca || null; this.ciphers = opts.ciphers || null; this.rejectUnauthorized = opts.rejectUnauthorized === undefined ? null : opts.rejectUnauthorized; this.forceNode = !!opts.forceNode; // other options for Node.js client var freeGlobal = typeof global === 'object' && global; if (freeGlobal.global === freeGlobal) { if (opts.extraHeaders && Object.keys(opts.extraHeaders).length > 0) { this.extraHeaders = opts.extraHeaders; } if (opts.localAddress) { this.localAddress = opts.localAddress; } } // set on handshake this.id = null; this.upgrades = null; this.pingInterval = null; this.pingTimeout = null; // set on heartbeat this.pingIntervalTimer = null; this.pingTimeoutTimer = null; this.open(); } Socket.priorWebsocketSuccess = false; /** * Mix in `Emitter`. */ Emitter(Socket.prototype); /** * Protocol version. * * @api public */ Socket.protocol = parser.protocol; // this is an int /** * Expose deps for legacy compatibility * and standalone browser access. */ Socket.Socket = Socket; Socket.Transport = require('./transport'); Socket.transports = require('./transports/index'); Socket.parser = require('engine.io-parser'); /** * Creates transport of the given type. * * @param {String} transport name * @return {Transport} * @api private */ Socket.prototype.createTransport = function (name) { debug('creating transport "%s"', name); var query = clone(this.query); // append engine.io protocol identifier query.EIO = parser.protocol; // transport name query.transport = name; // session id if we already have one if (this.id) query.sid = this.id; var transport = new transports[name]({ agent: this.agent, hostname: this.hostname, port: this.port, secure: this.secure, path: this.path, query: query, forceJSONP: this.forceJSONP, jsonp: this.jsonp, forceBase64: this.forceBase64, enablesXDR: this.enablesXDR, timestampRequests: this.timestampRequests, timestampParam: this.timestampParam, policyPort: this.policyPort, socket: this, pfx: this.pfx, key: this.key, passphrase: this.passphrase, cert: this.cert, ca: this.ca, ciphers: this.ciphers, rejectUnauthorized: this.rejectUnauthorized, perMessageDeflate: this.perMessageDeflate, extraHeaders: this.extraHeaders, forceNode: this.forceNode, localAddress: this.localAddress }); return transport; }; function clone (obj) { var o = {}; for (var i in obj) { if (obj.hasOwnProperty(i)) { o[i] = obj[i]; } } return o; } /** * Initializes transport to use and starts probe. * * @api private */ Socket.prototype.open = function () { var transport; if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') !== -1) { transport = 'websocket'; } else if (0 === this.transports.length) { // Emit error on next tick so it can be listened to var self = this; setTimeout(function () { self.emit('error', 'No transports available'); }, 0); return; } else { transport = this.transports[0]; } this.readyState = 'opening'; // Retry with the next transport if the transport is disabled (jsonp: false) try { transport = this.createTransport(transport); } catch (e) { this.transports.shift(); this.open(); return; } transport.open(); this.setTransport(transport); }; /** * Sets the current transport. Disables the existing one (if any). * * @api private */ Socket.prototype.setTransport = function (transport) { debug('setting transport %s', transport.name); var self = this; if (this.transport) { debug('clearing existing transport %s', this.transport.name); this.transport.removeAllListeners(); } // set up transport this.transport = transport; // set up transport listeners transport .on('drain', function () { self.onDrain(); }) .on('packet', function (packet) { self.onPacket(packet); }) .on('error', function (e) { self.onError(e); }) .on('close', function () { self.onClose('transport close'); }); }; /** * Probes a transport. * * @param {String} transport name * @api private */ Socket.prototype.probe = function (name) { debug('probing transport "%s"', name); var transport = this.createTransport(name, { probe: 1 }); var failed = false; var self = this; Socket.priorWebsocketSuccess = false; function onTransportOpen () { if (self.onlyBinaryUpgrades) { var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary; failed = failed || upgradeLosesBinary; } if (failed) return; debug('probe transport "%s" opened', name); transport.send([{ type: 'ping', data: 'probe' }]); transport.once('packet', function (msg) { if (failed) return; if ('pong' === msg.type && 'probe' === msg.data) { debug('probe transport "%s" pong', name); self.upgrading = true; self.emit('upgrading', transport); if (!transport) return; Socket.priorWebsocketSuccess = 'websocket' === transport.name; debug('pausing current transport "%s"', self.transport.name); self.transport.pause(function () { if (failed) return; if ('closed' === self.readyState) return; debug('changing transport and sending upgrade packet'); cleanup(); self.setTransport(transport); transport.send([{ type: 'upgrade' }]); self.emit('upgrade', transport); transport = null; self.upgrading = false; self.flush(); }); } else { debug('probe transport "%s" failed', name); var err = new Error('probe error'); err.transport = transport.name; self.emit('upgradeError', err); } }); } function freezeTransport () { if (failed) return; // Any callback called by transport should be ignored since now failed = true; cleanup(); transport.close(); transport = null; } // Handle any error that happens while probing function onerror (err) { var error = new Error('probe error: ' + err); error.transport = transport.name; freezeTransport(); debug('probe transport "%s" failed because of error: %s', name, err); self.emit('upgradeError', error); } function onTransportClose () { onerror('transport closed'); } // When the socket is closed while we're probing function onclose () { onerror('socket closed'); } // When the socket is upgraded while we're probing function onupgrade (to) { if (transport && to.name !== transport.name) { debug('"%s" works - aborting "%s"', to.name, transport.name); freezeTransport(); } } // Remove all listeners on the transport and on self function cleanup () { transport.removeListener('open', onTransportOpen); transport.removeListener('error', onerror); transport.removeListener('close', onTransportClose); self.removeListener('close', onclose); self.removeListener('upgrading', onupgrade); } transport.once('open', onTransportOpen); transport.once('error', onerror); transport.once('close', onTransportClose); this.once('close', onclose); this.once('upgrading', onupgrade); transport.open(); }; /** * Called when connection is deemed open. * * @api public */ Socket.prototype.onOpen = function () { debug('socket open'); this.readyState = 'open'; Socket.priorWebsocketSuccess = 'websocket' === this.transport.name; this.emit('open'); this.flush(); // we check for `readyState` in case an `open` // listener already closed the socket if ('open' === this.readyState && this.upgrade && this.transport.pause) { debug('starting upgrade probes'); for (var i = 0, l = this.upgrades.length; i < l; i++) { this.probe(this.upgrades[i]); } } }; /** * Handles a packet. * * @api private */ Socket.prototype.onPacket = function (packet) { if ('opening' === this.readyState || 'open' === this.readyState || 'closing' === this.readyState) { debug('socket receive: type "%s", data "%s"', packet.type, packet.data); this.emit('packet', packet); // Socket is live - any packet counts this.emit('heartbeat'); switch (packet.type) { case 'open': this.onHandshake(parsejson(packet.data)); break; case 'pong': this.setPing(); this.emit('pong'); break; case 'error': var err = new Error('server error'); err.code = packet.data; this.onError(err); break; case 'message': this.emit('data', packet.data); this.emit('message', packet.data); break; } } else { debug('packet received with socket readyState "%s"', this.readyState); } }; /** * Called upon handshake completion. * * @param {Object} handshake obj * @api private */ Socket.prototype.onHandshake = function (data) { this.emit('handshake', data); this.id = data.sid; this.transport.query.sid = data.sid; this.upgrades = this.filterUpgrades(data.upgrades); this.pingInterval = data.pingInterval; this.pingTimeout = data.pingTimeout; this.onOpen(); // In case open handler closes socket if ('closed' === this.readyState) return; this.setPing(); // Prolong liveness of socket on heartbeat this.removeListener('heartbeat', this.onHeartbeat); this.on('heartbeat', this.onHeartbeat); }; /** * Resets ping timeout. * * @api private */ Socket.prototype.onHeartbeat = function (timeout) { clearTimeout(this.pingTimeoutTimer); var self = this; self.pingTimeoutTimer = setTimeout(function () { if ('closed' === self.readyState) return; self.onClose('ping timeout'); }, timeout || (self.pingInterval + self.pingTimeout)); }; /** * Pings server every `this.pingInterval` and expects response * within `this.pingTimeout` or closes connection. * * @api private */ Socket.prototype.setPing = function () { var self = this; clearTimeout(self.pingIntervalTimer); self.pingIntervalTimer = setTimeout(function () { debug('writing ping packet - expecting pong within %sms', self.pingTimeout); self.ping(); self.onHeartbeat(self.pingTimeout); }, self.pingInterval); }; /** * Sends a ping packet. * * @api private */ Socket.prototype.ping = function () { var self = this; this.sendPacket('ping', function () { self.emit('ping'); }); }; /** * Called on `drain` event * * @api private */ Socket.prototype.onDrain = function () { this.writeBuffer.splice(0, this.prevBufferLen); // setting prevBufferLen = 0 is very important // for example, when upgrading, upgrade packet is sent over, // and a nonzero prevBufferLen could cause problems on `drain` this.prevBufferLen = 0; if (0 === this.writeBuffer.length) { this.emit('drain'); } else { this.flush(); } }; /** * Flush write buffers. * * @api private */ Socket.prototype.flush = function () { if ('closed' !== this.readyState && this.transport.writable && !this.upgrading && this.writeBuffer.length) { debug('flushing %d packets in socket', this.writeBuffer.length); this.transport.send(this.writeBuffer); // keep track of current length of writeBuffer // splice writeBuffer and callbackBuffer on `drain` this.prevBufferLen = this.writeBuffer.length; this.emit('flush'); } }; /** * Sends a message. * * @param {String} message. * @param {Function} callback function. * @param {Object} options. * @return {Socket} for chaining. * @api public */ Socket.prototype.write = Socket.prototype.send = function (msg, options, fn) { this.sendPacket('message', msg, options, fn); return this; }; /** * Sends a packet. * * @param {String} packet type. * @param {String} data. * @param {Object} options. * @param {Function} callback function. * @api private */ Socket.prototype.sendPacket = function (type, data, options, fn) { if ('function' === typeof data) { fn = data; data = undefined; } if ('function' === typeof options) { fn = options; options = null; } if ('closing' === this.readyState || 'closed' === this.readyState) { return; } options = options || {}; options.compress = false !== options.compress; var packet = { type: type, data: data, options: options }; this.emit('packetCreate', packet); this.writeBuffer.push(packet); if (fn) this.once('flush', fn); this.flush(); }; /** * Closes the connection. * * @api private */ Socket.prototype.close = function () { if ('opening' === this.readyState || 'open' === this.readyState) { this.readyState = 'closing'; var self = this; if (this.writeBuffer.length) { this.once('drain', function () { if (this.upgrading) { waitForUpgrade(); } else { close(); } }); } else if (this.upgrading) { waitForUpgrade(); } else { close(); } } function close () { self.onClose('forced close'); debug('socket closing - telling transport to close'); self.transport.close(); } function cleanupAndClose () { self.removeListener('upgrade', cleanupAndClose); self.removeListener('upgradeError', cleanupAndClose); close(); } function waitForUpgrade () { // wait for upgrade to finish since we can't send packets while pausing a transport self.once('upgrade', cleanupAndClose); self.once('upgradeError', cleanupAndClose); } return this; }; /** * Called upon transport error * * @api private */ Socket.prototype.onError = function (err) { debug('socket error %j', err); Socket.priorWebsocketSuccess = false; this.emit('error', err); this.onClose('transport error', err); }; /** * Called upon transport close. * * @api private */ Socket.prototype.onClose = function (reason, desc) { if ('opening' === this.readyState || 'open' === this.readyState || 'closing' === this.readyState) { debug('socket close with reason: "%s"', reason); var self = this; // clear timers clearTimeout(this.pingIntervalTimer); clearTimeout(this.pingTimeoutTimer); // stop event from firing again for transport this.transport.removeAllListeners('close'); // ensure transport won't stay open this.transport.close(); // ignore further transport communication this.transport.removeAllListeners(); // set ready state this.readyState = 'closed'; // clear session id this.id = null; // emit close event this.emit('close', reason, desc); // clean buffers after, so users can still // grab the buffers on `close` event self.writeBuffer = []; self.prevBufferLen = 0; } }; /** * Filters upgrades, returning only those matching client transports. * * @param {Array} server upgrades * @api private * */ Socket.prototype.filterUpgrades = function (upgrades) { var filteredUpgrades = []; for (var i = 0, j = upgrades.length; i < j; i++) { if (~index(this.transports, upgrades[i])) filteredUpgrades.push(upgrades[i]); } return filteredUpgrades; }; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./transport":23,"./transports/index":24,"component-emitter":30,"debug":31,"engine.io-parser":34,"indexof":38,"parsejson":41,"parseqs":42,"parseuri":43}],23:[function(require,module,exports){ /** * Module dependencies. */ var parser = require('engine.io-parser'); var Emitter = require('component-emitter'); /** * Module exports. */ module.exports = Transport; /** * Transport abstract constructor. * * @param {Object} options. * @api private */ function Transport (opts) { this.path = opts.path; this.hostname = opts.hostname; this.port = opts.port; this.secure = opts.secure; this.query = opts.query; this.timestampParam = opts.timestampParam; this.timestampRequests = opts.timestampRequests; this.readyState = ''; this.agent = opts.agent || false; this.socket = opts.socket; this.enablesXDR = opts.enablesXDR; // SSL options for Node.js client this.pfx = opts.pfx; this.key = opts.key; this.passphrase = opts.passphrase; this.cert = opts.cert; this.ca = opts.ca; this.ciphers = opts.ciphers; this.rejectUnauthorized = opts.rejectUnauthorized; this.forceNode = opts.forceNode; // other options for Node.js client this.extraHeaders = opts.extraHeaders; this.localAddress = opts.localAddress; } /** * Mix in `Emitter`. */ Emitter(Transport.prototype); /** * Emits an error. * * @param {String} str * @return {Transport} for chaining * @api public */ Transport.prototype.onError = function (msg, desc) { var err = new Error(msg); err.type = 'TransportError'; err.description = desc; this.emit('error', err); return this; }; /** * Opens the transport. * * @api public */ Transport.prototype.open = function () { if ('closed' === this.readyState || '' === this.readyState) { this.readyState = 'opening'; this.doOpen(); } return this; }; /** * Closes the transport. * * @api private */ Transport.prototype.close = function () { if ('opening' === this.readyState || 'open' === this.readyState) { this.doClose(); this.onClose(); } return this; }; /** * Sends multiple packets. * * @param {Array} packets * @api private */ Transport.prototype.send = function (packets) { if ('open' === this.readyState) { this.write(packets); } else { throw new Error('Transport not open'); } }; /** * Called upon open * * @api private */ Transport.prototype.onOpen = function () { this.readyState = 'open'; this.writable = true; this.emit('open'); }; /** * Called with data. * * @param {String} data * @api private */ Transport.prototype.onData = function (data) { var packet = parser.decodePacket(data, this.socket.binaryType); this.onPacket(packet); }; /** * Called with a decoded packet. */ Transport.prototype.onPacket = function (packet) { this.emit('packet', packet); }; /** * Called upon close. * * @api private */ Transport.prototype.onClose = function () { this.readyState = 'closed'; this.emit('close'); }; },{"component-emitter":30,"engine.io-parser":34}],24:[function(require,module,exports){ (function (global){ /** * Module dependencies */ var XMLHttpRequest = require('xmlhttprequest-ssl'); var XHR = require('./polling-xhr'); var JSONP = require('./polling-jsonp'); var websocket = require('./websocket'); /** * Export transports. */ exports.polling = polling; exports.websocket = websocket; /** * Polling transport polymorphic constructor. * Decides on xhr vs jsonp based on feature detection. * * @api private */ function polling (opts) { var xhr; var xd = false; var xs = false; var jsonp = false !== opts.jsonp; if (global.location) { var isSSL = 'https:' === location.protocol; var port = location.port; // some user agents have empty `location.port` if (!port) { port = isSSL ? 443 : 80; } xd = opts.hostname !== location.hostname || port !== opts.port; xs = opts.secure !== isSSL; } opts.xdomain = xd; opts.xscheme = xs; xhr = new XMLHttpRequest(opts); if ('open' in xhr && !opts.forceJSONP) { return new XHR(opts); } else { if (!jsonp) throw new Error('JSONP disabled'); return new JSONP(opts); } } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./polling-jsonp":25,"./polling-xhr":26,"./websocket":28,"xmlhttprequest-ssl":29}],25:[function(require,module,exports){ (function (global){ /** * Module requirements. */ var Polling = require('./polling'); var inherit = require('component-inherit'); /** * Module exports. */ module.exports = JSONPPolling; /** * Cached regular expressions. */ var rNewline = /\n/g; var rEscapedNewline = /\\n/g; /** * Global JSONP callbacks. */ var callbacks; /** * Noop. */ function empty () { } /** * JSONP Polling constructor. * * @param {Object} opts. * @api public */ function JSONPPolling (opts) { Polling.call(this, opts); this.query = this.query || {}; // define global callbacks array if not present // we do this here (lazily) to avoid unneeded global pollution if (!callbacks) { // we need to consider multiple engines in the same page if (!global.___eio) global.___eio = []; callbacks = global.___eio; } // callback identifier this.index = callbacks.length; // add callback to jsonp global var self = this; callbacks.push(function (msg) { self.onData(msg); }); // append to query string this.query.j = this.index; // prevent spurious errors from being emitted when the window is unloaded if (global.document && global.addEventListener) { global.addEventListener('beforeunload', function () { if (self.script) self.script.onerror = empty; }, false); } } /** * Inherits from Polling. */ inherit(JSONPPolling, Polling); /* * JSONP only supports binary as base64 encoded strings */ JSONPPolling.prototype.supportsBinary = false; /** * Closes the socket. * * @api private */ JSONPPolling.prototype.doClose = function () { if (this.script) { this.script.parentNode.removeChild(this.script); this.script = null; } if (this.form) { this.form.parentNode.removeChild(this.form); this.form = null; this.iframe = null; } Polling.prototype.doClose.call(this); }; /** * Starts a poll cycle. * * @api private */ JSONPPolling.prototype.doPoll = function () { var self = this; var script = document.createElement('script'); if (this.script) { this.script.parentNode.removeChild(this.script); this.script = null; } script.async = true; script.src = this.uri(); script.onerror = function (e) { self.onError('jsonp poll error', e); }; var insertAt = document.getElementsByTagName('script')[0]; if (insertAt) { insertAt.parentNode.insertBefore(script, insertAt); } else { (document.head || document.body).appendChild(script); } this.script = script; var isUAgecko = 'undefined' !== typeof navigator && /gecko/i.test(navigator.userAgent); if (isUAgecko) { setTimeout(function () { var iframe = document.createElement('iframe'); document.body.appendChild(iframe); document.body.removeChild(iframe); }, 100); } }; /** * Writes with a hidden iframe. * * @param {String} data to send * @param {Function} called upon flush. * @api private */ JSONPPolling.prototype.doWrite = function (data, fn) { var self = this; if (!this.form) { var form = document.createElement('form'); var area = document.createElement('textarea'); var id = this.iframeId = 'eio_iframe_' + this.index; var iframe; form.className = 'socketio'; form.style.position = 'absolute'; form.style.top = '-1000px'; form.style.left = '-1000px'; form.target = id; form.method = 'POST'; form.setAttribute('accept-charset', 'utf-8'); area.name = 'd'; form.appendChild(area); document.body.appendChild(form); this.form = form; this.area = area; } this.form.action = this.uri(); function complete () { initIframe(); fn(); } function initIframe () { if (self.iframe) { try { self.form.removeChild(self.iframe); } catch (e) { self.onError('jsonp polling iframe removal error', e); } } try { // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) var html = '