BvS Vacation Reminder

Alerts you when your vacation is ready.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           BvS Vacation Reminder
// @namespace      TheSpy
// @description    Alerts you when your vacation is ready.
// @include        http*://*animecubed.com/billy/bvs/pages/main.html
// @include        http*://*animecubed.com/billy/bvs/village.html
// @include        http*://*animecubed.com/billy/bvs/villagebeach.html
// @include        http*://*animecubedgaming.com/billy/bvs/pages/main.html
// @include        http*://*animecubedgaming.com/billy/bvs/village.html
// @include        http*://*animecubedgaming.com/billy/bvs/villagebeach.html
// @version        1.12
// @history        1.12 New domain - animecubedgaming.com - Channel28
// @history        1.11 Now https compatible (Updated by Channel28)
// @history        1.10 Added grant permissions (Updated by Channel28)
// @history        1.09 Fixed a DOM issue (temporary fix)
// @history        1.08 The floating bar now only shows on the following pages: main, village, beach
// @history        1.07 Fixed wrong date times for last vacation
// @history        1.06 Date and time are in seperate rows, added vacation stamina, added new year compatibility
// @history        1.05 Added multi character support (forgot it earlier)
// @history        1.04 Updated with game update, added floating bar
// @history        1.03 Fixed an 'Apocalypse Calendar' bug
// @history        1.02 Added 'Apocalypse Calendar' support, supports new year as well
// @history        1.01 Initial release
// @licence        MIT; http://www.opensource.org/licenses/mit-license.php
// @copyright      2010-2011, TheSpy
// @grant          GM_log
// @grant          GM_addStyle
// ==/UserScript==

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

