BvS Clock Modified

Floating server time clock for Billy Vs. SNAKEMAN!

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @id             bvsclockmodified
// @name           BvS Clock Modified
// @description    Floating server time clock for Billy Vs. SNAKEMAN!
// @namespace      skarn22
// @include        http*://*animecubed.com/billy/bvs/*
// @include        http*://*animecubedgaming.com/billy/bvs/*
// @licence        MIT; http://www.opensource.org/licenses/mit-license.php
// @copyright      2009, Daniel Karlsson
// @version        1.2.6
// @history        1.2.6 New domain - animecubedgaming.com - Channel28
// @history        1.2.5 Now https compatible (Updated by Channel28)
// @history        1.2.4 Added grant permissions (Updated by Channel28)
// @history        1.2.3 Removed out-dated scriptupdater. Formatting for scriptish.
// @history        1.2.2 Modified to parse the fifth dark hour.
// @history        1.2.2 Fixed invasion timer bug when target name starts with a number
// @history        1.2.1 Fixed a bingo timer bug
// @history        1.2.0 Added timer window with bingo and invasion timers
// @history        1.2.0 Added Dark Hour and dayroll counter
// @history        1.1.3 AM/PM confusion fixed
// @history        1.1.2 Fixed parsing bug
// @history        1.1.1 Fixed annoying flickering while moving the clock
// @history        1.1.0 Toggle 24h/12h clock by doubleclicking on the clock
// @history        1.0.0 Initial release
// @grant          GM_addStyle
// @grant          GM_log
// ==/UserScript==


var SETTINGS = {
	servertime: "12h",
	darkhour: "Countdown",
	dayroll: "Countdown"
};

var OPTIONS = {
	servertime: ["24h", "12h", "Hide"],
	darkhour: ["Countdown", "24h", "12h", "Hide"],
	dayroll: ["Countdown", "24h", "12h", "Hide"],
}

const MINUTE = 60 * 1000; //ms
const HOUR = 60 * MINUTE; //ms
const DAY = 24 * HOUR; //ms
const UPDATEINTERVAL = 250; //ms

