robotwar/public/js/main.js
2014-10-04 15:56:50 -04:00

366 lines
12 KiB
JavaScript

;(function() {
var BUFFER_LENGTH = 1000;
var MAX_FAST_FORWARD = 40;
var STARTING_FAST_FORWARD = 15;
var FPS = 60;
var ROBOT_COLORS = ["#fa2d0b", "#0bfaf7", "#faf20b", "#e312f0", "#4567fb"];
var SHELL_COLOR = "#ffffff";
var SOUNDS = {
shellRelease: {
oggSrc: "audio/trprsht1.ogg",
mp3Src: "audio/trprsht1.mp3",
numInstances: 40
}
};
var Geom = (function() {
var degreesToRadians = function(angle) {
return angle * Math.PI / 180;
}
var RWDegreesToJSDegrees = function(angle) {
return angle - 90;
}
var polarToCartesian = function(angleInDegrees, d) {
var angle = degreesToRadians(RWDegreesToJSDegrees(angleInDegrees));
return {
x: d * Math.cos(angle),
y: d * Math.sin(angle)
}
}
return {
polarToCartesian: polarToCartesian
}
})();
function Worlds(programs, bufferLength) {
// The constructor first gets a game id from the server,
// then runs the first fetch.
var queue = new Queue();
var gameId;
var gameInfo;
var onLoaded = $.Deferred();
var isFetching = false;
var isFinished = false;
var previousWorld = null;
var currentWorld = null;
function fetch(callback) {
$.getJSON('worlds/' + gameId + '/' + bufferLength, function(data) {
queue.enqueueArray(data);
if (callback) callback();
});
}
function advance(fastForward) {
previousWorld = currentWorld;
queue.dropMulti(fastForward);
// if the queue has run out, just stall with the
// existing currentWorld until we receive a new one.
currentWorld = queue.peek() || currentWorld;
if (!isFetching && queue.getLength() < bufferLength) {
isFetching = true;
fetch(function() {
isFetching = false;
});
}
}
function finish() {
isFinished = true;
}
$.getJSON('init?programs=' + encodeURIComponent(programs))
.done(function(data) {
gameId = data['id'];
var gameInfoFromServer = data['game-info'];
gameInfo = {
robotRadius: gameInfoFromServer["ROBOT-RADIUS"],
robotRangeX: gameInfoFromServer["ROBOT-RANGE-X"],
robotRangeY: gameInfoFromServer["ROBOT-RANGE-Y"],
gameSecondsPerTick: gameInfoFromServer["*GAME-SECONDS-PER-TICK*"]
};
console.log(gameInfo);
fetch(function() {
currentWorld = queue.peek();
onLoaded.resolve();
});
});
return {
advance: advance,
finish: finish,
onLoaded: onLoaded,
isFinished: function() { return isFinished },
getGameInfo: function() { return gameInfo },
getPreviousWorld: function() { return previousWorld;},
getCurrentWorld: function() { return currentWorld;}
}
}
function SoundPlayer(attributes) {
var oggSrc = attributes.oggSrc;
var mp3Src = attributes.mp3Src;
var numInstances = attributes.numInstances;
var soundIdx = 0;
var src = (new Audio().canPlayType("audio/ogg")) ? oggSrc : mp3Src;
var els = [];
for (var i = 0; i < numInstances; i++) {
els.push(new Audio(src));
}
return {
play: function() {
els[soundIdx].play();
soundIdx = (soundIdx + 1) % numInstances;
}
};
}
function Animation(el, sounds, gameInfo) {
var soundPlayers = {};
for (var k in sounds) {
if (sounds.hasOwnProperty(k)) {
soundPlayers[k] = new SoundPlayer(sounds[k]);
}
}
var width = parseInt(el.width);
var height = parseInt(el.height);
var roomForRobots = gameInfo.robotRadius * 2;
var arenaWidth = gameInfo.robotRangeX + roomForRobots;
var arenaHeight = gameInfo.robotRangeY + roomForRobots;
var scaleFactorX = width / arenaWidth;
var scaleFactorY = height / arenaHeight;
var scaleX = function(x) {
return Math.round(x * scaleFactorX);
}
var scaleY = function(y) {
return Math.round(y * scaleFactorY);
}
var offsetX = function(x) {
return scaleX(gameInfo.robotRadius + x);
}
var offsetY = function(y) {
return scaleY(gameInfo.robotRadius + y);
}
// TODO: regularize this here and on the server so that
// the arena is always square, and there's no ambiguity or question,
// like why are we using scaleFactorX here and don't need
// scaleFactorY?
var robotDisplayRadius = scaleX(gameInfo.robotRadius);
var shellDisplayRadius = scaleX(gameInfo.robotRadius * 0.3);
var gunDisplayLength = scaleX(gameInfo.robotRadius * 1.4);
var gunDisplayWidth = scaleX(gameInfo.robotRadius * 0.5);
var ctx = el.getContext('2d');
ctx.lineCap = 'square';
// TODO: this whole drawing section is kind of hacky. reorganize
// the behaviors of these functions to be less redundant,
// and more abstracted.
var fillCircle = function(x, y, r, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, true);
ctx.fill();
}
var strokeCircle = function(x, y, r, lineWidth) {
ctx.lineWidth = lineWidth;
ctx.strokeStyle = "#000";
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, true);
ctx.stroke();
}
var fillSquare = function(x, y, size, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.rect(x - size / 2, y - size / 2, size, size);
ctx.fill();
}
var drawLinePolar = function(x, y, angle, d, lineWidth, color) {
var delta = Geom.polarToCartesian(angle, d);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + delta.x, y + delta.y);
ctx.strokeStyle = color;
ctx.stroke();
}
var drawRobot = function(robot, color) {
var x = offsetX(robot['pos-x']);
var y = offsetY(robot['pos-y']);
fillSquare(x, y, robotDisplayRadius * 2, color);
strokeCircle(x, y, robotDisplayRadius * 0.6, gunDisplayWidth * 0.3);
drawLinePolar(x, y, robot['aim'], gunDisplayLength, gunDisplayWidth, color);
}
var drawShell = function(shell) {
var x = offsetX(shell['pos-x']);
var y = offsetY(shell['pos-y']);
fillCircle(x, y, shellDisplayRadius, SHELL_COLOR);
}
var explodeShell = function(shell) {
var x = offsetX(shell['pos-x']);
var y = offsetY(shell['pos-y']);
fillCircle(x, y, shellDisplayRadius * 10, SHELL_COLOR);
}
var animateWorld = function(previousWorld, currentWorld) {
ctx.clearRect(0, 0, width, height);
var currentShells = currentWorld["shells"];
var previousShells = previousWorld["shells"];
for (key in previousShells) {
if (previousShells.hasOwnProperty(key)) {
if (currentShells.hasOwnProperty(key)) {
drawShell(previousShells[key]);
}
else {
explodeShell(previousShells[key]);
}
}
}
currentWorld.robots.forEach(function(robot, idx) {
if (previousWorld.robots[idx]["damage"] !== robot["damage"]) {
drawRobot(robot, "#fff");
}
else {
drawRobot(robot, ROBOT_COLORS[idx]);
}
});
if (currentWorld["next-shell-id"] !== previousWorld["next-shell-id"]) {
soundPlayers.shellRelease.play();
}
}
return {
animateWorld: animateWorld
};
}
function loop(worlds, interval, callback) {
(function continueLoop(tick) {
if (worlds.isFinished()) {
return;
}
callback();
var nextTick = tick + interval;
setTimeout(function() {
continueLoop(nextTick);
}, nextTick - Date.now());
})(Date.now());
}
function startGame(worlds) {
var gameInfo = worlds.getGameInfo();
var debugAnimationCounter = 0;
var debugSimulationCounter = 0;
var debugSecondsCounter = 0;
var debugStartTime = Date.now();
var fastForward = STARTING_FAST_FORWARD;
var tickDuration = parseInt (gameInfo.gameSecondsPerTick * 1000);
var frameDuration = parseInt (1000 / FPS);
var canvasEl = $('#canvas')[0];
var animation = new Animation(canvasEl, SOUNDS, gameInfo);
// TODO: remove this tick loop entirely,
// and just have the animation loop calculate which
// simulation to pick each time.
loop(worlds, tickDuration, function() {
debugSimulationCounter++;
worlds.advance(fastForward);
});
loop(worlds, frameDuration, function() {
debugAnimationCounter++;
animation.animateWorld(worlds.getPreviousWorld(), worlds.getCurrentWorld());
});
loop(worlds, 1000, function() {
debugSecondsCounter++;
console.log(Math.floor((Date.now() - debugStartTime) / 1000) +
" " + debugAnimationCounter +
" " + debugSimulationCounter);
});
// Keyboard event listener for fast-forward control
// TODO: dispose of this if we start a new worlds object.
// Or just have it not mutate fastForward.
$('body').bind('keydown', function(event) {
if (event.which === 37) {
fastForward = Math.max(fastForward - 1, 1);
}
if (event.which === 39) {
fastForward = Math.min(fastForward + 1, MAX_FAST_FORWARD);
}
console.log("fast forward: " + fastForward);
});
}
function init() {
var worlds;
// Text and keyboard event listeners for sending program names to server
$('#programsInput').bind('keydown', function(event) {
if (event.which === 13) {
event.stopPropagation();
event.preventDefault();
if (worlds) {
worlds.finish();
}
// css animation and blurring of input box
// (takes 1000 milliseconds total)
$('.instruction-box').css({ height: 0 });
setTimeout(function() {
$('#canvas').css({ opacity: 1 });
console.log("finished hiding instruction box");
console.log(Date.now());
}, 500);
$(this).blur();
var programs = this.value;
worlds = new Worlds(programs, BUFFER_LENGTH);
// wait till at least the animation is done (1000 milliseconds)
// before we start the battle
setTimeout(function() {
worlds.onLoaded.done(function() {
startGame(worlds);
console.log("finished loading first worlds");
console.log(Date.now());
});
console.log("finished showing arena");
console.log(Date.now());
}, 1000);
}
});
// Fetch list of robots for user
$.getJSON('program-names', function(data) {
$('#programNames').text(data.names.join(", "));
$('body').css({display: 'block'});
});
}
// Fire it up.
$(init);
})();