BvS Improved Spy Report

Tabulated and sorted spy report for Billy vs SNAKEMAN.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           BvS Improved Spy Report
// @namespace      http://userscripts.org/users/dtkarlsson
// @description    Tabulated and sorted spy report for Billy vs SNAKEMAN.
// @include        http*://*animecubed.com/billy/bvs/village.*
// @include        http*://*animecubed.com/billy/bvs/villagespyreport.*
// @include        http*://*animecubedgaming.com/billy/bvs/village.*
// @include        http*://*animecubedgaming.com/billy/bvs/villagespyreport.*
// @match          http*://*animecubed.com/billy/bvs/village.*
// @match          http*://*animecubed.com/billy/bvs/villagespyreport.*
// @match          http*://*animecubedgaming.com/billy/bvs/village.*
// @match          http*://*animecubedgaming.com/billy/bvs/villagespyreport.*
// @licence        MIT; http://www.opensource.org/licenses/mit-license.php
// @copyright      2009, Daniel Karlsson (http://userscripts.org/users/dtkarlsson)
// @version        1.2.3
// @history        1.2.3 New domain - animecubedgaming.com - Channel28
// @history        1.2.2 Now https compatible (Updated by Channel28)
// @history        1.2.1 Added grant permissions (Updated by Channel28)
// @history        1.2.0 Added tier filter options
// @history        1.2.0 Stable sort for Chrome
// @history        1.1.1 Support for Google Chrome
// @grant          GM_log
// ==/UserScript==

var log;
if (console && console.log)
	log = console.log;
else if (GM_log)
	log = GM_log;
else
	log = function() {}

/*
	Village objects have the following members:
	name - Village name (string)
	order - Original spy report order (integer, 1 - <n>)
	ryo - Ryo Stores (integer)
	bc - Brilliant Crystals (integer)
	mw - Medicinal Water (integer)
	pm - Precious Metals (integer)
	sf - Solid Fire (integer)
	ui - Unmelting Ice (integer)
	defense - Village defended against (string)
	target - Current target (string)
	attackers - Number of villages attacking (integer)
	tier - 0 - Pre-NRF, 1 - NRF, 2 - Beacon
	peace - Peacetime (integer)
	weight - Sorting weight (number)
*/

function Village()
{
	// Init members that might be missing from spy report
	this.defense = "";
	this.target = "";
	this.peace = 0;
	this.tier = 0;
}

