Floating server time clock for Billy Vs. SNAKEMAN!
// ==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(); }