// ==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();
}