/*
	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 = "Userscript";

	my.ns = namespace + ".";
	my.setItem = function(key, val) {
		try {
			my.storage.setItem(escape(my.ns + key), val);
		}
		catch (e) {
			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) {
			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;
	}
}

// Merge sort: merge function
function _merge(array, left, right, cmp)
{
	var ai = 0;
	var li = 0;
	var ri = 0;
	
	while (li < left.length && ri < right.length)
		if (cmp(left[li], right[ri]) <= 0)
			array[ai++] = left[li++];
		else
			array[ai++] = right[ri++];
			
	while (li < left.length)
		array[ai++] = left[li++];
	while (ri < right.length)
		array[ai++] = right[ri++];
}

// Merge sort
function mergeSort(array, cmp)
{
	if (array.length <= 1)
		return array;

	var left = [];
	var right = [];
	var mid = Math.floor(array.length / 2);
	
	// Divide
	for (var i = 0; i < mid; i++)
		left.push(array[i]);
	for (var i = mid; i < array.length; i++)
		right.push(array[i]);

	// Sort parts
	mergeSort(left, cmp);
	mergeSort(right, cmp);
	
	// Merge parts
	_merge(array, left, right, cmp);
}

function getPlayerName()
{
	return document.evaluate("//input[@name='player']", document, null,
		XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue.value;
}

function f(n)
{
	if (!n)
		return 0;
	// mapping from [0, inf) to [0, 1)
	return n / (n + 1);
}

function weight(village, wbc, wmw, wpm, wsf, wui)
{
	// Village resource weight used for sorting, higher weight on top
	var w = 0;
	var wsum = wbc + wmw + wpm + wsf + wui;
	if (wsum == 0)
		return 0;

	// Tiebreaker
	w = f(village.bc) * wbc + f(village.mw) * wmw + f(village.pm) * wpm + f(village.sf) * wsf +
		f(village.ui) * wui;
	w /= wsum;

	for (var n = 4; n >= 0; n--) {
		if (village.bc > n)
			w += wbc;
		if (village.mw > n)
			w += wmw;
		if (village.pm > n)
			w += wpm;
		if (village.sf > n)
			w += wsf;
		if (village.ui > n)
			w += wui;
		w /= wsum;
	}
	return w;
}

function numberToString(n)
{
	// Add thousand separator
	// 1234567 -> "1,234,567"
	if (n < 0)
		return "-" + numberToString(-n);
	var str = "" + Math.round(n);
	return str.replace(/\d{1,3}(?=(\d{3})+(?!\d))/g, "$&,");
}

function stringToNumber(n)
{
	// n should be a string, possibly with "," as thousand separator
	if (n)
		return parseInt(n.replace(/,/g, ""));
	return 0;
}

function cmpOrder(v1, v2)
{
	// Sort by ascending order
	return cmpTier(v1, v2) || v1.order - v2.order;
}

function cmpAttackers(v1, v2)
{
	// Sort by ascending number of attackers
	return cmpTier(v1, v2) || v1.attackers - v2.attackers;
}

function cmpName(v1, v2)
{
	// Sort by ascending name
	if (cmpTier(v1, v2)) return cmpTier(v1, v2);
	if (v1.name < v2.name)
		return -1;
	else if (v1.name > v2.name)
		return 1;
	else
		return 0;
}

function cmpPeace(v1, v2)
{
	// Sort by ascending peacetime
	return cmpTier(v1, v2) || v1.peace - v2.peace;
}

function cmpRyo(v1, v2)
{
	// Sort by descending ryo
	return cmpTier(v1, v2) || v2.ryo - v1.ryo;
}

function cmp9(v1, v2)
{
	// Sort by descending UI
	return cmpTier(v1, v2) || v2.ui - v1.ui;
}

function cmpDefense(v1, v2)
{
	// Sort by ascending defense name
	if (cmpTier(v1, v2)) return cmpTier(v1, v2);
	if (v1.defense < v2.defense)
		return -1;
	else if (v1.defense > v2.defense)
		return 1;
	else
		return 0;
}

function cmpTarget(v1, v2)
{
	// Sort by ascending target
	if (cmpTier(v1, v2)) return cmpTier(v1, v2);
	if (v1.target < v2.target)
		return -1;
	else if (v1.target > v2.target)
		return 1;
	else
		return 0;
}

function cmpRes(v1, v2)
{
	// Sort by descending resources
	return cmpTier(v1, v2) || v2.weight - v1.weight;
}

function cmpTier(v1, v2)
{
	// Sort by Tier
	return v2.tier - v1.tier;
}

// Global-ish variables
var tierFilter = [true, true, true];
var villageList;
var comparator = [
	cmpOrder,
	cmpAttackers,
	cmpName,
	cmpPeace, 
	cmpRyo,
	cmpRes,
	cmpRes,
	cmpRes,
	cmpRes,
	cmpRes,
	cmpDefense,
	cmpTarget
	];

function setButton(n, state)
{
	var b = document.getElementById("isr_button" + n);
	if (state)
		b.setAttribute("class", "active");
	else
		b.removeAttribute("class");
}

function toggleButton(n)
{
	setButton(n, !isActive(n));
}

function isActive(n)
{
	// Return true is button <n> is activated
	if (/active/.test(document.getElementById("isr_button" + n).getAttribute("class")))
		return true;
	return false;
}
	
function sortOption(n)
{
	// Click event handler sends button id, grab number from the id
	var match = n.match(/\d+/);
	if (match)
		n = parseInt(match[0]);
	else
		return;
	
	if (n >= 5 && n <= 9) {
		for (var i = 1; i <= 4; i++)
			setButton(i, false);
		for (var i = 10; i <= 11; i++)
			setButton(i, false);
		toggleButton(n);
	} else
		for (var i = 1; i <= 11; i++)
			if (i == n)
				toggleButton(i);
			else
				setButton(i, false);

	n = 0;
	for (var i = 1; i <= 11; i++)
		if (isActive(i)) {
			n = i;
			break;
		}
		
	if (n >= 5 && n <= 9) {
		// Sorting by resources, recalculate weights
		for (var i = 0; i < villageList.length; i++) {
			var v = villageList[i];
			v.weight = weight(v,
				isActive(5) ? 1 : 0, 
				isActive(6) ? 1 : 0, 
				isActive(7) ? 1 : 0, 
				isActive(8) ? 1.1 : 0, 
				isActive(9) ? 1.1 : 0);
		}
	}
	mergeSort(villageList, comparator[n]);
	fillSpyTable();
}

function createElement(tag, attributes, html)
{
	var node = document.createElement(tag);
	if (attributes) {
		for (var a in attributes)
			node.setAttribute(a, attributes[a]);
	}
	if (html) {
		node.innerHTML = html;
	}
	return node;
}

// Create table row and fill with td from array
function tableRow(data)
{
	var tr = document.createElement("tr");
	
	for (var i in data) {
		var td = document.createElement("td");
		tr.appendChild(td);
		
		switch (typeof(data[i])) {
		case "string":
		case "number":
			td.textContent = data[i];
			break;
		case "object":
			if (data[i].element)
				td.appendChild(data[i].element);
			if (data[i].text)
				td.textContent = data[i].text;
			if (data[i].html)
				td.innerHTML = data[i].html;
			if (data[i].cls)
				td.setAttribute("class", data[i].cls);
			if (data[i].id)
				td.setAttribute("id", data[i].id);
			break;
		}
	}
	return tr;
}

function addHeaderNode(tag, type, text)
{
	var head = document.getElementsByTagName('head')[0];
	if (!head)
		return;
	var node = document.createElement(tag);
	node.type = type;
	node.textContent = text;
	head.appendChild(node);
}

function findSpyReportTd()
{
	// villagespyreport.html spy report
	var bSnap = document.evaluate("//tbody/tr/td/b[1]", document, null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	
	for (var i = 0; i < bSnap.snapshotLength; i++)
		if (/spy report/i.test(bSnap.snapshotItem(i).innerHTML))
			return bSnap.snapshotItem(i).parentNode;

	return null;
}

function parseSpyReport()
{
	// node should be the <td> containing spy report
	var spyTable = findSpyReportTd();
	if (!spyTable)
		return null;
	
	// Scan the spy report and collect spy data
	villageList = [];
	var villageMap = {};
	var header;
	var currentVillage = new Village();
	for (var line = spyTable.firstChild; line != null; line = line.nextSibling) {
		if (line.tagName) {
			if (/b/i.test(line.tagName) && !header) {
				header = line.textContent;
			} else if (/hr/i.test(line.tagName)) {
				// Village done, don't store unless we found a name
				if (currentVillage.name) {
					villageList.push(currentVillage);
					villageMap[currentVillage.name] = villageList.length - 1;
				}
				currentVillage = new Village();
			}
			continue;
		}
		// Skip non-text nodes
		if (line.nodeType != 3)
			continue;
		
		var txt = line.textContent;
		
		if (/NRF Found/.test(txt))
			currentVillage.tier = 1;
		else if (/Beacon Found/.test(txt))
			currentVillage.tier = 2;
		else if (/Defended.from\:.(.*).Village/.test(txt))
			currentVillage.defense = RegExp.lastParen;
		else if (/Attack Plans: (.*) Village/.test(txt))
			currentVillage.target = RegExp.lastParen;
		else if (/Current Stores: (.*) Ryo/.test(txt))
			currentVillage.ryo = stringToNumber(RegExp.lastParen);
		else if (/Brilliant Crystals: (\d+)/.test(txt))
			currentVillage.bc = stringToNumber(RegExp.lastParen);
		else if (/Medicinal Water: (\d+)/.test(txt))
			currentVillage.mw = stringToNumber(RegExp.lastParen);
		else if (/Precious Metals: (\d+)/.test(txt))
			currentVillage.pm = stringToNumber(RegExp.lastParen);
		else if (/Solid Fire: (\d+)/.test(txt))
			currentVillage.sf = stringToNumber(RegExp.lastParen);
		else if (/Unmelting Ice: (\d+)/.test(txt))
			currentVillage.ui = stringToNumber(RegExp.lastParen);
		else if (/Peacetime Bonus: (.?\d+)/.test(txt))
			currentVillage.peace = stringToNumber(RegExp.lastParen);
		else if (/(.*) Village/.test(txt))
			currentVillage.name = RegExp.lastParen;
	}
	if (currentVillage.name) {
		villageList.push(currentVillage);
		villageMap[currentVillage.name] = villageList.length - 1;
	}

	// Calculate weight and assign ordinal numbers
	var n = 1;
	for (var i in villageList) {
		villageList[i].weight = weight(villageList[i], 1, 1, 1, 1.1, 1.1);
		villageList[i].order = n++;
		villageList[i].attackers = 0;
	}
	
	for (var v1 in villageList) {
		if (villageList[v1].target) {
			var v2 = villageMap[villageList[v1].target];
			if (v2) {
				if (villageList[v1].tier == villageList[v2].tier)
					villageList[v2].attackers++;
				else
					villageList[v1].target = "(" + villageList[v1].target + ")";
			}
		}
		if (villageList[v1].defense) {
			var v2 = villageMap[villageList[v1].defense];
			if (v2 && villageList[v1].tier != villageList[v2].tier)
				villageList[v1].defense = "(" + villageList[v1].defense + ")";
		}
	}

	header = header.replace(/.*(Spy Report.*\)).*/, "$1");
	
	return header;
}