/*
	DOM Storage wrapper class (credits: http://userscripts.org/users/dtkarlsson)
	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;
		}
	}
	// Kludge, avoid Firefox crash
	my.removeItem = function(key) {
		try {
			my.storage.setItem(escape(my.ns + key), null);
		}
		catch (e) {
			GM_log(e);
		}
	}
	// Return array of all keys in this namespace
	my.keys = function() {
		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;
	}
}

// UI (credits: http://userscripts.org/users/dtkarlsson)
function Window(id, storage)
{
	var my = this;
	my.id = id;
	my.offsetX = 0;
	my.offsetY = 0;
	my.moving = false;

	// Window dragging events
	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);
			if(x < 0) x = 0;
			if(y < 0) y = 0;
			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";
}

// Parse server time clock
// (credits: http://userscripts.org/users/dtkarlsson)
function parseServerTime()
{
	var clock = document.getElementById("clock");
	if (clock)
		delayedParseServerTime(clock);
}

// Try to parse server time clock periodically. The clock is updated by a timer script
// so it is not available immediately on page load
// (credits: http://userscripts.org/users/dtkarlsson)
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);

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

// Parse vacation time
function parseVacationTime()
{
	var match = document.evaluate("//b/font[contains(translate(@color, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '0000a1')]/text()", document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue.nodeValue.match(/ (\d+)\/(\d+) \(.+ (\d+):(\d+)\)/);
	if (match) {
		var month = parseInt(match[1]) - 1;
		var day = parseInt(match[2]);
		var hour = parseInt(match[3]);
		var minute = parseInt(match[4]);
		var date = new Date();
		date.setTime(serverNow());
		var vacationDate = new Date(date.getFullYear(), month, day, hour, minute);
		var vacationTime = vacationDate.getTime();
		// new year issues, substract one year
		if(serverNow() < vacationTime) {
			vacationDate = new Date(date.getFullYear() - 1, month, day, hour, minute);
			vacationTime = vacationDate.getTime();
		}
		vacationSettings.setItem(playerName() + ".vacation", vacationTime);

		var calendar = document.evaluate("//b[contains(.,'Apocalypse Calendar')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		vacationSettings.setItem(playerName() + ".calendar", calendar.snapshotLength > 0 ? 1 : 0);

		var island = document.evaluate("//b[contains(.,'Neo-Monster Island')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		vacationSettings.setItem(playerName() + ".staminabonus", island.snapshotLength > 0 ? 1 : 0);
	}
	else {
	}
}

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

// 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(vacationSettings.getItem("offset"));
}

// ...
function vacationTime() {
	return parseInt(vacationSettings.getItem(playerName() + ".vacation"));
}

// ...
function vacationStaminaBonus() {
	return parseInt(vacationSettings.getItem(playerName() + ".staminabonus"));
}

// ...
function calendarUpgrade() {
	return parseInt(vacationSettings.getItem(playerName() + ".calendar"));
}

// ...
function vacationStaminaBonusString() {
	var staminabonus;
	var days = (serverNow() - vacationTime()) / (1000 * 60 * 60 * 24);
	if(calendarUpgrade() == 1) {
		days *= 1.5;
	}
	if(days >= 14) {
		staminabonus = 111; 
	}
	else {
		staminabonus = 5 * Math.floor(days);
	}
	if(vacationStaminaBonus() == 1) {
		staminabonus *= 1.5;
	}
	return Math.floor(staminabonus);
}

// ...
function vacationString() {
	if(vacationTime() <= 0) {
		return "Unknown";
	}
	if(calendarUpgrade() == 1) {
		time = 14 * 24 * 60 * 2 / 3;
	}
	else {
		time = 14 * 24 * 60;
	}
	var dif = time - Math.floor((serverNow() - vacationTime()) / (1000 * 60));
	if(dif > 0) {
		var m = dif % 60;
		dif = (dif - m) / 60;
		var h = dif % 24;
		dif = (dif - h) / 24;
		var d = dif;
		return d + "d " + h + "h " + m + "m";
	}
	else {
		return "Ready";
	}
}

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

function timeString(time) {
	time = parseInt(time);
	if(isNaN(time)) {
		return "Unknown";
	}

	var date = new Date();
	date.setTime(time);
	var year = date.getFullYear();
	var month = date.getMonth() + 1;
	var day = date.getDate();
	var hour = date.getHours();
	var minute = date.getMinutes();
	var ampm = (hour >= 12 ? "PM" : "AM");
	hour %= 12;
	if(hour == 0) {
		hour = 12;
	}

	return year + "/" + twoDigits(month) + "/" + twoDigits(day) + "<br/>" + twoDigits(hour) + ":" + twoDigits(minute) + " " + ampm;
}

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

// ...
function FloatingVacation() {
	var my = this;

	my.window = new Window("floatingvacation", vacationSettings);

	// Set up floating vacation layer
	GM_addStyle("#floatingvacation {border: 2px solid #111E01; position: fixed; z-index: 100; color: #FFFFFF; background-color: #434411; padding: 4px; text-align: center; cursor: move;}");
	GM_addStyle("#floatingvacation dl {margin: 0; padding: 0;}");
	GM_addStyle("#floatingvacation dt {margin: 0; padding: 0; font-size: 12px;}");
	GM_addStyle("#floatingvacation dd {margin: 0; padding: 0; font-size: 24px;}");

	my.draw = function() {
		if(!isNaN(vacationTime())) {
			my.window.element.innerHTML = "<dl><dt>Last known vacation</dt><dd id='vacationlast'>Unknown</dd><dt>Vacation status</dt><dd id='vacationleft'>Unknown</dd><dt>Vacation stamina</dt><dd id='vacationstamina'>Unknown</dd></dl>";
		}
		else {
			my.window.element.innerHTML = "<dl><dt>Last known vacation</dt><dd id='vacationlast'>Unknown</dd></dl>";
		}
	}

	my.update = function() {
		var nodevacationlast = document.getElementById("vacationlast");
		if (nodevacationlast) {
			nodevacationlast.innerHTML = timeString(vacationTime());
		}

		var vacationleft = document.getElementById("vacationleft");
		if (vacationleft) {
			vacationleft.innerHTML = vacationString();
		}

		var vacationstamina = document.getElementById("vacationstamina");
		if (vacationstamina) {
			vacationstamina.innerHTML = vacationStaminaBonusString();
		}

		setTimeout(my.update, 1000);
	}

	my.draw();
	my.update();
}

if(playerName() != "none") {
	var vacationSettings = new DOMStorage("local", "BvSVacation");
	var floatingVacation = new FloatingVacation();
}

if (/billy.bvs.pages.main\b/.test(location.href) || /billy.bvs.arena/.test(location.href)) {
	parseServerTime();
}
else if (/billy.bvs.villagebeach/.test(location.href)) {
	parseVacationTime();
}