/*
	BvS Utility Functions
*/
var BvS = {
	playerName: function() {
		try {
			return document.evaluate("//input[@name='player' and @type='hidden']", document, null,
				XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue.value;
		}
		catch (e) {
			return;
		}
	}
}

/*
	DOM Storage wrapper class
	Constructor:
		var store = new DOMStorage({"session"|"local"}, [<namespace>]);
	Set item:
		store.setItem(<key>, <value>);
	Get item:
		store.getItem(<key>[, <default value>]);
	Remove item:
		store.removeItem(<key>);
	Get all keys in namespace as array:
		var array = store.keys();
*/
function DOMStorage(type, namespace)
{
	var my = this;
	
	if (typeof(type) != "string")
		type = "session";
	switch (type) {
		case "local": my.storage = localStorage; break;
		case "session": my.storage = sessionStorage; break;
		default: my.storage = sessionStorage;
	}
	
	if (!namespace || typeof(namespace) != "string")
		namespace = "Greasemonkey";

	my.ns = namespace + ".";
	my.setItem = function(key, val) {
		try {
			my.storage.setItem(escape(my.ns + key), val);
		}
		catch (e) {
			GM_log(e);
		}
	},
	my.getItem = function(key, def) {
		try {
			var val = my.storage.getItem(escape(my.ns + key));
			if (val)
				return val;
			else
				return def;
		}
		catch (e) {
			return def;
		}
	}
	my.removeItem = function(key) {
		try {
			// Kludge, avoid Firefox crash
			my.storage.setItem(escape(my.ns + key), null);
		}
		catch (e) {
			GM_log(e);
		}
	}
	my.keys = function() {
		// Return array of all keys in this namespace
		var arr = [];
		var i = 0;
		do {
			try {
				var key = unescape(my.storage.key(i));
				if (key.indexOf(my.ns) == 0 && my.storage.getItem(key))
					arr.push(key.slice(my.ns.length));
			}
			catch (e) {
				break;
			}
			i++;
		} while (true);
		return arr;
	}
}

var clockSettings = new DOMStorage("local", "BvSClock");
var playerTimers;
if (BvS.playerName())
	playerTimers = new DOMStorage("local", "BvSClock." + BvS.playerName());

function twoDigits(n)
{
	if (n < 10)
		return "0" + n;
	else
		return "" + n;
}

// Time functions

// Current time in ms since 1970-01-01 UTC
function utcNow()
{
	var d = new Date();
	return d.getTime() + d.getTimezoneOffset() * 60000;
}

// Current server time in ms
function serverNow()
{
	return utcNow() + parseInt(clockSettings.getItem("offset"));
}

// Next dayroll (servertime)
function dayroll()
{
	var dr = new Date();
	dr.setTime(serverNow());
	dr.setHours(5);
	dr.setMinutes(10);
	dr.setSeconds(0);
	dr.setMilliseconds(0);
	dr = dr.getTime();
	if (dr < serverNow())
		dr += DAY;
	return dr;
}

// Milliseconds to hours, minutes, seconds
function msToHMS(t)
{
	if (t < 0)
		return "-" + msToHMS(-t);

	t = Math.ceil(t / 1000);
	var h = Math.floor(t / 3600);
	var m = Math.floor((t % 3600) / 60);
	var s = t % 60;
	return twoDigits(h) + ":" + twoDigits(m) + ":" + twoDigits(s);
}

// Convert 12h to 24h
function convert12h_24h(hour, ampm)
{
	hour %= 12;
	if (ampm == "PM")
		hour += 12;
	return hour;
}

// Convert time (in ms from 1970-01-01 BvS time)
function timeString(time, fmt)
{
	// Formats:
	// Countdown: T-hh:mm:ss
	// 12h: hh:mm:ss am/pm
	// 24h: hh:mm:ss
	time = parseInt(time);
	
	if (fmt == "Countdown") {
		var str = msToHMS(time - serverNow());
		if (str[0] == "-")
			return "T+" + str.substr(1);
		else
			return "T-" + str;
	} else if (fmt == "Timer") {
		var seconds = (time - serverNow()) / 1000;
		if (seconds < 0)
			return "Now";
		var minutes = seconds / 60;
		var hours = minutes / 60;
		if (hours > 4)
			return Math.round(hours) + " h";
		else if (minutes > 5)
			return Math.round(minutes) + " min";
		else
			return Math.round(seconds) + " s";
	} else {
		var d = new Date();
		d.setTime(time);
		var h = d.getHours();
		var m = d.getMinutes();
		var s = d.getSeconds();
		
		if (fmt == "24h")
			return twoDigits(h) + ":" + twoDigits(m) + ":" + twoDigits(s);
		else if (fmt == "12h") {
			var ampm = (h >= 12 ? "PM" : "AM");
			h %= 12;
			if (h == 0)
				h = 12;
			return twoDigits(h) + ":" + twoDigits(m) + ":" + twoDigits(s) + " " + ampm;
		}
	}
}

// Parsing

// Get player name
function playerName()
{
	var input = document.evaluate("//input[@name='player' and @type='hidden']", document, null,
		XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;
	if (input)
		return input.value;
}

// Try to parse server time clock periodically. The clock is updated by a timer script
// so it is not available immediately on page load
function delayedParseServerTime(element)
{
	var match = element.textContent.match(/0?(\d+):0?(\d+):0?(\d+) (.M)/);
	if (match) {
		var hours = parseInt(match[1]);
		var minutes = parseInt(match[2]);
		var seconds = parseInt(match[3]);

		hours = hours % 12;
		if (match[4] == "PM")
			hours += 12;

		var server = new Date();
		server.setHours(hours);
		server.setMinutes(minutes);
		server.setSeconds(seconds);
		server.setMilliseconds(0);

		// Make sure offset is < 0 and > -12h
		var offset = server.getTime() - utcNow();
		if (offset > 0)
			offset -= DAY;
		if (offset < -DAY / 2)
			offset += DAY;

		var oldOffset = getOffset();
		
		if (Math.abs(oldOffset - offset) < 10000)
			offset = Math.round((offset + oldOffset) / 2);

		clockSettings.setItem("offset", offset);
		clockSettings.setItem("sync", utcNow());
	} else {
		// Try again in 0.25s
		setTimeout(function() {delayedParseServerTime(element);}, 250);
	}
}

// Helper function for getting clock offset from localStorage
function getOffset()
{
	var offset;
	try {
		offset = clockSettings.getItem("offset");
		return parseInt(offset);
	}
	catch (e) {
		GM_log(e);
		return;
	}
}

// Parse server time clock
function parseServerTime()
{
	var clock = document.getElementById("clock");
	if (clock)
		delayedParseServerTime(clock);
}

// Parse dark hours
function parseDarkHours()
{
	var dh = document.getElementById("hours");
	if (dh) {
		var match = dh.textContent.match(
			/(\d+).?(.M)[^\d]*(\d+).?(.M)[^\d]*(\d+).?(.M)[^\d]*(\d+).?(.M)[^\d]*(\d+).?(.M)/);
		if (match) {
			var hours = [];
			
			for (var i = 0; i < 5; i++) {
				hours[i] = new Date();
				hours[i].setTime(serverNow());
				hours[i].setHours(convert12h_24h(parseInt(match[2 * i + 1]), match[2 * i + 2]));
				hours[i].setMinutes(0);
				hours[i].setSeconds(0);
				hours[i].setMilliseconds(0);
				
				hours[i] = hours[i].getTime();
				if (hours[i] + DAY < dayroll() - HOUR)
					hours[i] += DAY;
				clockSettings.setItem("darkhour" + i, hours[i]);
			}
			return true;
		}
	}
}

function parseInvasionPlan()
{
	var data = document.evaluate("//table[@width='240']/tbody/tr/td", document, null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		
	var village, time;
	for (var i = 0; i < data.snapshotLength; i++) {
		var txt = data.snapshotItem(i).textContent;
		var rows = txt.split(/\n/);
		for (var r in rows) {
			var match = rows[r].match(/Planning to Invade:\s*(.*) Village(.*)/);
			if (match) {
				village = match[1];
				time = match[2];
				if (/(\d+)$/.test(time))
					time = parseInt(RegExp.lastParen) * MINUTE;
				else if (/Invasion is Ready/.test(time))
					time = 0;
				else
					time = false;
				break;
			} else if (/Planning to Invade: None/.test(rows[r])) {
				playerTimers.removeItem("invasion.targer");
				playerTimers.removeItem("invasion.time");
				break;
			}
		}
	}
	if (village && (time || time == 0) && playerTimers) {
		playerTimers.setItem("invasion.target", village);
		playerTimers.setItem("invasion.time", time + serverNow());
	}
}

function parseBingoCooldown()
{
	if (!/billy.bvs.pages.main/.test(location.href))
		return;

	var data = document.evaluate("//table[count(descendant::tr)=1 and " +
		"count(descendant::td)=1]/tbody/tr/td", document, null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

	var cooldown = 0;
	for (var i = 0; i < data.snapshotLength; i++) {
		var txt = data.snapshotItem(i).textContent.replace(/\s+/g, " ");
		var match = txt.match(/(.*)\!.*Release in[^\d]*(\d+) (\w+)/);
		if (match) {
			var unit = match[3].replace(/\s+/g, "");
			var type = match[1].replace(/\s+/g, "");
			var time = parseInt(match[2]);
			var min, max;
			switch (unit) {
			case "hours":
				min = time * HOUR;
				max = min + HOUR;
				break;
			case "minutes":
				min = time * MINUTE;
				max = min + MINUTE;
				break;
			default:
				min = time * 1000;
				max = min;
			}
			min += serverNow();
			max += serverNow();
			
			var set, remove;
			switch (type) {
			case "Bingo'd":
				set = "bingo";
				remove = "cooldown";
				break;
			case "Cooldown":
				set = "cooldown";
				remove = "bingo";
				break;
			}
			try {
				t = playerTimers.getItem(set).split(/-/);
				var pmin = parseInt(t[0]);
				var pmax = parseInt(t[1]);
				pmin = Math.max(min, pmin);
				pmax = Math.min(max, pmax);
				if (pmax >= pmin) {
					min = pmin;
					max = pmax;
				}
			}
			catch (e) {}
			playerTimers.setItem(set, min + "-" + max);
			playerTimers.removeItem(remove);
			return;
		}
	}
	playerTimers.removeItem("cooldown");
	playerTimers.removeItem("bingo");
}

// UI

function Window(id, storage)
{
	var my = this;
	
	my.id = id;
	
	// Window dragging events
	my.offsetX = 0;
	my.offsetY = 0;
	my.moving = false;
	my.drag = function(event) {
		if (my.moving) {
			my.element.style.left = (event.clientX - my.offsetX)+'px';
			my.element.style.top = (event.clientY - my.offsetY)+'px';
			event.preventDefault();
		}
	}
	my.stopDrag = function(event) {
		if (my.moving) {
			my.moving = false;
			var x = parseInt(my.element.style.left);
			var y = parseInt(my.element.style.top);
			storage.setItem(my.id + ".coord.x", x);
			storage.setItem(my.id + ".coord.y", y);
			my.element.style.opacity = 1;
			window.removeEventListener('mouseup', my.stopDrag, true);
			window.removeEventListener('mousemove', my.drag, true);
		}
	}
	my.startDrag = function(event) {
		if (event.button != 0) {
			my.moving = false;
			return;
		}
		my.offsetX = event.clientX - parseInt(my.element.style.left);
		my.offsetY = event.clientY - parseInt(my.element.style.top);
		my.moving = true;
		my.element.style.opacity = 0.75;
		event.preventDefault();
		window.addEventListener('mouseup', my.stopDrag, true);
		window.addEventListener('mousemove', my.drag, true);
	}

	my.element = document.createElement("div");
	my.element.id = id;
	document.body.appendChild(my.element);
	my.element.addEventListener('mousedown', my.startDrag, true);

	if (storage.getItem(my.id + ".coord.x"))
		my.element.style.left = storage.getItem(my.id + ".coord.x") + "px";
	else
		my.element.style.left = "6px";
	if (storage.getItem(my.id + ".coord.y"))
		my.element.style.top = storage.getItem(my.id + ".coord.y") + "px";
	else
		my.element.style.top = "6px";

}

function FloatingClock()
{
	var my = this;

	my.window = new Window("floatingclock", clockSettings);
	
	// Set up floating clock
	GM_addStyle("#floatingclock {border: 2px solid black; position: fixed; z-index: 100; " +
		"color: white; background-color: rgb(2%, 28%, 4%); padding: 4px; " +
		"text-align: center; cursor: move;");
	GM_addStyle("#floatingclock dl {margin: 0; padding: 0;}");
	GM_addStyle("#floatingclock dt {margin: 0; padding: 0; font-size: 12px;}");
	GM_addStyle("#floatingclock dd {margin: 0; padding: 0; font-size: 24px;}");

	// Updates the clock periodically
	my.update = function()
	{
		var node = document.getElementById("bcservertime");
		if (!node)
			return;

		var offset = getOffset();
		if (!offset)
			return;

		var clock = new Date();
		clock.setTime(utcNow() + parseInt(offset));

		node.textContent = timeString(serverNow(), SETTINGS.servertime);

		var dr = document.getElementById("bcdayroll");
		if (dr)
			dr.textContent = timeString(dayroll(), SETTINGS.dayroll);
			
		var dh = document.getElementById("bcdarkhour");
		if (dh) {
			var clock = document.getElementById("floatingclock");
			var next = DAY;
			var now = serverNow();
			for (var i = 0; i < 5; i++) {
				var t = parseInt(clockSettings.getItem("darkhour" + i)) - now;
				if (t < next && t > -HOUR)
					next = t;
			}
			if (next < 0) {
				clock.style.backgroundColor = "rgb(22%, 1%, 9%)";
				dh.textContent = "Now";
			} else {
				clock.style.backgroundColor = "rgb(2%, 28%, 4%)";
				dh.textContent = timeString(next + now, SETTINGS.darkhour);
			}
		}
		setTimeout(my.update, UPDATEINTERVAL);
	}

	my.redraw = function() {
		var html = "<dl>" +
			"<dt>BvS Server Time</dt>" +
			"<dd id='bcservertime'>??:??:??</dd>";
		if (SETTINGS.darkhour != "Hidden")
			html += "<dt>Next Dark Hour</dt><dd id='bcdarkhour'>??:??:??</dd>";
		if (SETTINGS.dayroll != "Hidden")
			html += "<dt>Dayroll</dt><dd id='bcdayroll'>??:??:??</dd>";
		html += "</dl>";
		my.window.element.innerHTML = html;
	}

	my.redraw();
	my.update();
}

function Timers()
{
	if (!playerTimers)
		return;
	
	var my = this;

	my.window = new Window("bctimers", playerTimers);
	
	// Set up floating clock
	GM_addStyle("#bctimers {border: 2px solid black; position: fixed; z-index: 100; " +
		"color: white; background-color: rgb(2%, 28%, 4%); padding: 4px; " +
		"text-align: center; cursor: move;");
	GM_addStyle("#bctimers table {color: white; margin: 0; padding: 0; font-size: 12px; border-collapse: collapse;}");
	GM_addStyle("#bctimers thead {font-size: 16px;}");
	GM_addStyle("#bctimers td {padding: 3px;}");
	GM_addStyle("#bctimers td.time {color: yellow; text-align: right;}");

	// Updates the clock periodically
	my.update = function()
	{
		var tbody = my.window.element.getElementsByTagName("tbody")[0];
		
		var html = "";
		if (playerTimers.getItem("cooldown")) {
			var t = playerTimers.getItem("cooldown").split(/-/);
			t = (parseInt(t[0]) + parseInt(t[1])) / 2;
			if (t - serverNow() > 0)
				html += "<tr><td>Cooldown</td><td class='time'>" +
					timeString(t, "Timer") +
					"</td></tr>";
			else
				playerTimers.removeItem("cooldown");
		} else if (playerTimers.getItem("bingo")) {
			var t = playerTimers.getItem("bingo").split(/-/);
			t = (parseInt(t[0]) + parseInt(t[1])) / 2;
			if (t - serverNow() > 0)
				html += "<tr><td>Bingo</td><td class='time'>" +
					timeString(t, "Timer") +
					"</td></tr>";
			else
				playerTimers.removeItem("bingo");
		}
		if (playerTimers.getItem("invasion.target")) {
			var time = "";
			if (parseInt(playerTimers.getItem("invasion.time")) < serverNow())
				time = "Now";
			else
				time = timeString(playerTimers.getItem("invasion.time"), "Timer");
			html += "<tr><td>Invasion: " + playerTimers.getItem("invasion.target") +
				"</td><td class='time'>" + time +
				"</td></tr>";
		}
		
		tbody.innerHTML = html;
		setTimeout(my.update, UPDATEINTERVAL);
	}

	my.redraw = function() {
		my.window.element.innerHTML = "<table><thead>" +
			"<tr><td colspan='2'>Timers - " + playerName() + "</td></tr>" +
			"</thead><tbody/></table>";
	}

	my.redraw();
	my.update();
}

var clock = new FloatingClock();
var timers = new Timers();

if (/billy.bvs.pages.main\b/.test(location.href)) {
	parseServerTime();
	parseDarkHours();
	parseBingoCooldown();
} else if (/billy.bvs.arena/.test(location.href)) {
	parseServerTime();
	parseDarkHours();
} else if (/billy.bvs.village\b/.test(location.href)) {
	parseInvasionPlan();
}