function createSpyTable(id, header)
{
	// Create table, headers and empty tbody
	var table = document.createElement("table");
	table.id = id;

	table.innerHTML = "<thead><tr class='isrtitle'><th colspan='11'/></tr>" +
		"<tr class='isrbuttonrow'>" +
		"<th id='isr_button1'>#Att</th>" +
		"<th id='isr_button2'>Village</th>" +
		"<th id='isr_button3'>Peace</th>" +
		"<th id='isr_button4'>Ryo</th>" +
		"<th id='isr_button5' class='active'>BC</th>" +
		"<th id='isr_button6' class='active'>MW</th>" +
		"<th id='isr_button7' class='active'>PM</th>" +
		"<th id='isr_button8' class='active'>SF</th>" +
		"<th id='isr_button9' class='active'>UI</th>" +
		"<th id='isr_button11'>Target</th>" +
		"<th id='isr_button10'>Defense</th>" +
		"</tr></thead>";

	table.getElementsByTagName("th")[0].innerHTML = header;

	var tbody = document.createElement("tbody");
	table.appendChild(tbody);
	tbody.id = "isrbody";
	return table;
}

function fillSpyTable()
{
	// Get saved spy insertion times
	var player = getPlayerName();
	var db = new DOMStorage("local", "BvSImprovedSpyReport." + player);
	var infiltrating = {};
	var inf = db.getItem("infiltrating", "").split(":");
	for (var i = 0; i < inf.length; i++) {
		var match = inf[i].match(/(.*)=(.*)/);
		if (match)
			infiltrating[match[1]] = parseInt(match[2]);
	}
	var date = new Date();
	var age = parseInt(db.getItem("timestamp", "0"));
	age = Math.floor((date.getTime() - age) / 3600 / 1000);

	// Fill in <tbody>
	var tbody = document.getElementById("isrbody");
	tbody.textContent = "";
	var even = true;
	for (var i in villageList) {
		var v = villageList[i];
		
		if (!tierFilter[v.tier])
			continue;

		var data = [];
		data.push({cls: "number", text: v.attackers});
		if (infiltrating[v.name]) {
			// Village is still being infiltraded according to latest seen village.html spy list
			var t = infiltrating[v.name] - age;
			if (t < 0)
				data.push({cls: "name", html: "<a href='./vlookup.html?village=" + v.name + "'>" +
					v.name + "</a> (0 h?)"});
			else
				data.push({cls: "name", html: "<a href='./vlookup.html?village=" + v.name + "'>" +
					v.name + "</a> (" + t + " h)"});
		} else
			data.push({cls: "name", html: "<a href='./vlookup.html?village=" + v.name + "'>" +
				v.name + "</a>"});
		data.push({cls: "peace", text: v.peace ? "+" + v.peace : ""});
		data.push({cls: "ryo", text: numberToString(v.ryo)});
		data.push({cls: "resource", text: v.bc});
		data.push({cls: "resource", text: v.mw});
		data.push({cls: "resource", text: v.pm});
		data.push({cls: "resource", text: v.sf});
		data.push({cls: "resource", text: v.ui});
		if (v.target[0] == "(")
			data.push({cls: "name oot", text: v.target});
		else
			data.push({cls: "name", text: v.target});
		if (v.defense[0] == "(")
			data.push({cls: "name oot", text: v.defense});
		else
			data.push({cls: "name", text: v.defense});

		var tr = tableRow(data);
		tbody.appendChild(tr);
		tr.setAttribute("class", (even ? "even" : "odd") + " tier" + v.tier);
		even = !even;
	}
}

