<commit_before>/**
 * Multiple attachment basic JavaScript file, contains the entire UI functions
 *
 * @package Wedgward:MassAttach
 * @author Shitiz "Dragooon" Garg <Email mail@dragooon.net> <Url http://smf-media.com> (and Nao)
 * @copyright 2012, Shitiz "Dragooon" Garg <mail@dragooon.net>
 * @license
 *		Licensed under "New BSD License (3-clause version)"
 *		http://www.opensource.org/licenses/BSD-3-Clause
 *
 * @version 1.0
 */

@language Wedgeward:MassAttach:plugin;

// One method would've been to hook into Wedge's attach functions, but since there are quite a lot of fundamental differences
// between the workings, I decided to write my own instead.
$(function (jQuery, undefined)
{
	// No point in this if we cannot support XHR level 2 upload
	if (!(window.ProgressEvent && window.FormData && $.support.cors))
		return true;

	var
		$element = $('#attachments_container > input[type=file]:first'),
		$files = [],
		$current = -1,
		$is_uploading = false,
		total_size = 0,
		xhr = null;

	$element
		// Release this input of the default chains, we got new ones!
		.off('change')
		// Update this element to support multiple attachments
		.attr('name', 'attachment_holder')
		// Bind the "change" event to properly handle multiple attachments into upload
		.change(function () { return attachFiles(this.files || {}, 0); });

	$('

' + $txt['massattach_drag_help'] + '

' + $txt['massattach_drag_help_subtext'] + '

')
		.hide()
		.prependTo($element.parent());

	var dragUIOpened = false, dragTimer = +new Date();

	$(document.body)
		.on('dragover', function (e)
		{
			e.originalEvent.dataTransfer.dropEffect = 'none';

			// Expand the additional option if it's collapsed
			if (!dragUIOpened)
			{
				if (!$('#postAttachment2').is(':visible'))
					$('#postMoreExpandLink').data('that').toggle();

				// Show a neat "Drop the file here" notice
				$element.fadeOut('fast', function () { $('#dropnotice').fadeIn(); });
				dragUIOpened = true;
			}
			dragTimer = +new Date();

			return false;
		})
		.on('dragleave', function ()
		{
			setTimeout(function ()
			{
				if ((+new Date()) - dragTimer > 200)
				{
					$('#dropnotice').fadeOut('fast', function () { $element.fadeIn(); });
					dragUIOpened = false;
				}
			}, 200);
		});

	// Bind the form to prevent accidental submitting when uploading.
	$('#postmodify').on('submit', function (e)
	{
		if ($is_uploading)
		{
			say($txt['massattach_currently_uploading']);
			return false;
		}
	});

	$('#dropnotice')
		.on('dragover', function (e)
		{
			dragTimer = +new Date();
			e.originalEvent.dataTransfer.dropEffect = 'copy';

			return false;
		})
		.on('drop', function (e)
		{
			// Make sure we are dragging a file over
			if (!e.originalEvent.dataTransfer && !(dt.files || (!is_webkit && e.originalEvent.dataTransfer.types.contains && e.originalEvent.dataTransfer.types.contains('Files'))))
				return false;

			dragUIOpened = false;

			var files = e.originalEvent.dataTransfer.files;
			$('#dropnotice').fadeOut('fast', function ()
			{
				$element.fadeIn(function () { attachFiles(files, 0); });
			});

			return false;
		});

	var startUpload = function ()
	{
		if ($is_uploading)
			return true;

		// Are we done?
		if ($files[++$current] === undefined)
		{
			$current--;
			return true;
		}

		$is_uploading = true;
		var
			$timer = +new Date(),
			$progress = $('

')
				.appendTo($files[$current].element);

		xhr = new XMLHttpRequest();
		xhr.open('POST', weUrl('action=massattach;board=' + we_board));
		xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		xhr.setRequestHeader('X-File-Name', $files[$current].fileName || $files[$current].name);
		xhr.setRequestHeader('Content-Type', 'application/octet-stream');
		xhr.upload.onprogress = function (e)
		{
			if (e.lengthComputable && (+new Date()) - $timer > 500)
			{
				$timer = +new Date();
				$progress.find('.plainbox').width((e.loaded / e.total) * 150);
			}
		};
		xhr.onreadystatechange = function (e)
		{
			if (xhr.readyState == 4 && xhr.status == 200)
			{
				var $response = $.parseJSON(xhr.responseText);
				$progress.remove();

				// !! @todo: still needs to be able to handle removal requests...
				if ($response.valid)
				{
					$files[$current].element
						.find('.delete').val(we_delete).end()
						.find('span').css('font-style', '');
					$('input[name="attach_del\[\]"]').last().closest('dd').after('
<input type="checkbox" id="attachment_' + $response.id + '" name="attach_del[]" value="' + $response.id + '" checked onclick="oAttach().checkActive();" /> ' + $response.name + '
');
				}
				else
					$files[$current].element
						.find('.delete').remove().end()
						.find('span').css('color', 'red')
						.append('
' + $response.error);

				// Move onto the next file
				$is_uploading = false;
				startUpload();
			}
		};

		xhr.send($files[$current]);
	},

	attachFiles = function (files, i)
	{
		if (files[i] === undefined)
		{
			// Since we automatically take the files and upload them, the file input
			// should be empty so that we don't upload on POST
			$clone = $element.clone(true);
			$('<form></form>').append($clone)[0].reset();
			$element.before($clone).detach();
			$.cleanData($element);
			$element = $clone;

			return true;
		}

		// Check for file's extension
		var
			filename = files[i].fileName || files[i].name,
			filesize = files[i].fileSize || files[i].size,
			extension = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();

		if (attachOpts.checkExtension && !in_array(extension, attachOpts.validExtensions))
		{
			say(attachOpts.ext_error.replace('{ext}', extension));
			return attachFiles(files, ++i);
		}

		// Check number of files
		if (attachOpts.maxNum > 0 && attachOpts.currentNum + $files.length > attachOpts.maxNum)
		{
			say(attachOpts.maxNum_error);
			return;
		}

		// Check for file's size
		if (attachOpts.sizeLimit > 0 && filesize / 1024 > attachOpts.sizeLimit)
		{
			say(attachOpts.filesize_error);
			return attachFiles(files, ++i);
		}

		// Check total file's size
		if (attachOpts.totalSizeLimit > 0 && (filesize / 1024 + attachOpts.totalSize + total_size) > attachOpts.totalSizeLimit)
		{
			say(attachOpts.totalFilesize_error);
			return;
		}

		var $container = $('
');

		$('<input type="button" class="delete" style="margin-top: 4px">')
			.val(we_cancel)
			.click(function ()
			{
				var i = $(this).parent().data('id');

				$(this).parent().remove();

				$files.splice(i, 1);

				// Fix all the IDs to correctly match their array index
				for (j = 0; j < $files.length; j++)
					$files[j].element.data('id', j);

				// This the one being uploaded?
				if (i == $current && $is_uploading)
				{
					xhr.abort();
					$is_uploading = false;
					$current--;
					startUpload();
				}
			})
			.appendTo($container);

		$('')
			.text(files[i].fileName || files[i].name)
			.appendTo($container);

		if (/^image\//.test(files[i].type))
		{
			var imgPreview = new FileReader();

			if (imgPreview)
			{
				imgPreview.onload = function (e)
				{
					$('')
						.appendTo($container)
						.load(function () {
							$(this)
								.height(Math.min($(this).height(), 50))
								.show();
						})
						.attr('src', e.target.result)
						.hide();
				};
				imgPreview.readAsDataURL(files[i]);
			}
		}

		$container.appendTo($element.parent());
		$files[$files.length] = files[i];
		$files[$files.length - 1].element = $container;
		$container.data('id', $files.length - 1);
		total_size += filesize / 1024;

		// Always start upload automatically, it'll automatically skip if in progress
		startUpload();

		return attachFiles(files, ++i);
	};
});
<commit_msg>Make sure drop notice is visible only while an actual file is being dragged<commit_after>/**
 * Multiple attachment basic JavaScript file, contains the entire UI functions
 *
 * @package Wedgward:MassAttach
 * @author Shitiz "Dragooon" Garg <Email mail@dragooon.net> <Url http://smf-media.com> (and Nao)
 * @copyright 2012, Shitiz "Dragooon" Garg <mail@dragooon.net>
 * @license
 *		Licensed under "New BSD License (3-clause version)"
 *		http://www.opensource.org/licenses/BSD-3-Clause
 *
 * @version 1.0
 */

@language Wedgeward:MassAttach:plugin;

// One method would've been to hook into Wedge's attach functions, but since there are quite a lot of fundamental differences
// between the workings, I decided to write my own instead.
$(function (jQuery, undefined)
{
	// No point in this if we cannot support XHR level 2 upload
	if (!(window.ProgressEvent && window.FormData && $.support.cors))
		return true;

	var
		$element = $('#attachments_container > input[type=file]:first'),
		$files = [],
		$current = -1,
		$is_uploading = false,
		total_size = 0,
		xhr = null;

	$element
		// Release this input of the default chains, we got new ones!
		.off('change')
		// Update this element to support multiple attachments
		.attr('name', 'attachment_holder')
		// Bind the "change" event to properly handle multiple attachments into upload
		.change(function () { return attachFiles(this.files || {}, 0); });

	$('

' + $txt['massattach_drag_help'] + '

' + $txt['massattach_drag_help_subtext'] + '

')
		.hide()
		.prependTo($element.parent());

	var dragUIOpened = false, dragTimer = +new Date();

	$(document.body)
		.on('dragover', function (e)
		{
			// Make sure we are dragging a file over
			var dt = e.originalEvent.dataTransfer;
			if (dt.types == null || !(dt.types.indexOf ? dt.types.indexOf('Files') != -1 : dt.types.contains('application/x-moz-file')))
				return true;

			e.originalEvent.dataTransfer.dropEffect = 'none';

			// Expand the additional option if it's collapsed
			if (!dragUIOpened)
			{
				if (!$('#postAttachment2').is(':visible'))
					$('#postMoreExpandLink').data('that').toggle();

				// Show a neat "Drop the file here" notice
				$element.fadeOut('fast', function () { $('#dropnotice').fadeIn(); });
				dragUIOpened = true;
			}
			dragTimer = +new Date();

			return false;
		})
		.on('dragleave', function ()
		{
			setTimeout(function ()
			{
				if ((+new Date()) - dragTimer > 200)
				{
					$('#dropnotice').fadeOut('fast', function () { $element.fadeIn(); });
					dragUIOpened = false;
				}
			}, 200);
		});

	// Bind the form to prevent accidental submitting when uploading.
	$('#postmodify').on('submit', function (e)
	{
		if ($is_uploading)
		{
			say($txt['massattach_currently_uploading']);
			return false;
		}
	});

	$('#dropnotice')
		.on('dragover', function (e)
		{
			dragTimer = +new Date();
			e.originalEvent.dataTransfer.dropEffect = 'copy';

			return false;
		})
		.on('drop', function (e)
		{
			var dt = e.originalEvent.dataTransfer;

			// Make sure we are dropping a file over
			if (!dt || !(dt.files || (!is_webkit && dt.types.contains && dt.types.contains('Files'))))
				return false;

			dragUIOpened = false;

			var files = dt.files;
			$('#dropnotice').fadeOut('fast', function ()
			{
				$element.fadeIn(function () { attachFiles(files, 0); });
			});

			return false;
		});

	var startUpload = function ()
	{
		if ($is_uploading)
			return true;

		// Are we done?
		if ($files[++$current] === undefined)
		{
			$current--;
			return true;
		}

		$is_uploading = true;
		var
			$timer = +new Date(),
			$progress = $('

')
				.appendTo($files[$current].element);

		xhr = new XMLHttpRequest();
		xhr.open('POST', weUrl('action=massattach;board=' + we_board));
		xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		xhr.setRequestHeader('X-File-Name', $files[$current].fileName || $files[$current].name);
		xhr.setRequestHeader('Content-Type', 'application/octet-stream');
		xhr.upload.onprogress = function (e)
		{
			if (e.lengthComputable && (+new Date()) - $timer > 500)
			{
				$timer = +new Date();
				$progress.find('.plainbox').width((e.loaded / e.total) * 150);
			}
		};
		xhr.onreadystatechange = function (e)
		{
			if (xhr.readyState == 4 && xhr.status == 200)
			{
				var $response = $.parseJSON(xhr.responseText);
				$progress.remove();

				// !! @todo: still needs to be able to handle removal requests...
				if ($response.valid)
				{
					$files[$current].element
						.find('.delete').val(we_delete).end()
						.find('span').css('font-style', '');
					$('input[name="attach_del\[\]"]').last().closest('dd').after('
<input type="checkbox" id="attachment_' + $response.id + '" name="attach_del[]" value="' + $response.id + '" checked onclick="oAttach().checkActive();" /> ' + $response.name + '
');
				}
				else
					$files[$current].element
						.find('.delete').remove().end()
						.find('span').css('color', 'red')
						.append('
' + $response.error);

				// Move onto the next file
				$is_uploading = false;
				startUpload();
			}
		};

		xhr.send($files[$current]);
	},

	attachFiles = function (files, i)
	{
		if (files[i] === undefined)
		{
			// Since we automatically take the files and upload them, the file input
			// should be empty so that we don't upload on POST
			$clone = $element.clone(true);
			$('<form></form>').append($clone)[0].reset();
			$element.before($clone).detach();
			$.cleanData($element);
			$element = $clone;

			return true;
		}

		// Check for file's extension
		var
			filename = files[i].fileName || files[i].name,
			filesize = files[i].fileSize || files[i].size,
			extension = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();

		if (attachOpts.checkExtension && !in_array(extension, attachOpts.validExtensions))
		{
			say(attachOpts.ext_error.replace('{ext}', extension));
			return attachFiles(files, ++i);
		}

		// Check number of files
		if (attachOpts.maxNum > 0 && attachOpts.currentNum + $files.length > attachOpts.maxNum)
		{
			say(attachOpts.maxNum_error);
			return;
		}

		// Check for file's size
		if (attachOpts.sizeLimit > 0 && filesize / 1024 > attachOpts.sizeLimit)
		{
			say(attachOpts.filesize_error);
			return attachFiles(files, ++i);
		}

		// Check total file's size
		if (attachOpts.totalSizeLimit > 0 && (filesize / 1024 + attachOpts.totalSize + total_size) > attachOpts.totalSizeLimit)
		{
			say(attachOpts.totalFilesize_error);
			return;
		}

		var $container = $('
');

		$('<input type="button" class="delete" style="margin-top: 4px">')
			.val(we_cancel)
			.click(function ()
			{
				var i = $(this).parent().data('id');

				$(this).parent().remove();

				$files.splice(i, 1);

				// Fix all the IDs to correctly match their array index
				for (j = 0; j < $files.length; j++)
					$files[j].element.data('id', j);

				// This the one being uploaded?
				if (i == $current && $is_uploading)
				{
					xhr.abort();
					$is_uploading = false;
					$current--;
					startUpload();
				}
			})
			.appendTo($container);

		$('')
			.text(files[i].fileName || files[i].name)
			.appendTo($container);

		if (/^image\//.test(files[i].type))
		{
			var imgPreview = new FileReader();

			if (imgPreview)
			{
				imgPreview.onload = function (e)
				{
					$('')
						.appendTo($container)
						.load(function () {
							$(this)
								.height(Math.min($(this).height(), 50))
								.show();
						})
						.attr('src', e.target.result)
						.hide();
				};
				imgPreview.readAsDataURL(files[i]);
			}
		}

		$container.appendTo($element.parent());
		$files[$files.length] = files[i];
		$files[$files.length - 1].element = $container;
		$container.data('id', $files.length - 1);
		total_size += filesize / 1024;

		// Always start upload automatically, it'll automatically skip if in progress
		startUpload();

		return attachFiles(files, ++i);
	};
});
<|endoftext|>


<commit_before>const url = require('url');
const WebSocket = require('ws');

var fs = require('fs'); // Node.js File System

const Rooms = require('./rooms');

// Enum of game modes.
var Mode = {
    START_ROOM: 1,
    JOIN_ROOM: 2,
    JOIN_RANDOM: 3,
};

var wss;

/* Initializes the WebSocket on the given server. */
function start(server) {
	wss = new WebSocket.Server({ server, perMessageDeflate: false });
	wss.on('connection', onConnection);
}

/* Called when a client connects to the WebSocket. */
function onConnection(client) {
	// Set the event listeners.
	client.on('message', function(message) {onIncomingMessage(message, client)});
	client.on('close', onClose);

	client.send('Connection opened.');
}

/* Called when the server receives a message from a client. */
function onIncomingMessage(message, client) {
	try {
		// First try parsing the data as JSON.
		var json = JSON.parse(message);
		handleClientJson(client, json);
	} catch (error) {
		if (error instanceof SyntaxError) {
			// If it's not JSON, just broadcast the message.
			broadcastText(client, message);
		} else {
			// If the message was JSON, but an unrelated error happened.
			throw error;
		}
	}
}

/* Handle JSON sent from a client. */
function handleClientJson(client, json) {
	if (json.mode) {
        if (json.mode == Mode.START_ROOM) {
            startRoomWithFriends(client);
		} else if (json.mode == Mode.JOIN_ROOM) {
            joinRoomWithFriends(client, json.roomCode);
		} else if (json.mode == Mode.JOIN_RANDOM) {
            joinRandom(client);
		}
	}

    if (json.hasOwnProperty('readyToVote')) {
        Rooms.setPlayerAsReady(client, json.readyToVote);
    }

    if (json.ballot) {
        Rooms.submitBallot(client, json.ballot);
    }

	if (json.message) {
		broadcastText(client, json.message);
	}
}

/* Broadcast text to all the clients in the sender's room. */
function broadcastText(sender, message) {
    var room = Rooms.getRoomOfClient(sender);
	room.broadcast(message, sender);
    fs.appendFile('./game/bots/recentmessages.txt', message + '\n', 'utf8');
}

/* Called when a client closes the WebSocket connection. */
function onClose(code, reason) {
    console.log("A client disconnected. Code: " + code + ". Reason: " + reason);
    // Unfortunately, we cannot tell which client disconnected.
}

function joinRandom(client) {
    // Keep track of the newly joined client.
    var room = Rooms.getWaitingRoom();
	Rooms.addClientToRoom(client, room);
}

function startRoomWithFriends(client) {
    var code = Rooms.newRoomCode();
    var room = Rooms.getRoomByCode(code);
    Rooms.addClientToRoom(client, room);
    sendCodeToClient(code, client);
}

function sendCodeToClient(code, client) {
    var payload = {'roomCode': code};
    if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify(payload));
    }
}

function joinRoomWithFriends(client, roomCode) {
    if (Rooms.validCode(roomCode)) {
        var room = Rooms.getRoomByCode(roomCode);
        Rooms.addClientToRoom(client, room);
        sendJoinReceipt(client);
    } else {
        // If we can't find the room, display an error to the user.
        clientError(client, "That room does not exist.");
    }
}

// Tell the client that they have successfully joined a room.
function sendJoinReceipt(client) {
    if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({"joinedRoom": true}));
    }
}

function clientError(client, message) {
    if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({"error": message}));
    }
}

module.exports = {start: start};
<commit_msg>Now prunes old messages when the log file gets large<commit_after>const url = require("url");
const WebSocket = require("ws");
var fs = require("fs"); // Node.js File System
var Transform = require("stream").Transform; // Node.js Transform stream
const util = require("util");
const Rooms = require("./rooms");

const FIVE_MEGABYTES = 5243000;

const messageHistory = "./game/bots/recentmessages.txt";

// Enum of game modes.
var Mode = {
	START_ROOM: 1,
	JOIN_ROOM: 2,
	JOIN_RANDOM: 3,
};

var wss;

/* Initializes the WebSocket on the given server. */
function start(server) {
	wss = new WebSocket.Server({ server, perMessageDeflate: false });
	wss.on("connection", onConnection);
}

/* Called when a client connects to the WebSocket. */
function onConnection(client) {
	// Set the event listeners.
	client.on("message", function(message) {onIncomingMessage(message, client)});
	client.on("close", onClose);

	client.send("Connection opened.");
}

/* Called when the server receives a message from a client. */
function onIncomingMessage(message, client) {
	try {
		// First try parsing the data as JSON.
		var json = JSON.parse(message);
		handleClientJson(client, json);
	} catch (error) {
		if (error instanceof SyntaxError) {
			// If it's not JSON, just broadcast the message.
			broadcastText(client, message);
		} else {
			// If the message was JSON, but an unrelated error happened.
			throw error;
		}
	}
}

/* Handle JSON sent from a client. */
function handleClientJson(client, json) {
	if (json.mode) {
		if (json.mode == Mode.START_ROOM) {
			startRoomWithFriends(client);
		} else if (json.mode == Mode.JOIN_ROOM) {
			joinRoomWithFriends(client, json.roomCode);
		} else if (json.mode == Mode.JOIN_RANDOM) {
			joinRandom(client);
		}
	}

	if (json.hasOwnProperty("readyToVote")) {
		Rooms.setPlayerAsReady(client, json.readyToVote);
	}

	if (json.ballot) {
		Rooms.submitBallot(client, json.ballot);
	}

	if (json.message) {
		broadcastText(client, json.message);
	}
}

/* Broadcast text to all the clients in the sender's room. */
function broadcastText(sender, message) {
	var room = Rooms.getRoomOfClient(sender);
	room.broadcast(message, sender);

	appendMessageToLog(messageHistory, message);
}

/* Called when a client closes the WebSocket connection. */
function onClose(code, reason) {
	console.log("A client disconnected. Code: " + code + ". Reason: " + reason);
	// Unfortunately, we cannot tell which client disconnected.
}

function joinRandom(client) {
	// Keep track of the newly joined client.
	var room = Rooms.getWaitingRoom();
	Rooms.addClientToRoom(client, room);
}

function startRoomWithFriends(client) {
	var code = Rooms.newRoomCode();
	var room = Rooms.getRoomByCode(code);
	Rooms.addClientToRoom(client, room);
	sendCodeToClient(code, client);
}

function sendCodeToClient(code, client) {
	var payload = {'roomCode': code};
	if (client.readyState === WebSocket.OPEN) {
		client.send(JSON.stringify(payload));
	}
}

function joinRoomWithFriends(client, roomCode) {
	if (Rooms.validCode(roomCode)) {
		var room = Rooms.getRoomByCode(roomCode);
		Rooms.addClientToRoom(client, room);
		sendJoinReceipt(client);
	} else {
		// If we can't find the room, display an error to the user.
		clientError(client, "That room does not exist.");
	}
}

// Tell the client that they have successfully joined a room.
function sendJoinReceipt(client) {
	if (client.readyState === WebSocket.OPEN) {
		client.send(JSON.stringify({"joinedRoom": true}));
	}
}

function clientError(client, message) {
	if (client.readyState === WebSocket.OPEN) {
		client.send(JSON.stringify({"error": message}));
	}
}

function appendMessageToLog(filename, message) {
	// Delete oldest messages, ten at a time
	// until the size is below an arbitrary limit
	while (fs.stat(filename).size > FIVE_MEGABYTES) {
		removeFirstLines(filename, 10);
	}

	// Append the message to the text log of recent messages
	fs.appendFile(filename, message + "\n", "utf8");
}

function removeFirstLines(filename, numLines) {
	for (var i = 0; i < numLines; i++) {
		var input = fs.createReadStream(filename);
		var output = fs.createWriteStream(filename);

		input.pipe(RemoveFirstLine()).pipe(output);
	}
}

function RemoveFirstLine(args) {
    if (! (this instanceof RemoveFirstLine)) {
        return new RemoveFirstLine(args);
    }
    Transform.call(this, args);
    this._buff = '';
    this._removed = false;
}
util.inherits(RemoveFirstLine, Transform);

RemoveFirstLine.prototype._transform = function(chunk, encoding, done) {
    if (this._removed) { // if already removed
        this.push(chunk); // just push through buffer
    } else {
        // collect string into buffer
        this._buff += chunk.toString();

        // check if string has newline symbol
        if (this._buff.indexOf('\n') !== -1) {
            // push to stream skipping first line
            this.push(this._buff.slice(this._buff.indexOf('\n') + 2));
            // clear string buffer
            this._buff = null;
            // mark as removed
            this._removed = true;
        }
    }
    done();
};

module.exports = {start: start};
<|endoftext|>


<commit_before>// ==UserScript==
// @name Monster Minigame Auto-script
// @namespace https://github.com/mouseas/steamSummerMinigame
// @description A script that runs the Steam Monster Minigame for you.
// @version 1.0
// @match http://steamcommunity.com/minigame/towerattack*
// @updateURL https://raw.githubusercontent.com/mouseas/steamSummerMinigame/master/autoPlay.js
// @downloadURL https://raw.githubusercontent.com/mouseas/steamSummerMinigame/master/autoPlay.js
// ==/UserScript==

// IMPORTANT: Update the @version property above to a higher number such as 1.1 and 1.2 when you update the script! Otherwise, Tamper / Greasemonkey users will not update automatically.

var isAlreadyRunning = false;

// disable particle effects - this drastically reduces the game's memory leak
if (window.g_Minigame !== undefined) {
	window.g_Minigame.CurrentScene().DoClickEffect = function() {};
	window.g_Minigame.CurrentScene().SpawnEmitter = function(emitter) {
		emitter.emit = false;
		return emitter;
	}
}

if (thingTimer !== undefined) {
	window.clearTimeout(thingTimer);
}

function doTheThing() {
	if (isAlreadyRunning || g_Minigame === undefined || !g_Minigame.CurrentScene().m_bRunning || !g_Minigame.CurrentScene().m_rgPlayerTechTree) {
		return;
	}
	isAlreadyRunning = true;

	goToLaneWithBestTarget();

	useGoodLuckCharmIfRelevant();
	useMedicsIfRelevant();

	// TODO use abilities if available and a suitable target exists
	// - Tactical Nuke on a Spawner if below 50% and above 25% of its health
	// - Cluster Bomb and Napalm if the current lane has a spawner and 2+ creeps
	// - Metal Detector if a boss, miniboss, or spawner death is imminent (predicted in > 2 and < 7 seconds)
	// - Morale Booster if available and lane has > 2 live enemies
	// - Decrease Cooldowns if another player used a long-cooldown ability < 10 seconds ago (any ability but Medics or a consumable)

	// TODO purchase abilities and upgrades intelligently

	attemptRespawn();

	isAlreadyRunning = false;
}

function goToLaneWithBestTarget() {
	// We can overlook spawners if all spawners are 40% hp or higher and a creep is under 10% hp
	var spawnerOKThreshold = 0.4;
	var creepSnagThreshold = 0.1;

	var targetFound = false;
	var lowHP = 0;
	var lowLane = 0;
	var lowTarget = 0;
	var lowPercentageHP = 0;

	var ENEMY_TYPE = {
		"SPAWNER":0,
		"CREEP":1,
		"BOSS":2,
		"MINIBOSS":3,
		"TREASURE":4
	}

	// determine which lane and enemy is the optimal target
	var enemyTypePriority = [
		ENEMY_TYPE.TREASURE,
		ENEMY_TYPE.BOSS,
		ENEMY_TYPE.MINIBOSS,
		ENEMY_TYPE.SPAWNER,
		ENEMY_TYPE.CREEP
		];

	var skippingSpawner = false;
	var skippedSpawnerLane = 0;
	var skippedSpawnerTarget = 0;

	for (var k = 0; !targetFound && k < enemyTypePriority.length; k++) {
		var enemies = [];

		// gather all the enemies of the specified type.
		for (var i = 0; i < 3; i++) {
			for (var j = 0; j < 4; j++) {
				var enemy = g_Minigame.CurrentScene().GetEnemy(i, j);
				if (enemy && enemy.m_data.type == enemyTypePriority[k]) {
					enemies[enemies.length] = enemy;
				}
			}
		}

		// target the enemy of the specified type with the lowest hp
		for (var i = 0; i < enemies.length; i++) {
			if (enemies[i] && !enemies[i].m_bIsDestroyed) {
				if(lowHP < 1 || enemies[i].m_flDisplayedHP < lowHP) {
					targetFound = true;
					lowHP = enemies[i].m_flDisplayedHP;
					lowLane = enemies[i].m_nLane;
					lowTarget = enemies[i].m_nID;
				}
				var percentageHP = enemies[i].m_flDisplayedHP / enemies[i].m_data.max_hp;
				if(lowPercentageHP == 0 || percentageHP < lowPercentageHP) {
					lowPercentageHP = percentageHP;
				}
			}
		}

		// If we just finished looking at spawners,
		// AND none of them were below our threshold,
		// remember them and look for low creeps (so don't quit now)
		if (enemyTypePriority[k] == ENEMY_TYPE.SPAWNER && lowPercentageHP > spawnerOKThreshold) {
			skippedSpawnerLane = lowLane;
			skippedSpawnerTarget = lowTarget;
			skippingSpawner = true;
			targetFound = false;
		}

		// If we skipped a spawner and just finished looking at creeps,
		// AND the lowest was above our snag threshold,
		// just go back to the spawner!
		if (skippingSpawner && enemyTypePriority[k] == ENEMY_TYPE.CREEP && lowPercentageHP > creepSnagThreshold ) {
			lowLane = skippedSpawnerLane;
			lowTarget = skippedSpawnerTarget;
		}
	}

	// go to the chosen lane
	if (targetFound) {
		if (g_Minigame.CurrentScene().m_nExpectedLane != lowLane) {
			//console.log('switching langes');
			g_Minigame.CurrentScene().TryChangeLane(lowLane);
		}

		// target the chosen enemy
		if (g_Minigame.CurrentScene().m_nTarget != lowTarget) {
			//console.log('switching targets');
			g_Minigame.CurrentScene().TryChangeTarget(lowTarget);
		}
	}
}

function useMedicsIfRelevant() {
	var myMaxHealth = g_Minigame.CurrentScene().m_rgPlayerTechTree.max_hp;

	// check if health is below 50%
	var hpPercent = g_Minigame.CurrentScene().m_rgPlayerData.hp / myMaxHealth;
	if (hpPercent > 0.5 || g_Minigame.CurrentScene().m_rgPlayerData.hp < 1) {
		return; // no need to heal - HP is above 50% or already dead
	}

	// check if Medics is purchased and cooled down
	if (hasPurchasedAbility(7)) {

		if (isAbilityCoolingDown(7)) {
			return;
		}

		// Medics is purchased, cooled down, and needed. Trigger it.
		console.log('Medics is purchased, cooled down, and needed. Trigger it.');
		triggerAbility(7);
	}
}

// Use Good Luck Charm if doable
function useGoodLuckCharmIfRelevant() {
	// check if Good Luck Charms is purchased and cooled down
	if (hasPurchasedAbility(6)) {
		if (isAbilityCoolingDown(6)) {
			return;
		}

		// Good Luck Charms is purchased, cooled down, and needed. Trigger it.
		console.log('Good Luck Charms is purchased, cooled down, and needed. Trigger it.');
		triggerAbility(6);
	}
}

//If player is dead, call respawn method
function attemptRespawn() {
	if ((g_Minigame.CurrentScene().m_bIsDead) &&
			((g_Minigame.CurrentScene().m_rgPlayerData.time_died * 1000) + 5000) < (new Date().getTime())) {
		RespawnPlayer();
	}
}

function isAbilityCoolingDown(abilityId) {
	return g_Minigame.CurrentScene().GetCooldownForAbility(abilityId) > 0;
}

function hasPurchasedAbility(abilityId) {
	// each bit in unlocked_abilities_bitfield corresponds to an ability.
	// the above condition checks if the ability's bit is set or cleared. I.e. it checks if
	// the player has purchased the specified ability.
	return (1 << abilityId) & g_Minigame.CurrentScene().m_rgPlayerTechTree.unlocked_abilities_bitfield;
}

function triggerAbility(abilityId) {
	var elem = document.getElementById('ability_' + abilityId);
	if (elem && elem.childElements() && elem.childElements().length >= 1) {
		g_Minigame.CurrentScene().TryAbility(document.getElementById('ability_' + abilityId).childElements()[0]);
	}
}

var thingTimer = window.setInterval(doTheThing, 1000);
<commit_msg>Increment version number to trigger Greasemonkey/Tampermonkey auto update.<commit_after>// ==UserScript==
// @name Monster Minigame Auto-script
// @namespace https://github.com/mouseas/steamSummerMinigame
// @description A script that runs the Steam Monster Minigame for you.
// @version 1.1
// @match http://steamcommunity.com/minigame/towerattack*
// @updateURL https://raw.githubusercontent.com/mouseas/steamSummerMinigame/master/autoPlay.js
// @downloadURL https://raw.githubusercontent.com/mouseas/steamSummerMinigame/master/autoPlay.js
// ==/UserScript==

// IMPORTANT: Update the @version property above to a higher number such as 1.1 and 1.2 when you update the script! Otherwise, Tamper / Greasemonkey users will not update automatically.

var isAlreadyRunning = false;

// disable particle effects - this drastically reduces the game's memory leak
if (window.g_Minigame !== undefined) {
	window.g_Minigame.CurrentScene().DoClickEffect = function() {};
	window.g_Minigame.CurrentScene().SpawnEmitter = function(emitter) {
		emitter.emit = false;
		return emitter;
	}
}

if (thingTimer !== undefined) {
	window.clearTimeout(thingTimer);
}

function doTheThing() {
	if (isAlreadyRunning || g_Minigame === undefined || !g_Minigame.CurrentScene().m_bRunning || !g_Minigame.CurrentScene().m_rgPlayerTechTree) {
		return;
	}
	isAlreadyRunning = true;

	goToLaneWithBestTarget();

	useGoodLuckCharmIfRelevant();
	useMedicsIfRelevant();

	// TODO use abilities if available and a suitable target exists
	// - Tactical Nuke on a Spawner if below 50% and above 25% of its health
	// - Cluster Bomb and Napalm if the current lane has a spawner and 2+ creeps
	// - Metal Detector if a boss, miniboss, or spawner death is imminent (predicted in > 2 and < 7 seconds)
	// - Morale Booster if available and lane has > 2 live enemies
	// - Decrease Cooldowns if another player used a long-cooldown ability < 10 seconds ago (any ability but Medics or a consumable)

	// TODO purchase abilities and upgrades intelligently

	attemptRespawn();

	isAlreadyRunning = false;
}

function goToLaneWithBestTarget() {
	// We can overlook spawners if all spawners are 40% hp or higher and a creep is under 10% hp
	var spawnerOKThreshold = 0.4;
	var creepSnagThreshold = 0.1;

	var targetFound = false;
	var lowHP = 0;
	var lowLane = 0;
	var lowTarget = 0;
	var lowPercentageHP = 0;

	var ENEMY_TYPE = {
		"SPAWNER":0,
		"CREEP":1,
		"BOSS":2,
		"MINIBOSS":3,
		"TREASURE":4
	}

	// determine which lane and enemy is the optimal target
	var enemyTypePriority = [
		ENEMY_TYPE.TREASURE,
		ENEMY_TYPE.BOSS,
		ENEMY_TYPE.MINIBOSS,
		ENEMY_TYPE.SPAWNER,
		ENEMY_TYPE.CREEP
		];

	var skippingSpawner = false;
	var skippedSpawnerLane = 0;
	var skippedSpawnerTarget = 0;

	for (var k = 0; !targetFound && k < enemyTypePriority.length; k++) {
		var enemies = [];

		// gather all the enemies of the specified type.
		for (var i = 0; i < 3; i++) {
			for (var j = 0; j < 4; j++) {
				var enemy = g_Minigame.CurrentScene().GetEnemy(i, j);
				if (enemy && enemy.m_data.type == enemyTypePriority[k]) {
					enemies[enemies.length] = enemy;
				}
			}
		}

		// target the enemy of the specified type with the lowest hp
		for (var i = 0; i < enemies.length; i++) {
			if (enemies[i] && !enemies[i].m_bIsDestroyed) {
				if(lowHP < 1 || enemies[i].m_flDisplayedHP < lowHP) {
					targetFound = true;
					lowHP = enemies[i].m_flDisplayedHP;
					lowLane = enemies[i].m_nLane;
					lowTarget = enemies[i].m_nID;
				}
				var percentageHP = enemies[i].m_flDisplayedHP / enemies[i].m_data.max_hp;
				if(lowPercentageHP == 0 || percentageHP < lowPercentageHP) {
					lowPercentageHP = percentageHP;
				}
			}
		}

		// If we just finished looking at spawners,
		// AND none of them were below our threshold,
		// remember them and look for low creeps (so don't quit now)
		if (enemyTypePriority[k] == ENEMY_TYPE.SPAWNER && lowPercentageHP > spawnerOKThreshold) {
			skippedSpawnerLane = lowLane;
			skippedSpawnerTarget = lowTarget;
			skippingSpawner = true;
			targetFound = false;
		}

		// If we skipped a spawner and just finished looking at creeps,
		// AND the lowest was above our snag threshold,
		// just go back to the spawner!
		if (skippingSpawner && enemyTypePriority[k] == ENEMY_TYPE.CREEP && lowPercentageHP > creepSnagThreshold ) {
			lowLane = skippedSpawnerLane;
			lowTarget = skippedSpawnerTarget;
		}
	}

	// go to the chosen lane
	if (targetFound) {
		if (g_Minigame.CurrentScene().m_nExpectedLane != lowLane) {
			//console.log('switching langes');
			g_Minigame.CurrentScene().TryChangeLane(lowLane);
		}

		// target the chosen enemy
		if (g_Minigame.CurrentScene().m_nTarget != lowTarget) {
			//console.log('switching targets');
			g_Minigame.CurrentScene().TryChangeTarget(lowTarget);
		}
	}
}

function useMedicsIfRelevant() {
	var myMaxHealth = g_Minigame.CurrentScene().m_rgPlayerTechTree.max_hp;

	// check if health is below 50%
	var hpPercent = g_Minigame.CurrentScene().m_rgPlayerData.hp / myMaxHealth;
	if (hpPercent > 0.5 || g_Minigame.CurrentScene().m_rgPlayerData.hp < 1) {
		return; // no need to heal - HP is above 50% or already dead
	}

	// check if Medics is purchased and cooled down
	if (hasPurchasedAbility(7)) {

		if (isAbilityCoolingDown(7)) {
			return;
		}

		// Medics is purchased, cooled down, and needed. Trigger it.
		console.log('Medics is purchased, cooled down, and needed. Trigger it.');
		triggerAbility(7);
	}
}

// Use Good Luck Charm if doable
function useGoodLuckCharmIfRelevant() {
	// check if Good Luck Charms is purchased and cooled down
	if (hasPurchasedAbility(6)) {
		if (isAbilityCoolingDown(6)) {
			return;
		}

		// Good Luck Charms is purchased, cooled down, and needed. Trigger it.
		console.log('Good Luck Charms is purchased, cooled down, and needed. Trigger it.');
		triggerAbility(6);
	}
}

//If player is dead, call respawn method
function attemptRespawn() {
	if ((g_Minigame.CurrentScene().m_bIsDead) &&
			((g_Minigame.CurrentScene().m_rgPlayerData.time_died * 1000) + 5000) < (new Date().getTime())) {
		RespawnPlayer();
	}
}

function isAbilityCoolingDown(abilityId) {
	return g_Minigame.CurrentScene().GetCooldownForAbility(abilityId) > 0;
}

function hasPurchasedAbility(abilityId) {
	// each bit in unlocked_abilities_bitfield corresponds to an ability.
	// the above condition checks if the ability's bit is set or cleared. I.e. it checks if
	// the player has purchased the specified ability.
	return (1 << abilityId) & g_Minigame.CurrentScene().m_rgPlayerTechTree.unlocked_abilities_bitfield;
}

function triggerAbility(abilityId) {
	var elem = document.getElementById('ability_' + abilityId);
	if (elem && elem.childElements() && elem.childElements().length >= 1) {
		g_Minigame.CurrentScene().TryAbility(document.getElementById('ability_' + abilityId).childElements()[0]);
	}
}

var thingTimer = window.setInterval(doTheThing, 1000);
<|endoftext|>


<commit_before>var ObjectID = require('mongodb').ObjectID,
    jsonSchema = require('json-schema');

module.exports = function (options) {

    //  Establish required options
    var app = options.app,
        collection = options.collection,
        name = options.name,
        path = options.path,
        schema = options.schema;

    //  Selection of which routes to create
    var getCreate = (options.getCreate) ? options.getCreate : true,
        postCreate = (options.postCreate) ? options.postCreate : true,
        putCreate = (options.putCreate) ? options.putCreate : true,
        deleteCreate = (options.deleteCreate) ? options.deleteCreate : true;

    //  Optional transform options
    var postTransform = (options.postTransform) ? options.postTransform : options.defaultTransform,
        putTransform = (options.putTransform) ? options.putTransform : options.defaultTransform;

    //  Optional authentication options
    var getAuthentication = (options.getAuthentication) ? options.getAuthentication : options.defaultAuthentication,
        postAuthentication = (options.postAuthentication) ? options.postAuthentication : options.defaultAuthentication,
        putAuthentication = (options.putAuthentication) ? options.putAuthentication : options.defaultAuthentication,
        deleteAuthentication = (options.deleteAuthentication) ? options.deleteAuthentication : options.defaultAuthentication;

    //  Optional ownership options
    var ownerIdFromReq = (options.ownerIdFromReq) ? options.ownerIdFromReq : null,
        ownerField = (options.ownerField) ? options.ownerField : null;

    //  Query utility
    var createQuery = (ownerIdFromReq && ownerField) ? function (req, preOwner) {
        if (!preOwner) preOwner = {};
        preOwner[ownerField] = ownerIdFromReq(req);
        return preOwner;
    } : function (req, preOwner) {
        return (preOwner) ? preOwner : {};
    };

    //  Build path structure
    var rootObjectPath = path + '/' + name;

    //
    //  Build routes
    //

    //  GET

    var getRouteFn = function (req, res) {
        var cursor = collection.find(createQuery(req)),
            sort = req.param('sort'),
            limit = req.param('limit'),
            skip = req.param('skip');

        if (sort) {
            if (sort.match(/^[a-zA-Z]*$/)) cursor.sort(sort);
            else cursor.sort(JSON.parse(sort));
        }
        if (limit) cursor.limit(Number(limit));
        if (limit && skip) cursor.skip(Number(skip));
        cursor.toArray(function (err, documents) {
            if (err) return res.json(500, err);
            else {
                if (limit && skip) cursor.count(function (err, count) {
                    if (err) return res.json(500, err);
                    res.json({data: documents, total: count});
                });
                else res.json({data: documents, total: documents.length});
            }
        });
    };
    if (getCreate) {
        if (getAuthentication) app.get(rootObjectPath, getAuthentication, getRouteFn);
        else app.get(rootObjectPath, getRouteFn);
    }

    var getIdRouteFn = function (req, res) {
        collection.findOne(createQuery(req, {_id: ObjectID(req.params.id)}), function (err, document) {
            if (err) return res.json(500, err);
            if (!document) return res.send(404);
            res.json(document);
        });
    };
    if (getCreate) {
        if (getAuthentication) app.get(rootObjectPath + '/:id', getAuthentication, getIdRouteFn);
        else app.get(rootObjectPath + '/:id', getIdRouteFn);
    }

    //  POST

    var postRouteFn = function (req, res) {
        var report = jsonSchema.validate(req.body, schema);
        if (!report.valid) return res.json(400, report.errors);
        if (postTransform) postTransform(req.body);
        if (ownerIdFromReq && ownerField) req.body[ownerField] = ownerIdFromReq(req);
        collection.insert(req.body, function (err, document) {
            if (err) return res.json(500, err);
            res.json(document[0]);
        });
    };
    if (postCreate) {
        if (postAuthentication) app.post(rootObjectPath, postAuthentication, postRouteFn);
        else app.post(rootObjectPath, postRouteFn);
    }

    //  PUT

    var putIdRouteFn = function (req, res) {
        var report = jsonSchema.validate(req.body, schema);
        if (!report.valid) return res.json(400, report.errors);
        if (putTransform) putTransform(req.body);
        collection.update(createQuery(req, {_id: ObjectID(req.params.id)}), {$set: req.body}, function (err, modCount) {
            if (err) return res.json(500, err);
            if (modCount === 0) return res.send(404);
            res.send(200);
        });
    };
    if (putCreate) {
        if (putAuthentication) app.put(rootObjectPath + '/:id', putAuthentication, putIdRouteFn);
        else app.put(rootObjectPath + '/:id', putIdRouteFn);
    }

    //  DELETE

    var deleteIdRouteFn = function (req, res) {
        collection.remove(createQuery(req, {_id: ObjectID(req.params.id)}), function (err, modCount) {
            if (err) return res.json(500, err);
            if (modCount === 0) return res.send(404);
            res.send(200);
        });
    };
    if (deleteCreate) {
        if (deleteAuthentication) app.delete(rootObjectPath + '/:id', deleteAuthentication, deleteIdRouteFn);
        else app.delete(rootObjectPath + '/:id', deleteIdRouteFn);
    }
};<commit_msg>-Converted autocrud function into a named object constructor.<commit_after>var ObjectID = require('mongodb').ObjectID,
    jsonSchema = require('json-schema');

module.exports = function Autocrud(options) {
    if (!(this instanceof Autocrud)) return new Autocrud(options);

    //  Establish required options
    var app = options.app,
        collection = options.collection,
        name = options.name,
        path = options.path,
        schema = options.schema;

    //  Selection of which routes to create
    var getCreate = (options.getCreate) ? options.getCreate : true,
        postCreate = (options.postCreate) ? options.postCreate : true,
        putCreate = (options.putCreate) ? options.putCreate : true,
        deleteCreate = (options.deleteCreate) ? options.deleteCreate : true;

    //  Optional transform options
    var postTransform = (options.postTransform) ? options.postTransform : options.defaultTransform,
        putTransform = (options.putTransform) ? options.putTransform : options.defaultTransform;

    //  Optional authentication options
    var getAuthentication = (options.getAuthentication) ? options.getAuthentication : options.defaultAuthentication,
        postAuthentication = (options.postAuthentication) ? options.postAuthentication : options.defaultAuthentication,
        putAuthentication = (options.putAuthentication) ? options.putAuthentication : options.defaultAuthentication,
        deleteAuthentication = (options.deleteAuthentication) ? options.deleteAuthentication : options.defaultAuthentication;

    //  Optional ownership options
    var ownerIdFromReq = (options.ownerIdFromReq) ? options.ownerIdFromReq : null,
        ownerField = (options.ownerField) ? options.ownerField : null;

    //  Query utility
    var createQuery = (ownerIdFromReq && ownerField) ? function (req, preOwner) {
        if (!preOwner) preOwner = {};
        preOwner[ownerField] = ownerIdFromReq(req);
        return preOwner;
    } : function (req, preOwner) {
        return (preOwner) ? preOwner : {};
    };

    //  Build path structure
    var rootObjectPath = path + '/' + name;

    //
    //  Build routes
    //

    //  GET

    this.getRouteFn = function (req, res) {
        var cursor = collection.find(createQuery(req)),
            sort = req.param('sort'),
            limit = req.param('limit'),
            skip = req.param('skip');

        if (sort) {
            if (sort.match(/^[a-zA-Z]*$/)) cursor.sort(sort);
            else cursor.sort(JSON.parse(sort));
        }
        if (limit) cursor.limit(Number(limit));
        if (limit && skip) cursor.skip(Number(skip));
        cursor.toArray(function (err, documents) {
            if (err) return res.json(500, err);
            else {
                if (limit && skip) cursor.count(function (err, count) {
                    if (err) return res.json(500, err);
                    res.json({data: documents, total: count});
                });
                else res.json({data: documents, total: documents.length});
            }
        });
    };
    if (getCreate) {
        if (getAuthentication) app.get(rootObjectPath, getAuthentication, this.getRouteFn);
        else app.get(rootObjectPath, this.getRouteFn);
    }

    this.getIdRouteFn = function (req, res) {
        collection.findOne(createQuery(req, {_id: ObjectID(req.params.id)}), function (err, document) {
            if (err) return res.json(500, err);
            if (!document) return res.send(404);
            res.json(document);
        });
    };
    if (getCreate) {
        if (getAuthentication) app.get(rootObjectPath + '/:id', getAuthentication, this.getIdRouteFn);
        else app.get(rootObjectPath + '/:id', this.getIdRouteFn);
    }

    //  POST

    this.postRouteFn = function (req, res) {
        var report = jsonSchema.validate(req.body, schema);
        if (!report.valid) return res.json(400, report.errors);
        if (postTransform) postTransform(req.body);
        if (ownerIdFromReq && ownerField) req.body[ownerField] = ownerIdFromReq(req);
        collection.insert(req.body, function (err, document) {
            if (err) return res.json(500, err);
            res.json(document[0]);
        });
    };
    if (postCreate) {
        if (postAuthentication) app.post(rootObjectPath, postAuthentication, this.postRouteFn);
        else app.post(rootObjectPath, this.postRouteFn);
    }

    //  PUT

    this.putIdRouteFn = function (req, res) {
        var report = jsonSchema.validate(req.body, schema);
        if (!report.valid) return res.json(400, report.errors);
        if (putTransform) putTransform(req.body);
        collection.update(createQuery(req, {_id: ObjectID(req.params.id)}), {$set: req.body}, function (err, modCount) {
            if (err) return res.json(500, err);
            if (modCount === 0) return res.send(404);
            res.send(200);
        });
    };
    if (putCreate) {
        if (putAuthentication) app.put(rootObjectPath + '/:id', putAuthentication, this.putIdRouteFn);
        else app.put(rootObjectPath + '/:id', this.putIdRouteFn);
    }

    //  DELETE

    this.deleteIdRouteFn = function (req, res) {
        collection.remove(createQuery(req, {_id: ObjectID(req.params.id)}), function (err, modCount) {
            if (err) return res.json(500, err);
            if (modCount === 0) return res.send(404);
            res.send(200);
        });
    };
    if (deleteCreate) {
        if (deleteAuthentication) app.delete(rootObjectPath + '/:id', deleteAuthentication, this.deleteIdRouteFn);
        else app.delete(rootObjectPath + '/:id', this.deleteIdRouteFn);
    }
};<|endoftext|>


<commit_before>var ProjectListView = Backbone.View.extend({
    id: 'ProjectListView',
    initialize: function() {
        _.bindAll(this, 'render', 'add', 'about');
        this.collection.bind('add', this.render);
        this.collection.bind('remove', this.render);
        this.collection.fetch({
            success: this.render,
            error: this.render
        });
    },
    render: function() {
        // Render the projects wrapper if not present.
        if ($(this.el).has('ul.projects').length === 0) {
            $(this.el).html(ich.ProjectListView());
            window.app.el.html(this.el);
        }

        // Add a row view for each project. Note that we use a pointer as the
        // projects are added to ensure that when new projects are added on a
        // re-render they are placed at the correct index in the list.
        var that = this;
        var pointer = null;
        this.collection.each(function(project) {
            if (!project.view) {
                project.view = new ProjectRowView({
                    model: project,
                    collection: this.collection
                });
                if (!pointer) {
                    $('ul.projects', self.el).prepend(project.view.el);
                }
                else {
                    $(pointer).after(project.view.el);
                }
            }
            pointer = project.view.el;
        });
        return this;
    },
    events: {
        'click input.submit': 'add',
        'click div#header a.info': 'about'
    },
    add: function() {
        var id = $('input.text', this.el).val();
        if (this.collection.get(id)) {
            window.app.message('Error', 'Project names must be unique.');
            $('input.text', this.el).val('');
            return false;
        }
        window.app.loading();
        var that = this;
        var project = new Project;
        var success = project.set(
            { id: id },
            { error: this.showError }
        );
        if (success) {
            project.setDefaults();
            project.save(project, {
                success: function() {
                    $('input.text', this.el).val('');
                    that.collection.add(project);
                    window.app.done();
                },
                error: this.showError
            });
        }
        return false;
    },
    about: function() {
        window.app.message('About TileLive', '@TODO: Put something facinating here.');
        return false;
    },
    showError: function(model, error) {
        window.app.done();
        window.app.message('Error', error);
    }
});

var ProjectRowView = Backbone.View.extend({
    tagName: 'li',
    className: 'clearfix',
    initialize: function() {
        _.bindAll(this, 'render', 'del');
        this.render();
    },
    // See http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#lon.2Flat_to_tile_numbers_2
    thumb: function() {
        var center = this.model.get('center') || {lat: 0, lon: 0, zoom: 2};
        var z = center.zoom;
        var lat_rad = center.lat * Math.PI / 180;
        var x = parseInt((center.lon + 180.0) / 360.0 * Math.pow(2,z));
        var y = parseInt((1.0 - Math.log(Math.tan(lat_rad) + (1 / Math.cos(lat_rad))) / Math.PI) / 2.0 * Math.pow(2,z));
        var url = this.model.layerURL({ signed: true });
        return url.replace('${z}', z).replace('${x}', x).replace('${y}', y);
    },
    render: function() {
        $(this.el).html(ich.ProjectRowView({
            id: this.model.get('id'),
            thumb: this.thumb()
        }));
        return this;
    },
    events: {
        'click .delete': 'del'
    },
    del: function() {
        var that = this;
        window.app.loading();
        if (confirm('Are you sure you want to delete this project?')) {
            this.model.destroy({
                success: function() {
                    that.remove();
                    window.app.done();
                },
                error: function() {
                    window.app.done();
                    window.app.message('Error', 'The project could not be deleted.');
                }
            });
        }
        else {
            window.app.done();
        }
        return false;
    }
});

var ProjectView = Backbone.View.extend({
    id: 'ProjectView',
    events: {
        'click #header a.save': 'saveProject',
        'click #header a.info': 'projectInfo',
        'click #header a.minimal': 'minimal',
        'click #header a.home': 'home'
    },
    initialize: function() {
        _.bindAll(this, 'render', 'saveProject', 'projectInfo',
            'home', 'minimal', 'changed');
        this.model.view = this;
        this.model.bind('change', this.changed);
        this.model.fetch({
            success: this.render,
            error: function(err, data) {
                var obj = $.parseJSON(data.responseText);
                window.app.message('Error', obj.message, 'error', function() {
                    window.location = '/';
                });
            }
        });
    },
    render: function() {
        $(this.el).html(ich.ProjectView(this.model));

        var layers = new LayerListView({
            collection: this.model.get('Layer'),
            project: this.model
        }),
            stylesheets = new StylesheetListView({
            collection: this.model.get('Stylesheet'),
            project: this.model
        }),
            colors = new ColorSwatchesToolView({
            collection: new ColorSwatchesList(null, {
                project: this.model
            }),
            project: this.model
        }),
            map = new MapView({
            model: this.model
        }),
            colorPicker = new ColorPickerToolView({
            model: this.model,
            project: this.model
        }),
            fontPicker = new FontPickerToolView({
            model: new Abilities,
            project: this.model
        });

        $('#sidebar', this.el).append(layers.el);
        $('#sidebar', this.el).append(colors.el);
        $('#sidebar', this.el).append(map.el);
        $('.sidebar-header', colors.el).append(colorPicker.el);
        $('#toolbar', this.el).append(fontPicker.el);
        $('#main', this.el).append(stylesheets.el);

        window.app.el.html(this.el);
        window.app.trigger('ready');
        return this;
    },
    saveProject: function() {
        var that = this;
        this.model.save(this.model, {
            success: function() {
                that.model.trigger('save');
                $('#header a.save', self.el).removeClass('changed');
                $('.CodeMirror-line-numbers div')
                    .removeClass('syntax-error')
                    .attr('title', '')
                    .unbind('mouseenter mouseleave'); // Removes tipsy.
            },
            error: function(err, data) {
                if (data.status == 500) {
                    var err_obj = $.parseJSON(data.responseText);
                    if (err_obj.line) {
                        var editor = _.detect(
                            that.model.view.stylesheets.collection.models,
                            function(s) {
                                return s.id == err_obj.filename;
                        });
                        $('div.CodeMirror-line-numbers div:nth-child('
                            + err_obj.line
                            + ')',
                            editor.view.codemirror.lineNumbers)
                            .addClass('syntax-error')
                            .attr('title', err_obj.message)
                            .tipsy({gravity: 'w'});
                    } else {
                        window.app.message('Error', err_obj.message);
                    }
                }
            }
        });
        return false;
    },
    projectInfo: function() {
        window.app.message('Project Info', {
            'tilelive_url': this.model.layerURL({signed: true}),
            'mml_url': [
                window.location.protocol,
                window.location.host
            ].join('//') + this.model.url()
        }, 'projectInfo');
        return false;
    },
    home: function() {
        return (!$('#header a.save', this.el).is('.changed') || confirm('You have unsaved changes. Are you sure you want to close this project?'));
    },
    minimal: function() {
        $('a.minimal', this.el).toggleClass('active');
        if ($('a.minimal', this.el).is('.active')) {
            $(this.el).addClass('minimal');
            this.watcher = new Watcher(this.model);
        }
        else {
            $(this.el).removeClass('minimal');
            this.watcher.destroy();
        }
        return false;
    },
    changed: function() {
        $('#header a.save', this.el).addClass('changed');
    }
});

/**
 * Watcher.
 * Class for updating project in minimal mode.
 */
var Watcher = function(model) {
    _.bindAll(this, 'fetch', 'destroy');
    var model = model;
    this.model = model;
    this.model.bind('change', this.fetch);
    this.md5 = new MD5();
    this.current = this.md5.digest(JSON.stringify(this.model));
    this.watcher = setInterval(function() { model.fetch(); }, 1000);
};

Watcher.prototype.fetch = function() {
    var state = this.md5.digest(JSON.stringify(this.model));
    if (this.current !== state) {
        this.current = state;
        this.model.trigger('save');
    }
};

Watcher.prototype.destroy = function() {
    window.clearInterval(this.watcher);
};

<commit_msg>Handle string error messages.<commit_after>var ProjectListView = Backbone.View.extend({
    id: 'ProjectListView',
    initialize: function() {
        _.bindAll(this, 'render', 'add', 'about');
        this.collection.bind('add', this.render);
        this.collection.bind('remove', this.render);
        this.collection.fetch({
            success: this.render,
            error: this.render
        });
    },
    render: function() {
        // Render the projects wrapper if not present.
        if ($(this.el).has('ul.projects').length === 0) {
            $(this.el).html(ich.ProjectListView());
            window.app.el.html(this.el);
        }

        // Add a row view for each project. Note that we use a pointer as the
        // projects are added to ensure that when new projects are added on a
        // re-render they are placed at the correct index in the list.
        var that = this;
        var pointer = null;
        this.collection.each(function(project) {
            if (!project.view) {
                project.view = new ProjectRowView({
                    model: project,
                    collection: this.collection
                });
                if (!pointer) {
                    $('ul.projects', self.el).prepend(project.view.el);
                }
                else {
                    $(pointer).after(project.view.el);
                }
            }
            pointer = project.view.el;
        });
        return this;
    },
    events: {
        'click input.submit': 'add',
        'click div#header a.info': 'about'
    },
    add: function() {
        var id = $('input.text', this.el).val();
        if (this.collection.get(id)) {
            window.app.message('Error', 'Project names must be unique.');
            $('input.text', this.el).val('');
            return false;
        }
        window.app.loading();
        var that = this;
        var project = new Project;
        var success = project.set(
            { id: id },
            { error: this.showError }
        );
        if (success) {
            project.setDefaults();
            project.save(project, {
                success: function() {
                    $('input.text', this.el).val('');
                    that.collection.add(project);
                    window.app.done();
                },
                error: this.showError
            });
        }
        return false;
    },
    about: function() {
        window.app.message('About TileLive', '@TODO: Put something facinating here.');
        return false;
    },
    showError: function(model, error) {
        window.app.done();
        window.app.message('Error', error);
    }
});

var ProjectRowView = Backbone.View.extend({
    tagName: 'li',
    className: 'clearfix',
    initialize: function() {
        _.bindAll(this, 'render', 'del');
        this.render();
    },
    // See http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#lon.2Flat_to_tile_numbers_2
    thumb: function() {
        var center = this.model.get('center') || {lat: 0, lon: 0, zoom: 2};
        var z = center.zoom;
        var lat_rad = center.lat * Math.PI / 180;
        var x = parseInt((center.lon + 180.0) / 360.0 * Math.pow(2,z));
        var y = parseInt((1.0 - Math.log(Math.tan(lat_rad) + (1 / Math.cos(lat_rad))) / Math.PI) / 2.0 * Math.pow(2,z));
        var url = this.model.layerURL({ signed: true });
        return url.replace('${z}', z).replace('${x}', x).replace('${y}', y);
    },
    render: function() {
        $(this.el).html(ich.ProjectRowView({
            id: this.model.get('id'),
            thumb: this.thumb()
        }));
        return this;
    },
    events: {
        'click .delete': 'del'
    },
    del: function() {
        var that = this;
        window.app.loading();
        if (confirm('Are you sure you want to delete this project?')) {
            this.model.destroy({
                success: function() {
                    that.remove();
                    window.app.done();
                },
                error: function() {
                    window.app.done();
                    window.app.message('Error', 'The project could not be deleted.');
                }
            });
        }
        else {
            window.app.done();
        }
        return false;
    }
});

var ProjectView = Backbone.View.extend({
    id: 'ProjectView',
    events: {
        'click #header a.save': 'saveProject',
        'click #header a.info': 'projectInfo',
        'click #header a.minimal': 'minimal',
        'click #header a.home': 'home'
    },
    initialize: function() {
        _.bindAll(this, 'render', 'saveProject', 'projectInfo',
            'home', 'minimal', 'changed');
        this.model.view = this;
        this.model.bind('change', this.changed);
        this.model.fetch({
            success: this.render,
            error: function(err, data) {
                var obj = $.parseJSON(data.responseText);
                window.app.message('Error', obj.message, 'error', function() {
                    window.location = '/';
                });
            }
        });
    },
    render: function() {
        $(this.el).html(ich.ProjectView(this.model));

        var layers = new LayerListView({
            collection: this.model.get('Layer'),
            project: this.model
        }),
            stylesheets = new StylesheetListView({
            collection: this.model.get('Stylesheet'),
            project: this.model
        }),
            colors = new ColorSwatchesToolView({
            collection: new ColorSwatchesList(null, {
                project: this.model
            }),
            project: this.model
        }),
            map = new MapView({
            model: this.model
        }),
            colorPicker = new ColorPickerToolView({
            model: this.model,
            project: this.model
        }),
            fontPicker = new FontPickerToolView({
            model: new Abilities,
            project: this.model
        });

        $('#sidebar', this.el).append(layers.el);
        $('#sidebar', this.el).append(colors.el);
        $('#sidebar', this.el).append(map.el);
        $('.sidebar-header', colors.el).append(colorPicker.el);
        $('#toolbar', this.el).append(fontPicker.el);
        $('#main', this.el).append(stylesheets.el);

        window.app.el.html(this.el);
        window.app.trigger('ready');
        return this;
    },
    saveProject: function() {
        var that = this;
        this.model.save(this.model, {
            success: function() {
                that.model.trigger('save');
                $('#header a.save', self.el).removeClass('changed');
                $('.CodeMirror-line-numbers div')
                    .removeClass('syntax-error')
                    .attr('title', '')
                    .unbind('mouseenter mouseleave'); // Removes tipsy.
            },
            error: function(err, data) {
                if (typeof data === 'string') {
                    window.app.message('Error', data);
                } else if (data.status == 500) {
                    var err_obj = $.parseJSON(data.responseText);
                    if (err_obj.line) {
                        var editor = _.detect(
                            that.model.view.stylesheets.collection.models,
                            function(s) {
                                return s.id == err_obj.filename;
                        });
                        $('div.CodeMirror-line-numbers div:nth-child('
                            + err_obj.line
                            + ')',
                            editor.view.codemirror.lineNumbers)
                            .addClass('syntax-error')
                            .attr('title', err_obj.message)
                            .tipsy({gravity: 'w'});
                    } else {
                        window.app.message('Error', err_obj.message);
                    }
                }
            }
        });
        return false;
    },
    projectInfo: function() {
        window.app.message('Project Info', {
            'tilelive_url': this.model.layerURL({signed: true}),
            'mml_url': [
                window.location.protocol,
                window.location.host
            ].join('//') + this.model.url()
        }, 'projectInfo');
        return false;
    },
    home: function() {
        return (!$('#header a.save', this.el).is('.changed') || confirm('You have unsaved changes. Are you sure you want to close this project?'));
    },
    minimal: function() {
        $('a.minimal', this.el).toggleClass('active');
        if ($('a.minimal', this.el).is('.active')) {
            $(this.el).addClass('minimal');
            this.watcher = new Watcher(this.model);
        }
        else {
            $(this.el).removeClass('minimal');
            this.watcher.destroy();
        }
        return false;
    },
    changed: function() {
        $('#header a.save', this.el).addClass('changed');
    }
});

/**
 * Watcher.
 * Class for updating project in minimal mode.
 */
var Watcher = function(model) {
    _.bindAll(this, 'fetch', 'destroy');
    var model = model;
    this.model = model;
    this.model.bind('change', this.fetch);
    this.md5 = new MD5();
    this.current = this.md5.digest(JSON.stringify(this.model));
    this.watcher = setInterval(function() { model.fetch(); }, 1000);
};

Watcher.prototype.fetch = function() {
    var state = this.md5.digest(JSON.stringify(this.model));
    if (this.current !== state) {
        this.current = state;
        this.model.trigger('save');
    }
};

Watcher.prototype.destroy = function() {
    window.clearInterval(this.watcher);
};

<|endoftext|>


<commit_before>(function (window, raf, caf) {

var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var i = 0; i < vendors.length && !window[raf]; ++x) {
  window[raf] = window[vendors[x] + 'RequestAnimationFrame'];
  window[caf] = (window[vendors[x] + 'CancelAnimationFrame'] ||
                 window[vendors[x] + 'CancelRequestAnimationFrame']);
}

if (!window[raf]) {
  var queue = [];

  window.setInterval(function () {
    for (var i = 0; i < queue.length; i++) {
      if (queue[i] !== false) {
        queue[i].call(null);
      }
    }

    queue = [];
  }, 1000 / 60);

  window[raf] = function (cb) {
    return queue.push(cb) - 1;
  };
}

if (!window[caf]) {
  window[caf] = function (id) {
    queue[id] = false;
  };
}

function loop(cb) {
  window[raf](function () {
    cb();
    loop(cb);
  });
}

function getJSON(url, cb) {
  var xhr = new XMLHttpRequest();
  xhr.open('get', url);
  xhr.onreadystatechange = handler;
  xhr.responseType = 'json';
  xhr.setRequestHeader('Accept', 'application/json');
  xhr.send();

  function handler() {
    if (this.readyState === this.DONE) {
      if (this.status === 200) {
        cb(null, this.response);
      } else {
        var errorMsg = '[getJSON] `' + url + '` failed\nStatus:   ' + this.status;
        if (this.response.message) {
          errorMsg += '\nResponse: ' + this.response.message;
        }
        cb(new Error(errorMsg));
      }
    }
  };
};

function getGamepads() {
  var apis = ['getGamepads', 'webkitGetGamepads', 'webkitGamepads'];
  for (var i = 0; i < apis.length; i++) {
    if (apis[i] in navigator) {
      return navigator[apis[i]]();
    }
  }
  return [];
}

var AXIS_THRESHOLD = 0.25;

var gamepadIdx = 0;
var gamepadContinuePolling = false;
var gamepadConnected = false;
var gamepadActive = false;

// Chrome doesn't have the gamepad events, and we can't
// feature detect them in Firefox unfortunately.
if ('chrome' in window) {
  gamepadContinuePolling = true;
}

window.addEventListener('gamepadconnected', function () {
  gamepadContinuePolling = true;
});

window.addEventListener('gamepaddisconnected', function () {
  gamepadContinuePolling = false;
});

var mapping = {
};

function GamepadDevice(gamepad) {
  ['axes', 'buttons', 'connected', 'id', 'index', 'mapping'].forEach(function (prop) {
    this[prop] = gamepad[prop];
  }.bind(this));
  this.pretty_id = this.id.replace(/\s\s+/g, ' ').toLowerCase();
  this._ids = this._getIds(this.id);
  this.vendor_id = this._ids[0];
  this.product_id = this._ids[0];
}

GamepadDevice.prototype._getIds = function (id) {
  var bits = id.split('-');
  if (bits.length < 2) {
    var match = id.match(/vendor: (\w+) product: (\w+)/i);
    if (!match) {
      return null;
    }

    return match.slice(1);
  }

  return bits.slice(0, 2);
};

var state = {
  buttons: {},
  axes: {}
};

for (var i = 0; i < 17; i++) {
  state.buttons[i] = {
    pressed: false,
    value: 0.0
  };
}

for (var i = 0; i < 4; i++) {
  state.axes[i] = [0.0];
}

var gamepad;
var waitingForAxisEndTimeout;

function poll() {
  loop(function () {
    if (!gamepadContinuePolling) {
      return;
    }

    if (!gamepad) {
      gamepad = getGamepads()[gamepadIdx];
    }

    var gd = gamepad;

    // if (!gamepad) {
    //   return;
    // }

    // var gd = new GamepadDevice(gamepad);

    gd.buttons.forEach(function (button, idx) {
      var buttonState = {
        index: idx,
        pressed: button.pressed,
        value: button.value
      };

      if (state.buttons[idx].pressed !== buttonState.pressed ||
          state.buttons[idx].value !== buttonState.value) {

        console.log('button #%s changed: %o', idx, buttonState);
        if (buttonState.pressed) {
          emit('buttondown', buttonState);
        } else {
          emit('buttonup', buttonState);
          emit('buttonpress', buttonState);
        }

      }

      state.buttons[idx] = buttonState;
    });

    gd.axes.forEach(function (axis, idx) {
      var axisMoveState = {
        index: idx,
        value: axis
      };

      if (Math.abs(axis) > AXIS_THRESHOLD && state.axes[idx].value !== axis) {
        console.log('axis #%s changed: %s', idx, axis);

        emit('axismove', axisMoveState);

        var pending = state.axes['pending.' + idx];

        if (!pending) {
          pending = [];
        }

        pending.push(axis);

        // Reset the queue every 100 seconds.
        clearTimeout(waitingForAxisEndTimeout);

        waitingForAxisEndTimeout = setTimeout(function () {
          var avg = utils.getAverage(pending);
          if (Math.abs(avg) <= AXIS_THRESHOLD) {
          } else {
            if (avg < 0) {
              roundedAvg = Math.floor(avg);
            } else {
              roundedAvg = Math.ceil(avg);
            }
          }

          console.log('axis #%s average: %s', idx, roundedAvg);

          var axisChangeState = {
            index: idx,
            value: roundedAvg
          };
          emit('axischange', axisChangeState);

          pending = [];
        }, 100);
      }

      state.axes[idx] = axisMoveState;
    });
  });
}

poll();

var listeners = {};

function emit(name, data) {
  if (name in listeners) {
    listeners[name].forEach(function (func) {
      func(data);
    });
  }
}

function on(name, func) {
  if (name in listeners) {
    listeners[name].push(func);
  } else {
    listeners[name] = [func];
  }
}

function off(name) {
  listeners[name] = [];
}

var utils = {};

utils.defaultsTo = function (value, defaultValue) {
  return typeof value === 'undefined' ? defaultValue : value;
};

utils.getAverage = function (list) {
  var sum = list.reduce(function (a, b) {
    return a + b;
  });
  return sum / list.length;
};

utils.closest = function (el, sel) {
  if (el !== null) {
    return el.matches(sel) ? el :
      (el.querySelector(sel) || utils.closest(el.parentNode, sel));
  }
};

var KEY_PROPS = [
  'altKey',
  'charCode',
  'ctrlKey',
  'key',
  'keyCode',
  'metaKey',
  'shiftKey',
  'which',
];

var KEYCODES_TO_KEYS = {
  3: 'Cancel',
  6: 'Help',
  8: 'Backspace',
  9: 'Tab',
  12: 'Clear',
  13: 'Enter',
  16: 'Shift',
  17: 'Control',
  18: 'Alt',
  19: 'Pause',
  20: 'CapsLock',
  27: 'Escape',
  28: 'Convert',
  29: 'NonConvert',
  30: 'Accept',
  31: 'ModeChange',
  33: 'PageUp',
  34: 'PageDown',
  35: 'End',
  36: 'Home',
  37: 'ArrowLeft',
  38: 'ArrowUp',
  39: 'ArrowRight',
  40: 'ArrowDown',
  41: 'Select',
  42: 'Print',
  43: 'Execute',
  44: 'PrintScreen',
  45: 'Insert',
  46: 'Delete',
  48: ['0', ')'],
  49: ['1', '!'],
  50: ['2', '@'],
  51: ['3', '#'],
  52: ['4', '$'],
  53: ['5', '%'],
  54: ['6', '^'],
  55: ['7', '&'],
  56: ['8', '*'],
  57: ['9', '('],
  91: 'OS',
  93: 'ContextMenu',
  144: 'NumLock',
  145: 'ScrollLock',
  181: 'VolumeMute',
  182: 'VolumeDown',
  183: 'VolumeUp',
  186: [';', ':'],
  187: ['=', '+'],
  188: [',', '<'],
  189: ['-', '_'],
  190: ['.', '>'],
  191: ['/', '?'],
  192: ['`', '~'],
  219: ['[', '{'],
  220: ['\\', '|'],
  221: [']', '}'],
  222: ["'", '"'],
  224: 'Meta',
  225: 'AltGraph',
  246: 'Attn',
  247: 'CrSel',
  248: 'ExSel',
  249: 'EraseEof',
  250: 'Play',
  251: 'ZoomOut',
};

var i = 0;

// Function keys (F1-24).
for (i = 1; i < 25; i++) {
  KEYCODES_TO_KEYS[111 + i] = 'F' + i;
}

// Printable ASCII characters.
var letter = '';
for (i = 65; i < 91; i++) {
  letter = String.fromCharCode(i);
  KEYCODES_TO_KEYS[i] = [letter.toLowerCase(), letter.toUpperCase()];
}

// Polyfill `key` on `KeyboardEvent`.
var testKeyboardEvent = new KeyboardEvent('keydown');
if (!('key' in testKeyboardEvent)) {
  Object.defineProperty(KeyboardEvent.prototype, 'key', {
    get: function (x) {
      var key = KEYCODES_TO_KEYS[this.which || this.keyCode];

      if (Array.isArray(key)) {
        key = key[+this.shiftKey];
      }

      return key;
    }
  });
}

function getSyntheticKeyProps(e) {
  var obj = {};
  KEY_PROPS.forEach(function (prop) {
    obj[prop] = e[prop];
  });
  return obj;
}

var controlsRowTemplate = document.querySelector('#controlsRowTemplate');

var controlsTable = document.querySelector('.controlsTableBody');

var rowNum = 0;

function addNewRow() {
  var tr = document.createElement('tr');
  tr.setAttribute('data-row', rowNum++);
  tr.innerHTML = controlsRowTemplate.innerHTML;
  controlsTable.appendChild(tr);
}

function addNewRowIfNeeded() {
  if (getLastTextbox().value) {
    addNewRow();
  }
}

addNewRow();

var getLastTextbox = function () {
  return document.querySelector('tr:last-child td:last-child input') || {};
};

var captures = {
  keyboard: {},
  gamepad: {}
};

document.body.addEventListener('focus', function (e) {
  e.target.setAttribute('data-placeholder', e.target.placeholder);
  e.target.placeholder = '';
}, true);

document.body.addEventListener('blur', function (e) {
  if (!e.value) {
    e.target.placeholder = e.target.getAttribute('data-placeholder')
  }
}, true);

document.body.addEventListener('keypress', function (e) {
  var parentRow = utils.closest(e.target, 'tr');
  var parentRowNum = parentRow.getAttribute('data-row');
  var controlDevice = e.target.getAttribute('data-device');

  if (e.key === 'Backspace' || e.key === 'Delete') {
    captures[controlDevice][parentRowNum] = [];
    e.target.value = '';
  }

  if (controlDevice !== 'keyboard') {
    return;
  }

  if (!(parentRowNum in captures[controlDevice])) {
    captures[controlDevice][parentRowNum] = [];
  }

  var eventProps = getSyntheticKeyProps(e);

  captures[controlDevice][parentRowNum].push(eventProps);

  var capturedKeys = captures[controlDevice][parentRowNum].map(function (x) {
    return x.key;
  });

  e.target.value = capturedKeys.join(' + ');
  addNewRowIfNeeded();

  var nextGamepadTextbox = parentRow.nextSibling.querySelector('input[data-device=gamepad]');
  if (nextGamepadTextbox) {
    nextGamepadTextbox.focus();
  }
}, true);

on('buttonpress', function (e) {
  var el = document.activeElement;
  if (!el) {
    return;
  }

  var controlDevice = el.getAttribute('data-device');
  if (controlDevice !== 'gamepad') {
    return;
  }

  var parentRow = utils.closest(el, 'tr');
  var parentRowNum = parentRow.getAttribute('data-row');

  if (!(parentRowNum in captures.gamepad)) {
    captures.gamepad[parentRowNum] = [];
  }

  captures.gamepad[parentRowNum].push(e);

  var capturedKeys = captures.gamepad[parentRowNum].map(function (x) {
    return 'buttons[' + utils.defaultsTo(x.index, 'unknown') + ']';
  });

  el.value = capturedKeys.join(' + ');
  addNewRowIfNeeded();
  var nextKeyboardTextbox = parentRow.querySelector('input[data-device=keyboard]');
  if (nextKeyboardTextbox) {
    nextKeyboardTextbox.focus();
  }
});

on('axischange', function (e) {
  var el = document.activeElement;
  if (!el) {
    return;
  }

  var controlDevice = el.getAttribute('data-device');
  if (controlDevice !== 'gamepad') {
    return;
  }

  var parentRow = utils.closest(el, 'tr');
  var parentRowNum = parentRow.getAttribute('data-row');

  if (!(parentRowNum in captures.gamepad)) {
    captures.gamepad[parentRowNum] = [];
  }

  captures.gamepad[parentRowNum].push(e);

  var capturedKeys = captures.gamepad[parentRowNum].map(function (x) {
    return 'axes[' + utils.defaultsTo(x.index, 'unknown') + ']=' + x.value;
  });

  el.value = capturedKeys.join(' + ');
  addNewRowIfNeeded();
  var nextKeyboardTextbox = parentRow.querySelector('input[data-device=keyboard]');
  if (nextKeyboardTextbox) {
    nextKeyboardTextbox.focus();
  }
});

// getJSON('mappings.json', function (err, data) {
//   if (err) {
//     return alert(err);
//   }

//   console.log(data);
// });

})(this, 'requestAnimationFrame', 'cancelAnimationFrame');
<commit_msg>do not toggle input's placeholder text on focus/blur<commit_after>(function (window, raf, caf) {

var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var i = 0; i < vendors.length && !window[raf]; ++x) {
  window[raf] = window[vendors[x] + 'RequestAnimationFrame'];
  window[caf] = (window[vendors[x] + 'CancelAnimationFrame'] ||
                 window[vendors[x] + 'CancelRequestAnimationFrame']);
}

if (!window[raf]) {
  var queue = [];

  window.setInterval(function () {
    for (var i = 0; i < queue.length; i++) {
      if (queue[i] !== false) {
        queue[i].call(null);
      }
    }

    queue = [];
  }, 1000 / 60);

  window[raf] = function (cb) {
    return queue.push(cb) - 1;
  };
}

if (!window[caf]) {
  window[caf] = function (id) {
    queue[id] = false;
  };
}

function loop(cb) {
  window[raf](function () {
    cb();
    loop(cb);
  });
}

function getJSON(url, cb) {
  var xhr = new XMLHttpRequest();
  xhr.open('get', url);
  xhr.onreadystatechange = handler;
  xhr.responseType = 'json';
  xhr.setRequestHeader('Accept', 'application/json');
  xhr.send();

  function handler() {
    if (this.readyState === this.DONE) {
      if (this.status === 200) {
        cb(null, this.response);
      } else {
        var errorMsg = '[getJSON] `' + url + '` failed\nStatus:   ' + this.status;
        if (this.response.message) {
          errorMsg += '\nResponse: ' + this.response.message;
        }
        cb(new Error(errorMsg));
      }
    }
  };
};

function getGamepads() {
  var apis = ['getGamepads', 'webkitGetGamepads', 'webkitGamepads'];
  for (var i = 0; i < apis.length; i++) {
    if (apis[i] in navigator) {
      return navigator[apis[i]]();
    }
  }
  return [];
}

var AXIS_THRESHOLD = 0.25;

var gamepadIdx = 0;
var gamepadContinuePolling = false;
var gamepadConnected = false;
var gamepadActive = false;

// Chrome doesn't have the gamepad events, and we can't
// feature detect them in Firefox unfortunately.
if ('chrome' in window) {
  gamepadContinuePolling = true;
}

window.addEventListener('gamepadconnected', function () {
  gamepadContinuePolling = true;
});

window.addEventListener('gamepaddisconnected', function () {
  gamepadContinuePolling = false;
});

var mapping = {
};

function GamepadDevice(gamepad) {
  ['axes', 'buttons', 'connected', 'id', 'index', 'mapping'].forEach(function (prop) {
    this[prop] = gamepad[prop];
  }.bind(this));
  this.pretty_id = this.id.replace(/\s\s+/g, ' ').toLowerCase();
  this._ids = this._getIds(this.id);
  this.vendor_id = this._ids[0];
  this.product_id = this._ids[0];
}

GamepadDevice.prototype._getIds = function (id) {
  var bits = id.split('-');
  if (bits.length < 2) {
    var match = id.match(/vendor: (\w+) product: (\w+)/i);
    if (!match) {
      return null;
    }

    return match.slice(1);
  }

  return bits.slice(0, 2);
};

var state = {
  buttons: {},
  axes: {}
};

for (var i = 0; i < 17; i++) {
  state.buttons[i] = {
    pressed: false,
    value: 0.0
  };
}

for (var i = 0; i < 4; i++) {
  state.axes[i] = [0.0];
}

var gamepad;
var waitingForAxisEndTimeout;

function poll() {
  loop(function () {
    if (!gamepadContinuePolling) {
      return;
    }

    if (!gamepad) {
      gamepad = getGamepads()[gamepadIdx];
    }

    var gd = gamepad;

    // if (!gamepad) {
    //   return;
    // }

    // var gd = new GamepadDevice(gamepad);

    gd.buttons.forEach(function (button, idx) {
      var buttonState = {
        index: idx,
        pressed: button.pressed,
        value: button.value
      };

      if (state.buttons[idx].pressed !== buttonState.pressed ||
          state.buttons[idx].value !== buttonState.value) {

        console.log('button #%s changed: %o', idx, buttonState);
        if (buttonState.pressed) {
          emit('buttondown', buttonState);
        } else {
          emit('buttonup', buttonState);
          emit('buttonpress', buttonState);
        }

      }

      state.buttons[idx] = buttonState;
    });

    gd.axes.forEach(function (axis, idx) {
      var axisMoveState = {
        index: idx,
        value: axis
      };

      if (Math.abs(axis) > AXIS_THRESHOLD && state.axes[idx].value !== axis) {
        console.log('axis #%s changed: %s', idx, axis);

        emit('axismove', axisMoveState);

        var pending = state.axes['pending.' + idx];

        if (!pending) {
          pending = [];
        }

        pending.push(axis);

        // Reset the queue every 100 seconds.
        clearTimeout(waitingForAxisEndTimeout);

        waitingForAxisEndTimeout = setTimeout(function () {
          var avg = utils.getAverage(pending);
          if (Math.abs(avg) <= AXIS_THRESHOLD) {
          } else {
            if (avg < 0) {
              roundedAvg = Math.floor(avg);
            } else {
              roundedAvg = Math.ceil(avg);
            }
          }

          console.log('axis #%s average: %s', idx, roundedAvg);

          var axisChangeState = {
            index: idx,
            value: roundedAvg
          };
          emit('axischange', axisChangeState);

          pending = [];
        }, 100);
      }

      state.axes[idx] = axisMoveState;
    });
  });
}

poll();

var listeners = {};

function emit(name, data) {
  if (name in listeners) {
    listeners[name].forEach(function (func) {
      func(data);
    });
  }
}

function on(name, func) {
  if (name in listeners) {
    listeners[name].push(func);
  } else {
    listeners[name] = [func];
  }
}

function off(name) {
  listeners[name] = [];
}

var utils = {};

utils.defaultsTo = function (value, defaultValue) {
  return typeof value === 'undefined' ? defaultValue : value;
};

utils.getAverage = function (list) {
  var sum = list.reduce(function (a, b) {
    return a + b;
  });
  return sum / list.length;
};

utils.closest = function (el, sel) {
  if (el !== null) {
    return el.matches(sel) ? el :
      (el.querySelector(sel) || utils.closest(el.parentNode, sel));
  }
};

var KEY_PROPS = [
  'altKey',
  'charCode',
  'ctrlKey',
  'key',
  'keyCode',
  'metaKey',
  'shiftKey',
  'which',
];

var KEYCODES_TO_KEYS = {
  3: 'Cancel',
  6: 'Help',
  8: 'Backspace',
  9: 'Tab',
  12: 'Clear',
  13: 'Enter',
  16: 'Shift',
  17: 'Control',
  18: 'Alt',
  19: 'Pause',
  20: 'CapsLock',
  27: 'Escape',
  28: 'Convert',
  29: 'NonConvert',
  30: 'Accept',
  31: 'ModeChange',
  33: 'PageUp',
  34: 'PageDown',
  35: 'End',
  36: 'Home',
  37: 'ArrowLeft',
  38: 'ArrowUp',
  39: 'ArrowRight',
  40: 'ArrowDown',
  41: 'Select',
  42: 'Print',
  43: 'Execute',
  44: 'PrintScreen',
  45: 'Insert',
  46: 'Delete',
  48: ['0', ')'],
  49: ['1', '!'],
  50: ['2', '@'],
  51: ['3', '#'],
  52: ['4', '$'],
  53: ['5', '%'],
  54: ['6', '^'],
  55: ['7', '&'],
  56: ['8', '*'],
  57: ['9', '('],
  91: 'OS',
  93: 'ContextMenu',
  144: 'NumLock',
  145: 'ScrollLock',
  181: 'VolumeMute',
  182: 'VolumeDown',
  183: 'VolumeUp',
  186: [';', ':'],
  187: ['=', '+'],
  188: [',', '<'],
  189: ['-', '_'],
  190: ['.', '>'],
  191: ['/', '?'],
  192: ['`', '~'],
  219: ['[', '{'],
  220: ['\\', '|'],
  221: [']', '}'],
  222: ["'", '"'],
  224: 'Meta',
  225: 'AltGraph',
  246: 'Attn',
  247: 'CrSel',
  248: 'ExSel',
  249: 'EraseEof',
  250: 'Play',
  251: 'ZoomOut',
};

var i = 0;

// Function keys (F1-24).
for (i = 1; i < 25; i++) {
  KEYCODES_TO_KEYS[111 + i] = 'F' + i;
}

// Printable ASCII characters.
var letter = '';
for (i = 65; i < 91; i++) {
  letter = String.fromCharCode(i);
  KEYCODES_TO_KEYS[i] = [letter.toLowerCase(), letter.toUpperCase()];
}

// Polyfill `key` on `KeyboardEvent`.
var testKeyboardEvent = new KeyboardEvent('keydown');
if (!('key' in testKeyboardEvent)) {
  Object.defineProperty(KeyboardEvent.prototype, 'key', {
    get: function (x) {
      var key = KEYCODES_TO_KEYS[this.which || this.keyCode];

      if (Array.isArray(key)) {
        key = key[+this.shiftKey];
      }

      return key;
    }
  });
}

function getSyntheticKeyProps(e) {
  var obj = {};
  KEY_PROPS.forEach(function (prop) {
    obj[prop] = e[prop];
  });
  return obj;
}

var controlsRowTemplate = document.querySelector('#controlsRowTemplate');

var controlsTable = document.querySelector('.controlsTableBody');

var rowNum = 0;

function addNewRow() {
  var tr = document.createElement('tr');
  tr.setAttribute('data-row', rowNum++);
  tr.innerHTML = controlsRowTemplate.innerHTML;
  controlsTable.appendChild(tr);
}

function addNewRowIfNeeded() {
  if (getLastTextbox().value) {
    addNewRow();
  }
}

addNewRow();

var getLastTextbox = function () {
  return document.querySelector('tr:last-child td:last-child input') || {};
};

var captures = {
  keyboard: {},
  gamepad: {}
};

document.body.addEventListener('keypress', function (e) {
  var parentRow = utils.closest(e.target, 'tr');
  var parentRowNum = parentRow.getAttribute('data-row');
  var controlDevice = e.target.getAttribute('data-device');

  if (e.key === 'Backspace' || e.key === 'Delete') {
    captures[controlDevice][parentRowNum] = [];
    e.target.value = '';
  }

  if (controlDevice !== 'keyboard') {
    return;
  }

  if (!(parentRowNum in captures[controlDevice])) {
    captures[controlDevice][parentRowNum] = [];
  }

  var eventProps = getSyntheticKeyProps(e);

  captures[controlDevice][parentRowNum].push(eventProps);

  var capturedKeys = captures[controlDevice][parentRowNum].map(function (x) {
    return x.key;
  });

  e.target.value = capturedKeys.join(' + ');
  addNewRowIfNeeded();

  var nextGamepadTextbox = parentRow.nextSibling.querySelector('input[data-device=gamepad]');
  if (nextGamepadTextbox) {
    nextGamepadTextbox.focus();
  }
}, true);

on('buttonpress', function (e) {
  var el = document.activeElement;
  if (!el) {
    return;
  }

  var controlDevice = el.getAttribute('data-device');
  if (controlDevice !== 'gamepad') {
    return;
  }

  var parentRow = utils.closest(el, 'tr');
  var parentRowNum = parentRow.getAttribute('data-row');

  if (!(parentRowNum in captures.gamepad)) {
    captures.gamepad[parentRowNum] = [];
  }

  captures.gamepad[parentRowNum].push(e);

  var capturedKeys = captures.gamepad[parentRowNum].map(function (x) {
    return 'buttons[' + utils.defaultsTo(x.index, 'unknown') + ']';
  });

  el.value = capturedKeys.join(' + ');
  addNewRowIfNeeded();
  var nextKeyboardTextbox = parentRow.querySelector('input[data-device=keyboard]');
  if (nextKeyboardTextbox) {
    nextKeyboardTextbox.focus();
  }
});

on('axischange', function (e) {
  var el = document.activeElement;
  if (!el) {
    return;
  }

  var controlDevice = el.getAttribute('data-device');
  if (controlDevice !== 'gamepad') {
    return;
  }

  var parentRow = utils.closest(el, 'tr');
  var parentRowNum = parentRow.getAttribute('data-row');

  if (!(parentRowNum in captures.gamepad)) {
    captures.gamepad[parentRowNum] = [];
  }

  captures.gamepad[parentRowNum].push(e);

  var capturedKeys = captures.gamepad[parentRowNum].map(function (x) {
    return 'axes[' + utils.defaultsTo(x.index, 'unknown') + ']=' + x.value;
  });

  el.value = capturedKeys.join(' + ');
  addNewRowIfNeeded();
  var nextKeyboardTextbox = parentRow.querySelector('input[data-device=keyboard]');
  if (nextKeyboardTextbox) {
    nextKeyboardTextbox.focus();
  }
});

// getJSON('mappings.json', function (err, data) {
//   if (err) {
//     return alert(err);
//   }

//   console.log(data);
// });

})(this, 'requestAnimationFrame', 'cancelAnimationFrame');
<|endoftext|>


<commit_before>module.exports = {
  config: {
    // default font size in pixels for all tabs
    fontSize: 16,

    // font family with optional fallbacks
    fontFamily: '"Operator Mono", monospace',
    uiFontFamily: '"Operator Mono", monospace',

    // terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk)
    cursorColor: 'rgba(248,28,229,0.8)',

    // `BEAM` for |, `UNDERLINE` for _, `BLOCK` for █
    cursorShape: 'BLOCK',

    // color of the text
    foregroundColor: '#fff',

    // terminal background color
    backgroundColor: '#000',

    // border color (window, tabs)
    borderColor: '#333',

    // custom css to embed in the main window
    css: `
      * {
        letter-spacing: 0.02em;
      }
    `,

    // custom css to embed in the terminal window
    termCSS: '',

    // set to `true` if you're using a Linux set up
    // that doesn't shows native menus
    // default: `false` on Linux, `true` on Windows (ignored on macOS)
    showHamburgerMenu: '',

    // set to `false` if you want to hide the minimize, maximize and close buttons
    // additionally, set to `'left'` if you want them on the left, like in Ubuntu
    // default: `true` on windows and Linux (ignored on macOS)
    showWindowControls: '',

    // custom padding (css format, i.e.: `top right bottom left`)
    padding: '0.75rem 1rem',

    // the full list. if you're going to provide the full color palette,
    // including the 6 x 6 color cubes and the grayscale map, just provide
    // an array here instead of a color map object
    colors: {
      black: '#000000',
      red: '#ff0000',
      green: '#33ff00',
      yellow: '#ffff00',
      blue: '#0066ff',
      magenta: '#cc00ff',
      cyan: '#00ffff',
      white: '#d0d0d0',
      gray: '#808080',
      lightBlack: '#808080',
      lightRed: '#ff0000',
      lightGreen: '#33ff00',
      lightYellow: '#ffff00',
      lightBlue: '#0066ff',
      lightMagenta: '#cc00ff',
      lightCyan: '#00ffff',
      lightWhite: '#ffffff'
    },

    // the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
    // if left empty, your system's login shell will be used by default
    shell: 'zsh',

    // for setting shell arguments (i.e. for using interactive shellArgs: ['-i'])
    // by default ['--login'] will be used
    shellArgs: ['--login'],

    // for environment variables
    env: {},

    // set to false for no bell
    bell: 'SOUND',

    // if true, selected text will automatically be copied to the clipboard
    copyOnSelect: false

    // URL to custom bell
    // bellSoundURL: 'http://example.com/bell.mp3',

    // for advanced config flags please refer to https://hyper.is/#cfg
  },

  // a list of plugins to fetch and install from npm
  // format: [@org/]project[#version]
  // examples:
  //   `hyperpower`
  //   `@company/project`
  //   `project#1.0.1`
  plugins: [
    'hyperterm-atom-dark'
  ],

  // in development, you can create a directory under
  // `~/.hyper_plugins/local/` and include it here
  // to load it and avoid it being `npm install`ed
  localPlugins: []
};
<commit_msg>add hyper plugins<commit_after>module.exports = {
  config: {
    // default font size in pixels for all tabs
    fontSize: 16,

    // font family with optional fallbacks
    fontFamily: '"Operator Mono", monospace',
    uiFontFamily: '"Operator Mono", monospace',

    // terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk)
    cursorColor: "rgba(248,28,229,0.8)",

    // `BEAM` for |, `UNDERLINE` for _, `BLOCK` for █
    cursorShape: "BLOCK",

    // color of the text
    foregroundColor: "#fff",

    // terminal background color
    backgroundColor: "#112827",

    // border color (window, tabs)
    borderColor: "#333",

    // custom css to embed in the main window
    css: `
      * {
        letter-spacing: 0.02em;
      }

      .splitpane_divider {
        background-color: #000 !important;
      }

      .splitpane_pane > .term_fit > div:first-child {
        display: none !important;
      }
    `,

    // custom css to embed in the terminal window
    termCSS: "",

    // set to `true` if you're using a Linux set up
    // that doesn't shows native menus
    // default: `false` on Linux, `true` on Windows (ignored on macOS)
    showHamburgerMenu: "",

    // set to `false` if you want to hide the minimize, maximize and close buttons
    // additionally, set to `'left'` if you want them on the left, like in Ubuntu
    // default: `true` on windows and Linux (ignored on macOS)
    showWindowControls: "",

    // custom padding (css format, i.e.: `top right bottom left`)
    padding: "0.75rem 1rem",

    // the full list. if you're going to provide the full color palette,
    // including the 6 x 6 color cubes and the grayscale map, just provide
    // an array here instead of a color map object
    colors: {
      black: "#000000",
      red: "#ff0000",
      green: "#33ff00",
      yellow: "#ffff00",
      blue: "#0066ff",
      magenta: "#cc00ff",
      cyan: "#00ffff",
      white: "#d0d0d0",
      gray: "#808080",
      lightBlack: "#808080",
      lightRed: "#ff0000",
      lightGreen: "#33ff00",
      lightYellow: "#ffff00",
      lightBlue: "#0066ff",
      lightMagenta: "#cc00ff",
      lightCyan: "#00ffff",
      lightWhite: "#ffffff"
    },

    // the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
    // if left empty, your system's login shell will be used by default
    shell: "zsh",

    // for setting shell arguments (i.e. for using interactive shellArgs: ['-i'])
    // by default ['--login'] will be used
    shellArgs: ["--login"],

    // for environment variables
    env: {},

    // set to false for no bell
    bell: "SOUND",

    // if true, selected text will automatically be copied to the clipboard
    copyOnSelect: false

    // URL to custom bell
    // bellSoundURL: 'http://example.com/bell.mp3',

    // for advanced config flags please refer to https://hyper.is/#cfg
  },

  // a list of plugins to fetch and install from npm
  // format: [@org/]project[#version]
  // examples:
  //   `hyperpower`
  //   `@company/project`
  //   `project#1.0.1`
  plugins: [
    "hyperterm-atom-dark",
    "hyper-opacity",
    "hyperpower",
    "hyper-highlight-active-pane",
    "hyper-pane"
  ],

  // in development, you can create a directory under
  // `~/.hyper_plugins/local/` and include it here
  // to load it and avoid it being `npm install`ed
  localPlugins: []
};
<|endoftext|>