mirror of
https://github.com/richardharrington/robotwar.git
synced 2024-05-28 08:41:28 +00:00
366 lines
12 KiB
JavaScript
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);
|
|
|
|
})();
|