function improveSpyReport() {
	// Insert stylesheet
	addHeaderNode("style", "text/css",
		"table#improvedspyreport {width: 525px; font-size: 12px; " +
			"background-color: black; border-spacing: 1px;}\n" +
		"#improvedspyreport .isrtitle {font-size: 15px;}\n" +
		"#improvedspyreport thead {background-color: rgb(220, 180, 140); font-weight: bold; " +
			"text-align: center;}\n" +
		"#improvedspyreport th {border: 1px outset rgb(220, 180, 140);}\n" +
		"#improvedspyreport td {padding: 1px;}\n" +
		".isrbuttonrow th {cursor: pointer;}\n" +
		".isrbuttonrow th.active {border: 1px inset rgb(200, 160, 125) !important; color: yellow;}\n" +
		"#isrbody a {font-weight: bold;}\n" +
		"#isrbody .oot {color: grey;}\n" +
		"#isrbody .even.tier2 {background-color: rgb(234, 216, 195);}\n" +
		"#isrbody .odd.tier2 {background-color: rgb(222, 202, 168);}\n" +
		"#isrbody .even.tier1 {background-color: rgb(195, 216, 234);}\n" +
		"#isrbody .odd.tier1 {background-color: rgb(168, 202, 222);}\n" +
		"#isrbody .even.tier0 {background-color: rgb(195, 234, 216);}\n" +
		"#isrbody .odd.tier0 {background-color: rgb(168, 222, 202);}\n" +
		"#isrbody tr.tier0:hover {background-color: rgb(255, 255, 95);}\n" +
		"#isrbody tr.tier1:hover {background-color: rgb(255, 255, 95);}\n" +
		"#isrbody tr.tier2:hover {background-color: rgb(255, 255, 95);}\n" +
		"#isrbody td.number {text-align: center;}\n" +
		"#isrbody td.peace {text-align: center;}\n" +
		"#isrbody td.ryo {text-align: right;}\n" +
		"#isrbody td.name {overflow: hidden;}\n" +
		"#isrbody td.resource {width: 24px; text-align: right;}\n" +
		"#isrtierselect {padding: 0; text-align: center;}\n" +
		"#isrtierselect li {display: inline; margin: 0 2px 0 2px; padding: 2px 3px 2px 3px; " +
			"cursor: pointer; font-weight: bold;}\n" +
		"#isrtierselect .button {border: 2px outset grey;}\n" +
		"#isrtierselect .activebutton {border: 2px inset grey;}\n" +
		"#isrtierselect .tier2 {border-color: rgb(222, 202, 168); background-color: rgb(222, 202, 168);}\n" +
		"#isrtierselect .tier1 {border-color: rgb(168, 202, 222); background-color: rgb(168, 202, 222);}\n" +
		"#isrtierselect .tier0 {border-color: rgb(168, 222, 202); background-color: rgb(168, 222, 202);}");

	// Find spy report <td> node
	var spyTable = findSpyReportTd();
	if (!spyTable)
		return;
	var header = parseSpyReport();
	if (!header)
		return;
	// Go to <table> parent
	while (!/table/i.test(spyTable.tagName))
		spyTable = spyTable.parentNode;

	var player = getPlayerName();
	var db = new DOMStorage("local", "BvSImprovedSpyReport." + player);

	// Sort by resources
	mergeSort(villageList, cmpRes);

	var header = "- " + header + " (" + villageList.length + " villages) -";
	var improvedSpyTable = createSpyTable("improvedspyreport", header);
	spyTable.parentNode.replaceChild(improvedSpyTable, spyTable);
	
	// Tier filter
	tierFilter = [];
	var t = db.getItem("tiers", "0:1:2").split(/:/);
	for (var i in t)
		tierFilter[parseInt(t[i])] = true;
	
	var tiersel = document.createElement("ul");
	tiersel.id = "isrtierselect";
	for (var t = 2; t >= 0; t--) {
		var li = document.createElement("li");
		if (tierFilter[t])
			li.setAttribute("class", "activebutton tier" + t);
		else
			li.setAttribute("class", "button tier" + t);
		switch (t) {
		case 2: li.textContent = "Beacon (Tier 3)"; break;
		case 1: li.textContent = "NRF (Tier 2)"; break;
		case 0: li.textContent = "Basic (Tier 1)"; break;
		}
		li.id = "isrtier" + t;
		tiersel.appendChild(li);
		li.addEventListener("click", function(event) {
			if (/isrtier(\d+)/.test(event.target.id)) {
				var tier = parseInt(RegExp.lastParen);
				tierFilter[tier] = !tierFilter[tier];
				if (tierFilter[tier])
					event.target.setAttribute("class", "activebutton tier" + tier);
				else
					event.target.setAttribute("class", "button tier" + tier);
				fillSpyTable();
				var save = [];
				for (var t = 0; t < 3; t++)
					if (tierFilter[t])
						save.push(t);
				db.setItem("tiers", save.join(":"));
				event.preventDefault();
			}
		}, true);
	}
	improvedSpyTable.parentNode.insertBefore(tiersel, improvedSpyTable);
	
	var date = new Date();
	var age = parseInt(db.getItem("timestamp", "0"));
	age = Math.floor((date.getTime() - age) / 3600 / 1000);

	if (age > 0) {
		var div = document.createElement("div");
		improvedSpyTable.parentNode.insertBefore(div, improvedSpyTable);
		div.setAttribute("style", "font-size: 14px; text-align: center; font-weight: bold;");
		div.innerHTML = "Spy insertion times updated " + age + " hours ago. Check Village/Show All " +
			"Spies to update.";
	}

	fillSpyTable();
	
	// Listen to click events on the button row
	// send element id to sortOption function
	document.getElementById("isr_button1").parentNode.addEventListener(
		'mousedown',
		function(event) {
			sortOption(event.target.getAttribute("id"));
			return;
		},
		true);
}

function scanSpyList()
{
	// village.html spy list
	var spies = document.evaluate("//table/tbody/tr/td/div/ul/li",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

	var found = false;
	var infiltrating = [];
	for (var i = 0; i < spies.snapshotLength; i++) {
		var spy = spies.snapshotItem(i).innerHTML;
		// Match "Villagename (xxH to go)" and "Villagename"
		// where villagename can be one or two words consisting of letters and digits
		// "Foo Bar (12H to go)" => ["Foo Bar (12H to go)", "Foo Bar", "Bar", " (12H to go)", "12"] 
		// "Foo Bar" => ["Foo Bar", "Foo Bar", "Bar", undefined, undefined]
		var match = spy.match(/^([\w\d]+(\s[\w\d]+)?)(\s\((\d+)H to go\))?$/)
		if (match) {
			found = true;
			if (match[4])
				infiltrating.push(match[1] + "=" + match[4]);
		}
	}
	if (found) {
		var player = getPlayerName();
		var db = new DOMStorage("local", "BvSImprovedSpyReport." + player);
		var date = new Date();
		db.setItem("timestamp", "" + date.getTime());
		db.setItem("infiltrating", infiltrating.join(":"));
	}
}

// main
if (/billy.bvs.villagespyreport\./.test(location.href))
	improveSpyReport();
else if (/billy.bvs.village\./.test(location.href))
	scanSpyList();