DH2 Fixed

Improve Diamond Hunt 2

目前為 2017-03-08 提交的版本,檢視 最新版本

// ==UserScript==
// @name         DH2 Fixed
// @namespace    FileFace
// @description  Improve Diamond Hunt 2
// @version      0.61.2
// @author       Zorbing
// @grant        none
// @run-at       document-start
// @include      http://www.diamondhunt.co/game.php
// ==/UserScript==

(function ()
{
'use strict';

const settings = {
	hideCraftingRecipes: {
		name: 'Hide crafting recipes of finished items'
		, title: `Hides crafting recipes of:
			<ul style="margin: .5rem 0 0;">
				<li>furnace, oil storage and oven recipes if they aren't better than the current level</li>
				<li>machines if the user has the maximum amount of this type (counts bound and unbound items)</li>
				<li>non-stackable items which the user already owns (counts bound and unbound items)</li>
			</ul>`
		, defaultValue: true
	}
	, useNewChat: {
		name: 'Use the new chat'
		, title: `Enables using the completely new chat with pm tabs, clickable links, clickable usernames to send a pm, intelligent scrolling and suggesting commands while typing`
		, defaultValue: true
		, requiresReload: true
	}
};



/**
 * observer
 */

let observedKeys = new Map();
/**
 * Observes the given key for change
 * 
 * @param {string} key	The name of the variable
 * @param {Function} fn	The function which is called on change
 */
function observe(key, fn)
{
	if (key instanceof Array)
	{
		for (let k of key)
		{
			observe(k, fn);
		}
	}
	else
	{
		if (!observedKeys.has(key))
		{
			observedKeys.set(key, new Set());
		}
		observedKeys.get(key).add(fn);
	}
	return fn;
}
function unobserve(key, fn)
{
	if (key instanceof Array)
	{
		let ret = [];
		for (let k of key)
		{
			ret.push(unobserve(k, fn));
		}
		return ret;
	}
	if (!observedKeys.has(key))
	{
		return false;
	}
	return observedKeys.get(key).delete(fn);
}
function updateValue(key, newValue)
{
	if (window[key] === newValue)
	{
		return false;
	}

	const oldValue = window[key];
	window[key] = newValue;
	(observedKeys.get(key) || []).forEach(fn => fn(key, oldValue, newValue));
	return true;
}



/**
 * global constants
 */

const tierLevels = ['empty', 'sapphire', 'emerald', 'ruby', 'diamond'];
const tierNames = ['Standard', 'Sapphire', 'Emerald', 'Ruby', 'Diamond'];
const tierItemList = ['pickaxe', 'shovel', 'hammer', 'axe', 'rake', 'fishingRod'];
const furnaceLevels = ['stone', 'bronze', 'iron', 'silver', 'gold'];
const furnaceCapacity = [10, 30, 75, 150, 300];
const ovenLevels = ['bronze', 'iron', 'silver', 'gold'];
const maxOilStorageLevel = 4; // 7
const oilStorageSize = [10e3, 50e3, 100e3, 300e3];



/**
 * general functions
 */

let styleElement = null;
function addStyle(styleCode)
{
	if (styleElement === null)
	{
		styleElement = document.createElement('style');
		document.head.appendChild(styleElement);
	}
	styleElement.innerHTML += styleCode;
}
function getBoundKey(key)
{
	return 'bound' + key[0].toUpperCase() + key.substr(1);
}
function getTierKey(key, tierLevel)
{
	return tierLevels[tierLevel] + key[0].toUpperCase() + key.substr(1);
}
function formatNumber(num)
{
	return parseFloat(num).toLocaleString('en');
}
function formatNumbersInText(text)
{
	return text.replace(/\d(?:[\d',\.]*\d)?/g, (numStr) =>
	{
		return formatNumber(parseInt(numStr.replace(/\D/g, ''), 10));
	});
}
function now()
{
	return (new Date()).getTime();
}
function padLeft(num, padChar)
{
	return (num < 10 ? padChar : '') + num;
}
// use time format established in DHQoL (https://greasyfork.org/scripts/16041-dhqol)
function formatTimer(timer)
{
	timer = parseInt(timer, 10);
	const hours = Math.floor(timer / 3600);
	const minutes = Math.floor((timer % 3600) / 60);
	const seconds = timer % 60;
	return padLeft(hours, '0') + ':' + padLeft(minutes, '0') + ':' + padLeft(seconds, '0');
}
const timeSteps = [
	{
		threshold: 1
		, name: 'second'
		, short: 'sec'
		, padp: 0
	}
	, {
		threshold: 60
		, name: 'minute'
		, short: 'min'
		, padp: 0
	}
	, {
		threshold: 3600
		, name: 'hour'
		, short: 'h'
		, padp: 1
	}
	, {
		threshold: 86400
		, name: 'day'
		, short: 'd'
		, padp: 2
	}
];
function formatTime2NearestUnit(time, long = false)
{
	let step = timeSteps[0];
	for (let i = timeSteps.length-1; i > 0; i--)
	{
		if (time >= timeSteps[i].threshold)
		{
			step = timeSteps[i];
			break;
		}
	}
	const factor = Math.pow(10, step.padp);
	const num = Math.round(time / step.threshold * factor) / factor;
	const unit = long ? step.name + (num === 1 ? '' : 's') : step.short;
	return num + ' ' + unit;
}
function ensureTooltip(id, target)
{
	const tooltipId = 'tooltip-' + id;
	let tooltipEl = document.getElementById(tooltipId);
	if (!tooltipEl)
	{
		tooltipEl = document.createElement('div');
		tooltipEl.id = tooltipId;
		tooltipEl.style.display = 'none';
		document.getElementById('tooltip-list').appendChild(tooltipEl);
	}

	// ensure binded events to show the tooltip
	if (target.dataset.tooltipId == null)
	{
		target.dataset.tooltipId = tooltipId;
		// target.setAttribute('data-tooltip-id', tooltipId);
		window.$(target).bind({
			mousemove: window.changeTooltipPosition
			, mouseenter: window.showTooltip
			, mouseleave: window.hideTooltip
		});
	}
	return tooltipEl;
}



/**
 * persistent store
 */

const storePrefix = 'dh2-';
const store = {
	get: (key) =>
	{
		const value = localStorage.getItem(storePrefix + key);
		try
		{
			return JSON.parse(value);
		}
		catch (e) {}
		return value;
	}
	, has: (key) =>
	{
		return localStorage.hasOwnProperty(storePrefix + key);
	}
	, persist: (key, value) =>
	{
		localStorage.setItem(storePrefix + key, JSON.stringify(value));
	}
	, remove: (key) =>
	{
		localStorage.removeItem(storePrefix + key);
	}
};



/**
 * settings
 */

function getSettingName(key)
{
	return 'setting.' + key;
}
const observedSettings = new Map();
function observeSetting(key, fn)
{
	if (!observedSettings.has(key))
	{
		observedSettings.set(key, new Set());
	}
	observedSettings.get(key).add(fn);
}
function unobserveSetting(key, fn)
{
	if (!observedKeys.has(key))
	{
		return false;
	}
	return observedKeys.get(key).delete(fn);
}
function getSetting(key)
{
	if (!settings.hasOwnProperty(key))
	{
		return;
	}
	const name = getSettingName(key);
	return store.has(name) ? store.get(name) : settings[key].defaultValue;
}
function setSetting(key, newValue)
{
	if (!settings.hasOwnProperty(key))
	{
		return;
	}
	const oldValue = getSetting(key);
	store.persist(getSettingName(key), newValue);
	if (oldValue !== newValue && observedSettings.has(key))
	{
		observedSettings.get(key).forEach(fn => fn(key, oldValue, newValue));
	}
}
function initSettings()
{
	const settingsTableId = 'd2h-settings';
	const settingIdPrefix = 'dh2-setting-';
	addStyle(`
table.table-style1 tr:not([onclick])
{
	cursor: initial;
}
#tab-container-profile h2.section-title
{
	color: orange;
	line-height: 1.2rem;
	margin-top: 2rem;
}
#tab-container-profile h2.section-title > span.note
{
	font-size: 0.9rem;
}
#${settingsTableId} tr.reload td:first-child::after
{
	content: '*';
	font-weight: bold;
	margin-left: 3px;
}
	`);

	function insertAfter(newChild, oldChild)
	{
		const parent = oldChild.parentElement;
		if (oldChild.nextElementSibling == null)
		{
			parent.appendChild(newChild);
		}
		else
		{
			parent.insertBefore(newChild, oldChild.nextElementSibling);
		}
	}
	function getCheckImageSrc(value)
	{
		return 'images/icons/' + (value ? 'check' : 'x') + '.png';
	}

	const profileTable = document.getElementById('profile-toggleTable');

	const settingsHeader = document.createElement('h2');
	settingsHeader.className = 'section-title';
	settingsHeader.innerHTML = `Userscript "DH2 Fixed"<br>
		<span class="note">(* changes require reloading the tab)</span>`;

	insertAfter(settingsHeader, profileTable);

	const settingsTable = document.createElement('table');
	settingsTable.id = settingsTableId;
	settingsTable.className = 'table-style1';
	settingsTable.width = '40%';
	settingsTable.innerHTML = `
	<tr style="background-color:grey;">
		<th>Setting</th>
		<th>Enabled</th>
	</tr>
	`;
	for (let key in settings)
	{
		const setting = settings[key];
		const settingId = settingIdPrefix + key;

		const row = settingsTable.insertRow(-1);
		row.classList.add('setting');
		if (setting.requiresReload)
		{
			row.classList.add('reload');
		}
		row.setAttribute('onclick', '');
		row.innerHTML = `
		<td>${setting.name}</td>
		<td><img src="${getCheckImageSrc(getSetting(key))}" id="${settingId}" class="image-icon-20"></td>
		`;

		const tooltipEl = ensureTooltip(settingId, row);
		tooltipEl.innerHTML = setting.title;
		if (setting.requiresReload)
		{
			tooltipEl.innerHTML += `<span style="color: hsla(20, 100%, 50%, 1); font-size: .9rem; display: block; margin-top: 0.5rem;">You have to reload the browser tab to apply changed settings.</span>`;
		}

		row.addEventListener('click', () =>
		{
			const newValue = !getSetting(key);
			setSetting(key, newValue);
			document.getElementById(settingId).src = getCheckImageSrc(newValue);
		});
	}
	insertAfter(settingsTable, settingsHeader);
}



/**
 * hide crafting recipes of lower tiers or of maxed machines
 */

function setRecipeVisibility(key, visible)
{
	const recipeRow = document.getElementById('crafting-' + key);
	if (recipeRow)
	{
		recipeRow.style.display = (!getSetting('hideCraftingRecipes') || visible) ? '' : 'none';
	}
}
function hideLeveledRecipes(max, getKey, init)
{
	const keys2Observe = [];
	let maxLevel = 0;
	for (let i = max-1; i >= 0; i--)
	{
		const level = i+1;
		const key = getKey(i);
		const boundKey = getBoundKey(key);
		keys2Observe.push(key);
		keys2Observe.push(boundKey);
		if (window[key] > 0 || window[boundKey] > 0)
		{
			maxLevel = Math.max(maxLevel, level);
		}

		setRecipeVisibility(key, level > maxLevel);
	}

	if (init)
	{
		observe(keys2Observe, () => hideLeveledRecipes(max, getKey, false));
	}
}
function hideToolRecipe(key, init)
{
	const emptyKey = getTierKey(key, 0);
	const keys2Observe = [emptyKey];
	let hasTool = window[emptyKey] > 0;
	for (let i = 0; i < tierLevels.length; i++)
	{
		const boundKey = getBoundKey(getTierKey(key, i));
		hasTool = hasTool || window[boundKey] > 0;
		keys2Observe.push(boundKey);
	}

	setRecipeVisibility(emptyKey, !hasTool);

	if (init)
	{
		observe(keys2Observe, () => hideToolRecipe(key, false));
	}
}
function hideRecipe(key, max, init)
{
	const maxValue = typeof max === 'function' ? max() : max;
	const boundKey = getBoundKey(key);
	const unbound = parseInt(window[key], 10);
	const bound = parseInt(window[boundKey], 10);

	setRecipeVisibility(key, (bound + unbound) < maxValue);

	if (init)
	{
		observe([key, boundKey], () => hideRecipe(key, max, false));
	}
}
function hideCraftedRecipes()
{
	function processRecipes(init)
	{
		// furnace
		hideLeveledRecipes(
			furnaceLevels.length
			, i => furnaceLevels[i] + 'Furnace'
			, init
		);
		// oil storage
		hideLeveledRecipes(
			7
			, i => 'oilStorage' + (i+1)
			, init
		);
		// oven recipes
		hideLeveledRecipes(
			ovenLevels.length
			, i => ovenLevels[i] + 'Oven'
			, init
		);
		// tools
		hideToolRecipe('axe', init);
		hideToolRecipe('hammer', init);
		hideToolRecipe('shovel', init);
		hideToolRecipe('pickaxe', init);
		hideToolRecipe('fishingRod', init);
		// drills
		hideRecipe('drills', 10, init);
		// crushers
		hideRecipe('crushers', 10, init);
		// oil pipe
		hideRecipe('oilPipe', 1, init);
		// boats
		hideRecipe('rowBoat', 1, init);
		hideRecipe('canoe', 1, init);

		if (init)
		{
			observeSetting('hideCraftingRecipes', () => processRecipes(false));
		}
	}
	processRecipes(true);

	const _processCraftingTab = window.processCraftingTab;
	window.processCraftingTab = () =>
	{
		const reinit = !!window.refreshLoadCraftingTable;
		_processCraftingTab();

		if (reinit)
		{
			processRecipes(false);
		}
	};
}



/**
 * improve item boxes
 */

function hideNumberInItemBox(key, setVisibility)
{
	const itemBox = document.getElementById('item-box-' + key);
	const numberElement = itemBox.lastElementChild;
	if (setVisibility)
	{
		numberElement.style.visibility = 'hidden';
	}
	else
	{
		numberElement.style.display = 'none';
	}
}
function addSpan2ItemBox(key)
{
	hideNumberInItemBox(key);

	const itemBox = document.getElementById('item-box-' + key);
	const span = document.createElement('span');
	itemBox.appendChild(span);
	return span;
}
function setOilPerSecond(span, oil)
{
	span.innerHTML = `+ ${formatNumber(oil)} L/s <img src="images/oil.png" class="image-icon-20" style="margin-top: -2px;">`;
}
function improveItemBoxes()
{
	// show capacity of furnace
	for (let i = 0; i < furnaceLevels.length; i++)
	{
		const key = furnaceLevels[i] + 'Furnace';
		const capacitySpan = addSpan2ItemBox(getBoundKey(key));
		capacitySpan.className = 'capacity';
		capacitySpan.textContent = 'Capacity: ' + formatNumber(furnaceCapacity[i]);
	}

	// show oil cap of oil storage
	for (let i = 0; i < maxOilStorageLevel; i++)
	{
		const key = 'oilStorage' + (i+1);
		const capSpan = addSpan2ItemBox(getBoundKey(key));
		capSpan.className = 'oil-cap';
		capSpan.textContent = 'Oil cap: ' + formatNumber(oilStorageSize[i]);
	}

	// show oil per second
	const handheldOilSpan = addSpan2ItemBox('handheldOilPump');
	setOilPerSecond(handheldOilSpan, 1*window.miner);
	observe('miner', () => setOilPerSecond(handheldOilSpan, 1*window.miner));
	const oilPipeSpan = addSpan2ItemBox('boundOilPipe');
	setOilPerSecond(oilPipeSpan, 50);

	// show current tier
	hideNumberInItemBox('emptyAnvil', true);
	hideNumberInItemBox('farmer', true);
	hideNumberInItemBox('planter', true);
	hideNumberInItemBox('cooksBook', true);
	hideNumberInItemBox('cooksPage', true);
	for (let tierItem of tierItemList)
	{
		for (let i = 0; i < tierLevels.length; i++)
		{
			const key = getTierKey(tierItem, i);
			const toolKey = tierItem == 'rake' ? key : getBoundKey(key);
			const tierSpan = addSpan2ItemBox(toolKey);
			tierSpan.className = 'tier';
			tierSpan.textContent = tierNames[i];
		}
	}

	// show boat progress
	const boatKeys = ['rowBoat', 'canoe'];
	const boatTimerKeys = boatKeys.map(k => k + 'Timer');
	function checkBoat(span, timerKey, init)
	{
		const isInTransit = window[timerKey] > 0;
		const otherInTransit = boatTimerKeys.some(k => k != timerKey && window[k] > 0);
		span.textContent = isInTransit ? 'In transit' : 'Ready';
		span.style.visibility = otherInTransit ? 'hidden' : '';

		if (init)
		{
			observe(boatTimerKeys, () => checkBoat(span, timerKey, false));
		}
	}
	for (let i = 0; i < boatKeys.length; i++)
	{
		const span = addSpan2ItemBox(getBoundKey(boatKeys[i]));
		checkBoat(span, boatTimerKeys[i], true);
	}
}



/**
 * fix wood cutting
 */

function fixWoodcutting()
{
	addStyle(`
img.woodcutting-tree-img
{
	border: 1px solid transparent;
}
	`);
}



/**
 * fix chat
 */

function isMuted(user)
{
	// return window.mutedPeople.some((name) => user.indexOf(name) > -1);
	return window.mutedPeople.includes(user);
}
function handleScrolling(chatbox)
{
	if (window.isAutoScrolling)
	{
		setTimeout(() => chatbox.scrollTop = chatbox.scrollHeight);
	}
}
const chatHistoryKey = 'chatHistory';
const maxChatHistoryLength = 100;
const TYPE_RELOAD = -1;
const TYPE_NORMAL = 0;
const TYPE_PM_FROM = 1;
const TYPE_PM_TO = 2;
const TYPE_SERVER_MSG = 3;
/**
 * The chunk hiding starts with at least 10 chunks.
 * So there are at least
 *	(chunkHidingMinChunks-1) * msgChunkSize + 1 = 9 * 100 + 1 = 901
 * messages before the chunk hiding mechanism starts.
 */
const chunkHidingMinChunks = 10;
const msgChunkSize = 100;
const reloadedChatData = {
	timestamp: 0
	, username: ''
	, userlevel: 0
	, icon: 0
	, tag: 0
	, type: TYPE_RELOAD
	, msg: '[...]'
};
// load chat history
let chatHistory = store.get(chatHistoryKey) || [];
// find index of last message which is not a pm
const lastNotPM = chatHistory.slice(0).reverse().find((d) =>
{
	return !isPM(d);
});
// insert a placeholder for a reloaded chat
if (lastNotPM && lastNotPM.type != TYPE_RELOAD)
{
	reloadedChatData.timestamp = (new Date()).getTime();
	chatHistory.push(reloadedChatData);
}
// for chat messages which arrive before DOMContentLoaded and can not be displayed since the DOM isn't ready
let chatInitialized = false;
function processChatData(username, icon, tag, msg, isPM)
{
	let userlevel = 0;
	let type = tag == 5 ? TYPE_SERVER_MSG : TYPE_NORMAL;
	if (isPM == 1)
	{
		const match = msg.match(/^\s*\[(.+) ([A-Za-z0-9 ]+)\]: (.+?)\s*$/) || ['', '', username, msg];
		type = match[1] == 'Sent to' ? TYPE_PM_TO : TYPE_PM_FROM;
		username = match[2];
		msg = match[3];
	}
	else if (tag != 5)
	{
		const match = msg.match(/^\s*\((\d+)\): (.+?)\s*$/);
		if (match)
		{
			userlevel = match[1];
			msg = match[2];
		}
		else
		{
			userlevel = window.getGlobalLevel();
		}
	}
	const data = {
		timestamp: now()
		, username: username
		, userlevel: userlevel
		, icon: icon
		, tag: tag
		, type: type
		, msg: msg
	};
	return data;
}
function add2ChatHistory(data)
{
	chatHistory.push(data);
	chatHistory = chatHistory.slice(-maxChatHistoryLength);
	store.persist(chatHistoryKey, chatHistory);
}
const chatBoxId = 'div-chat';
const generalChatTabId = 'tab-chat-general';
const generalChatDivId = 'div-chat-area';
const pmChatTabPrefix = 'tab-chat-pm-';
const pmChatDivPrefix = 'div-chat-pm-';
const chatInputId = 'chat-input-text';
function getChatTab(username)
{
	const id = username == '' ? generalChatTabId : pmChatTabPrefix + username.replace(/ /g, '_');
	let tab = document.getElementById(id);
	if (!tab)
	{
		tab = document.createElement('div');
		tab.className = 'chat-tab';
		tab.id = id;
		tab.dataset.username = username;
		tab.dataset.new = 0;
		tab.textContent = username;
		// thanks /u/Spino-Prime for pointing out this was missing
		const closeSpan = document.createElement('span');
		closeSpan.className = 'close';
		tab.appendChild(closeSpan);

		const chatTabs = document.getElementById('chat-tabs');
		const filler = chatTabs.querySelector('.filler');
		if (filler)
		{
			chatTabs.insertBefore(tab, filler);
		}
		else
		{
			chatTabs.appendChild(tab);
		}
	}
	return tab;
}
function getChatDiv(username)
{
	const id = username == '' ? generalChatDivId : pmChatDivPrefix + username.replace(/ /g, '_');
	let div = document.getElementById(id);
	if (!div)
	{
		div = document.createElement('div');
		div.setAttribute('disabled', 'disabled');
		div.id = id;
		div.className = 'div-chat-area';

		const height = document.getElementById(generalChatDivId).style.height;
		div.style.height = height;

		const generalChat = document.getElementById(generalChatDivId);
		generalChat.parentNode.insertBefore(div, generalChat);
	}
	return div;
}
function changeChatTab(oldTab, newTab)
{
	oldTab.classList.remove('selected');
	newTab.classList.add('selected');
	newTab.dataset.new = 0;

	const oldChatDiv = getChatDiv(oldTab.dataset.username);
	oldChatDiv.classList.remove('selected');
	const newChatDiv = getChatDiv(newTab.dataset.username);
	newChatDiv.classList.add('selected');

	const toUsername = newTab.dataset.username;
	const newTextPlaceholder = toUsername == '' ? window.username + ':' : 'PM to ' + toUsername + ':';
	document.getElementById(chatInputId).placeholder = newTextPlaceholder;

	if (window.isAutoScrolling)
	{
		setTimeout(() => newChatDiv.scrollTop = newChatDiv.scrollHeight);
	}
}
function closeChatTab(username)
{
	// TODO: maybe delete pms stored for that user?
	const oldTab = document.querySelector('#chat-tabs .chat-tab.selected');
	const tab2Close = getChatTab(username);
	if (oldTab.dataset.username == username)
	{
		const generalTab = getChatTab('');
		changeChatTab(tab2Close, generalTab);
	}
	tab2Close.parentElement.removeChild(tab2Close);
}
const chatIcons = [
	null
	, { key: 'halloween2015',	title: 'Halloween 2015' }
	, { key: 'christmas2015',	title: 'Chirstmas 2015' }
	, { key: 'easter2016',		title: 'Holiday' }
	, { key: 'halloween2016',	title: 'Halloween 2016' }
	, { key: 'christmas2016',	title: 'Chirstmas 2016' }
	, { key: 'dh1Max',			title: 'Max Level in DH1' }
	, { key: 'hardcore',		title: 'Hardcore Account' }
	, { key: 'quest',			title: 'Questmaster' }
];
const chatTags = [
	null
	, { key: 'donor', name: '' }
	, { key: 'contributor', name: 'Contributor' }
	, { key: 'mod', name: 'Moderator' }
	, { key: 'dev', name: 'Dev' }
	, { key: 'yell', name: 'Server Message' }
];
function isPM(data)
{
	return data.type == TYPE_PM_TO || data.type == TYPE_PM_FROM;
}
const locale = 'en-US';
const localeOptions = {
	hour12: false
	, year: 'numeric'
	, month: 'long'
	, day: 'numeric'
	, hour: '2-digit'
	, minute: '2-digit'
	, second: '2-digit'
};
const msgChunkMap = new Map();
let chatboxFragments = new Map();
function createMessageSegment(data)
{
	const isThisPm = isPM(data);
	const msgUsername = data.type == TYPE_PM_TO ? window.username : data.username;
	const historyIndex = chatHistory.indexOf(data);
	let isSameUser = null;
	let isSameTime = null;
	for (let i = historyIndex-1; i >= 0 && (isSameUser === null || isSameTime === null); i--)
	{
		const dataBefore = chatHistory[i];
		if (isThisPm && isPM(dataBefore) ||
			!isThisPm && !isPM(dataBefore))
		{
			if (isSameUser === null)
			{
				const beforeUsername = dataBefore.type == TYPE_PM_TO ? window.username : dataBefore.username;
				isSameUser = beforeUsername === msgUsername;
			}
			if (dataBefore.type != TYPE_RELOAD)
			{
				isSameTime = Math.floor(data.timestamp / 1000/60) - Math.floor(dataBefore.timestamp / 1000/60) === 0;
			}
		}
	}

	const d = new Date(data.timestamp);
	const hour = (d.getHours() < 10 ? '0' : '') +  d.getHours();
	const minute = (d.getMinutes() < 10 ? '0' : '') +  d.getMinutes();
	const icon = chatIcons[data.icon] || { key: '', title: '' };
	const tag = chatTags[data.tag] || { key: '', name: '' };
	// thanks aguyd (https://greasyfork.org/forum/profile/aguyd) for the vulnerability warning
	const formattedMsg = data.msg.replace(/(https?:\/\/[^\s"<>]+)/g, '<a target="_blank" href="$1">$1</a>');

	const msgTitle = data.type == TYPE_RELOAD ? 'Chat loaded on ' + d.toLocaleString(locale, localeOptions) : '';
	const user = data.type === TYPE_SERVER_MSG ? 'Server Message' : msgUsername;
	const levelAppendix = data.type == TYPE_NORMAL ? ' (' + data.userlevel + ')' : '';
	const userTitle = data.tag != 5 ? tag.name : '';
	return `<span class="chat-msg" data-type="${data.type}" data-tag="${tag.key}">`
		+ `<span
			class="timestamp"
			data-timestamp="${data.timestamp}"
			data-same-time="${isSameTime}">${hour}:${minute}</span>`
		+ `<span class="user" data-name="${msgUsername}" data-same-user="${isSameUser}">`
			+ `<span class="icon ${icon.key}" title="${icon.title}"></span>`
			+ `<span class="name chat-tag-${tag.key}" title="${userTitle}">${user}${levelAppendix}:</span>`
		+ `</span>`
		+ `<span class="msg" title="${msgTitle}">${formattedMsg}</span>`
	+ `</span>`;
}
function add2Chat(data)
{
	if (!chatInitialized)
	{
		return;
	}

	const isThisPm = isPM(data);
	// don't mute pms (you can just ignore pm-tab if you like)
	if (!isThisPm && isMuted(data.username))
	{
		return;
	}

	const userKey = isThisPm ? data.username : '';
	const chatTab = getChatTab(userKey);
	if (!chatTab.classList.contains('selected'))
	{
		chatTab.dataset.new = parseInt(chatTab.dataset.new, 10) + 1;
	}
	if (isThisPm)
	{
		window.lastPMUser = data.username;
	}

	// username is 3-12 characters long
	const chatbox = getChatDiv(userKey);
	let msgChunk = msgChunkMap.get(userKey);
	if (!msgChunk || msgChunk.children.length >= msgChunkSize)
	{
		msgChunk = document.createElement('div');
		msgChunk.className = 'msg-chunk';
		msgChunkMap.set(userKey, msgChunk);

		if (chatboxFragments != null)
		{
			if (!chatboxFragments.has(userKey))
			{
				chatboxFragments.set(userKey, document.createDocumentFragment());
			}
			chatboxFragments.get(userKey).appendChild(msgChunk);
		}
		else
		{
			chatbox.appendChild(msgChunk);
		}
	}

	const tmp = document.createElement('templateWrapper');
	tmp.innerHTML = createMessageSegment(data);
	msgChunk.appendChild(tmp.children[0]);

	handleScrolling(chatbox);
}
function applyChatStyle()
{
	addStyle(`
span.chat-msg
{
	display: flex;
	margin-bottom: 1px;
}
span.chat-msg:nth-child(2n)
{
	background-color: hsla(0, 0%, 90%, 1);
}
.chat-msg[data-type="${TYPE_RELOAD}"]
{
	font-size: 0.8rem;
}
.chat-msg .timestamp
{
	display: none;
}
.chat-msg:not([data-type="${TYPE_RELOAD}"]) .timestamp
{
	color: hsla(0, 0%, 50%, 1);
	display: inline-block;
	font-size: .9rem;
	margin: 0;
	margin-right: 5px;
	position: relative;
	width: 2.5rem;
}
.chat-msg .timestamp[data-same-time="true"]
{
	color: hsla(0, 0%, 50%, .1);
}
.chat-msg:not([data-type="${TYPE_RELOAD}"]) .timestamp:hover::after
{
	background-color: hsla(0, 0%, 12%, 1);
	border-radius: .2rem;
	content: attr(data-fulltime);
	color: hsla(0, 0%, 100%, 1);
	line-height: 1.35rem;
	padding: .4rem .8rem;
	position: absolute;
	left: 2.5rem;
	top: -0.4rem;
	text-align: center;
	white-space: nowrap;
}

.chat-msg[data-type="${TYPE_PM_FROM}"] { color: purple; }
.chat-msg[data-type="${TYPE_PM_TO}"] { color: purple; }
.chat-msg[data-type="${TYPE_SERVER_MSG}"] { color: blue; }
.chat-msg[data-tag="contributor"] { color: green; }
.chat-msg[data-tag="mod"] { color: #669999; }
.chat-msg[data-tag="dev"] { color: #666600; }
.chat-msg:not([data-type="${TYPE_RELOAD}"]) .user
{
	flex: 0 0 132px;
	margin-right: 5px;
	white-space: nowrap;
}
#${generalChatDivId} .chat-msg:not([data-type="${TYPE_RELOAD}"]) .user
{
	flex-basis: 182px;
	padding-left: 22px;
}
.chat-msg .user[data-same-user="true"]:not([data-name=""])
{
	opacity: 0;
}

.chat-msg .user .icon
{
	margin-left: -22px;
}
.chat-msg .user .icon::before
{
	background-size: 20px 20px;
	content: '';
	display: inline-block;
	margin-right: 2px;
	width: 20px;
	height: 20px;
	vertical-align: middle;
}
.chat-msg .user .icon.halloween2015::before	{ background-image: url('images/chat-icons/1.png'); }
.chat-msg .user .icon.christmas2015::before	{ background-image: url('images/chat-icons/2.png'); }
.chat-msg .user .icon.easter2016::before	{ background-image: url('images/chat-icons/3.png'); }
.chat-msg .user .icon.halloween2016::before	{ background-image: url('images/chat-icons/4.png'); }
.chat-msg .user .icon.christmas2016::before	{ background-image: url('images/chat-icons/5.png'); }
.chat-msg .user .icon.dh1Max::before		{ background-image: url('images/chat-icons/6.png'); }
.chat-msg .user .icon.hardcore::before		{ background-image: url('images/chat-icons/7.png'); }
.chat-msg .user .icon.quest::before			{ background-image: url('images/chat-icons/8.png'); }

.chat-msg .user .name
{
	color: rgba(0, 0, 0, 0.7);
	cursor: pointer;
}
.chat-msg .user .name.chat-tag-donor::before
{
	background-image: url('images/chat-icons/donor.png');
	background-size: 20px 20px;
	content: '';
	display: inline-block;
	height: 20px;
	width: 20px;
	vertical-align: middle;
}
.chat-msg .user .name.chat-tag-yell
{
	cursor: default;
}
.chat-msg .user .name.chat-tag-contributor,
.chat-msg .user .name.chat-tag-mod,
.chat-msg .user .name.chat-tag-dev,
.chat-msg .user .name.chat-tag-yell
{
	color: white;
	display: inline-block;
	font-size: 10pt;
	margin-top: -1px;
	padding-bottom: 0;
	text-align: center;
	/* 2px border, 10 padding */
	width: calc(100% - 2*1px - 2*5px);
}

.chat-msg[data-type="${TYPE_RELOAD}"] .user > *,
.chat-msg[data-type="${TYPE_PM_FROM}"] .user > .icon,
.chat-msg[data-type="${TYPE_PM_TO}"] .user > .icon
{
	display: none;
}

.chat-msg .msg
{
	min-width: 0;
	overflow: hidden;
	word-wrap: break-word;
}

#div-chat .div-chat-area
{
	width: 100%;
	height: 130px;
	display: none;
}
#div-chat .div-chat-area.selected
{
	display: block;
}
#chat-tabs
{
	display: flex;
	margin: 10px -6px -6px;
	flex-wrap: wrap;
}
#chat-tabs .chat-tab
{
	background-color: gray;
	border-top: 1px solid black;
	border-right: 1px solid black;
	cursor: pointer;
	display: inline-block;
	font-weight: normal;
	padding: 0.3rem .6rem;
	position: relative;
}
#chat-tabs .chat-tab.selected
{
	background-color: transparent;
	border-top-color: transparent;
}
#chat-tabs .chat-tab.filler
{
	background-color: hsla(0, 0%, 90%, 1);
	border-right: 0;
	box-shadow: inset 5px 5px 5px -5px rgba(0, 0, 0, 0.5);
	color: transparent;
	cursor: default;
	flex-grow: 1;
}
#chat-tabs .chat-tab::after
{
	color: white;
	content: '(' attr(data-new) ')';
	font-size: .9rem;
	font-weight: bold;
	margin-left: .4rem;
}
#chat-tabs .chat-tab[data-new="0"]::after
{
	color: inherit;
	font-weight: normal;
}
#chat-tabs .chat-tab:not(.general).selected::after,
#chat-tabs .chat-tab:not(.general):hover::after
{
	visibility: hidden;
}
#chat-tabs .chat-tab:not(.general).selected .close::after,
#chat-tabs .chat-tab:not(.general):hover .close::after
{
	content: '\xd7';
	font-size: 1.5rem;
	position: absolute;
	top: 0;
	right: .6rem;
	bottom: 0;
}
	`);
}
function addIntelligentScrolling()
{
	// add checkbox instead of button for toggling auto scrolling
	const btn = document.querySelector('input[value="Toggle Autoscroll"]');
	const checkboxId = 'chat-toggle-autoscroll';
	// create checkbox
	const toggleCheckbox = document.createElement('input');
	toggleCheckbox.type = 'checkbox';
	toggleCheckbox.id = checkboxId;
	toggleCheckbox.checked = true;
	// create label
	const toggleLabel = document.createElement('label');
	toggleLabel.htmlFor = checkboxId;
	toggleLabel.textContent = 'Autoscroll';
	btn.parentNode.insertBefore(toggleCheckbox, btn);
	btn.parentNode.insertBefore(toggleLabel, btn);
	btn.style.display = 'none';

	// add checkbox for intelligent scrolling
	const isCheckboxId = 'chat-toggle-intelligent-scroll';
	const intScrollCheckbox = document.createElement('input');
	intScrollCheckbox.type = 'checkbox';
	intScrollCheckbox.id = isCheckboxId;
	intScrollCheckbox.checked = true;
	// add label
	const intScrollLabel = document.createElement('label');
	intScrollLabel.htmlFor = isCheckboxId;
	intScrollLabel.textContent = 'Intelligent Scrolling';
	btn.parentNode.appendChild(intScrollCheckbox);
	btn.parentNode.appendChild(intScrollLabel);

	const chatArea = document.getElementById(generalChatDivId);
	let showScrollTextTimeout = null;
	function setAutoScrolling(value, full)
	{
		if (window.isAutoScrolling != value)
		{
			toggleCheckbox.checked = value;
			window.isAutoScrolling = value;
			const color = value ? 'lime' : 'red';
			const text = (value ? 'En' : 'Dis') + 'abled' + (full ? ' Autoscroll' : '');
			const scrollArgs = ['none', color, text];
			if (full)
			{
				window.clearTimeout(showScrollTextTimeout);
				showScrollTextTimeout = window.setTimeout(() => window.scrollText(...scrollArgs), 300);
			}
			else
			{
				window.scrollText(...scrollArgs);
			}
			return true;
		}
		return false;
	}
	toggleCheckbox.addEventListener('change', function ()
	{
		setAutoScrolling(this.checked);
		if (this.checked && intScrollCheckbox.checked)
		{
			chatArea.scrollTop = chatArea.scrollHeight - chatArea.clientHeight;
		}
	});

	const placeholderTemplate = document.createElement('div');
	placeholderTemplate.className = 'placeholder';
	const childStore = new WeakMap();
	function scrollHugeChat()
	{
		// # of children
		const chunkNum = chatArea.children.length;
		// start chunk hiding at a specific amount of chunks
		if (chunkNum < chunkHidingMinChunks)
		{
			return;
		}

		const visibleTop = chatArea.scrollTop;
		const visibleBottom = visibleTop + chatArea.clientHeight;
		const referenceTop = visibleTop - window.innerHeight;
		const referenceBottom = visibleBottom + window.innerHeight;
		let top = 0;
		// never hide the last element since its size may change at any time when a new message gets appended
		for (let i = 0; i < chunkNum-1; i++)
		{
			const child = chatArea.children[i];
			const height = child.clientHeight;
			const bottom = top + height;
			const isVisible = top >= referenceTop && top <= referenceBottom
				|| bottom >= referenceTop && bottom <= referenceBottom
				|| top < referenceTop && bottom > referenceBottom
			;
			const isPlaceholder = child.classList.contains('placeholder');
			if (!isVisible && !isPlaceholder)
			{
				const newPlaceholder = placeholderTemplate.cloneNode(false);
				newPlaceholder.style.height = height + 'px';
				chatArea.replaceChild(newPlaceholder, child);
				childStore.set(newPlaceholder, child);
			}
			else if (isVisible && isPlaceholder)
			{
				const oldChild = childStore.get(child);
				chatArea.replaceChild(oldChild, child);
				childStore.delete(child);
			}
			top = bottom;
		}
	}
	let timeouts = {};
	const timeoutDelay = 50;
	const maxDelay = 300;
	function startCancelableTimeout(key, handler)
	{
		let obj = timeouts[key] || {};
		const n = now();
		if (obj.start == null)
		{
			obj.start = n;
		}
		if (obj.start + maxDelay > n)
		{
			window.clearTimeout(obj.ref);
			obj.ref = window.setTimeout(() =>
			{
				obj.start = null;
				obj.ref = null;
				handler();
			}, timeoutDelay);
		}
		timeouts[key] = obj;
	}
	// does not consider pm tabs; may be changed in a future version?
	chatArea.addEventListener('scroll', () =>
	{
		if (intScrollCheckbox.checked)
		{
			const scrolled2Bottom = (chatArea.scrollTop + chatArea.clientHeight) >= chatArea.scrollHeight;
			setAutoScrolling(scrolled2Bottom, true);
		}

		startCancelableTimeout('scrollHugeChat', () => scrollHugeChat());
	});
}
function clickChatTab(newTab)
{
	const oldTab = document.querySelector('#chat-tabs .chat-tab.selected');
	if (newTab == oldTab)
	{
		return;
	}

	changeChatTab(oldTab, newTab);
}
function clickCloseChatTab(tab)
{
	const username = tab.dataset.username;
	const chatDiv = getChatDiv(username);
	if (chatDiv.children.length === 0 ||
		confirm(`Do you want to close the pm tab of "${username}"?`))
	{
		closeChatTab(username);
	}
}
function addChatTabs()
{
	const chatBoxArea = document.getElementById(chatBoxId);
	const chatTabs = document.createElement('div');
	chatTabs.id = 'chat-tabs';
	chatTabs.addEventListener('click', (event) =>
	{
		const newTab = event.target;
		if (newTab.classList.contains('close'))
		{
			return clickCloseChatTab(newTab.parentElement);
		}
		if (!newTab.classList.contains('chat-tab') || newTab.classList.contains('filler'))
		{
			return;
		}

		clickChatTab(newTab);
	});
	chatBoxArea.appendChild(chatTabs);

	const generalTab = getChatTab('');
	generalTab.classList.add('general');
	generalTab.classList.add('selected');
	generalTab.textContent = 'Server';
	const generalChatDiv = getChatDiv('');
	generalChatDiv.classList.add('selected');
	// works only if username length of 1 isn't allowed
	const fillerTab = getChatTab('f');
	fillerTab.classList.add('filler');
	fillerTab.textContent = '';

	const _sendChat = window.sendChat;
	window.sendChat = (inputEl) =>
	{
		let msg = inputEl.value;
		const selectedTab = document.querySelector('.chat-tab.selected');
		if (selectedTab.dataset.username != '' && msg[0] != '/')
		{
			inputEl.value = '/pm ' + selectedTab.dataset.username + ' ' + msg;
		}
		_sendChat(inputEl);
	};
}
function newAddToChatBox(username, icon, tag, msg, isPM)
{
	const data = processChatData(username, icon, tag, msg, isPM);
	add2ChatHistory(data);
	if (getSetting('useNewChat'))
	{
		add2Chat(data);
	}
	else
	{
		window.addToChatBox(username, icon, tag, msg, isPM);
	}
}
function newChat()
{
	addChatTabs();
	applyChatStyle();

	window.addToChatBox = newAddToChatBox;
	chatInitialized = true;

	const chatbox = document.getElementById(chatBoxId);
	chatbox.addEventListener('click', (event) =>
	{
		let target = event.target;
		while (target && target.id != chatBoxId && !target.classList.contains('user'))
		{
			target = target.parentElement;
		}
		if (!target || target.id == chatBoxId)
		{
			return;
		}

		const username = target.dataset.name;
		if (username == window.username || username == '')
		{
			return;
		}

		const userTab = getChatTab(username);
		clickChatTab(userTab);
		document.getElementById(chatInputId).focus();
	});
	chatbox.addEventListener('mouseover', (event) =>
	{
		const target = event.target;
		if (!target.classList.contains('timestamp') || !target.dataset.timestamp)
		{
			return;
		}

		const timestamp = parseInt(target.dataset.timestamp, 10);
		target.dataset.fulltime = (new Date(timestamp)).toLocaleDateString(locale, localeOptions);
		target.dataset.timestamp = '';
	});
}
const commands = ['pm', 'mute', 'ipmute'];
function addCommandSuggester()
{
	const input = document.getElementById(chatInputId);
	input.addEventListener('keyup', (event) =>
	{
		if (event.key != 'Backspace' && event.key != 'Delete' &&
			input.selectionStart == input.selectionEnd &&
			input.selectionStart == input.value.length &&
			input.value.startsWith('/'))
		{
			const value = input.value.substr(1);
			const suggestions = commands.filter(c => c.startsWith(value));
			if (suggestions.length == 1)
			{
				input.value = '/' + suggestions[0];
				input.selectionStart = 1 + value.length;
				input.selectionEnd = input.value.length;
			}
		}
	});
}
const tutorialCmd = 'tutorial';
function addOwnCommands()
{
	commands.push(tutorialCmd);

	const _doChatCommand = window.doChatCommand;
	window.doChatCommand = (value) =>
	{
		// thanks aguyd (https://greasyfork.org/forum/profile/aguyd) for the idea
		if (value.startsWith('/'))
		{
			const rest = value.substr(1);
			if (rest.startsWith(tutorialCmd))
			{
				const name = rest.substr(tutorialCmd.length).trim();
				let msg = 'https://www.reddit.com/r/DiamondHunt/comments/5vrufh/diamond_hunt_2_starter_faq/';
				if (name.length != 0)
				{
					// maybe add '@' before the name?
					msg = name + ', ' + msg;
				}
				window.sendBytes('CHAT=' + msg);
				return true;
			}
		}
		return _doChatCommand(value);
	};
}
function initChat()
{
	if (!getSetting('useNewChat'))
	{
		return;
	}

	newChat();
	addIntelligentScrolling();
	addCommandSuggester();
	addOwnCommands();

	const _enlargeChat = window.enlargeChat;
	const chatBoxArea = document.getElementById(chatBoxId);
	function setChatBoxHeight(height)
	{
		document.getElementById(generalChatDivId).style.height = height;
		const chatDivs = chatBoxArea.querySelectorAll('div[id^="' + pmChatDivPrefix + '"]');
		for (let i = 0; i < chatDivs.length; i++)
		{
			chatDivs[i].style.height = height;
		}
	}
	window.enlargeChat = (enlargeB) =>
	{
		_enlargeChat(enlargeB);

		const height = document.getElementById(generalChatDivId).style.height;
		store.persist('chat.height', height);
		setChatBoxHeight(height);
	};
	setChatBoxHeight(store.get('chat.height'));

	// TEMP >>> (due to a naming issue, migrate the data)
	const oldChatHistoryKey = 'chatHistory2';
	const oldChatHistory = store.get(oldChatHistoryKey);
	if (oldChatHistory != null)
	{
		store.persist(chatHistoryKey, oldChatHistory);
		store.remove(oldChatHistoryKey);
	}
	// TEMP <<<

	// add history to chat
	chatHistory.forEach(d => add2Chat(d));
	chatboxFragments.forEach((fragment, key) =>
	{
		const chatbox = getChatDiv(key);
		chatbox.appendChild(fragment);
	});
	chatboxFragments = null;
	// reset the new counter for all tabs
	const tabs = document.querySelectorAll('.chat-tab');
	for (let i = 0; i < tabs.length; i++)
	{
		tabs[i].dataset.new = 0;
	}
}



/**
 * hopefully only temporary fixes
 */

function temporaryFixes()
{
	// fix grow time of some seeds
	const seeds = {
		'limeLeafSeeds': {
			replace: '1 hour'
			, replaceWith: '1 hour and 30 minutes'
		}
	};
	for (let seedName in seeds)
	{
		const tooltip = document.getElementById('tooltip-' + seedName);
		const timeNode = tooltip.lastElementChild.lastChild;
		const seed = seeds[seedName];
		timeNode.textContent = timeNode.textContent.replace(seed.replace, seed.replaceWith);
	}

	// fix exhaustion timer and updating brewing and cooking recipes
	const _clientGameLoop = window.clientGameLoop;
	window.clientGameLoop = () =>
	{
		_clientGameLoop();
		setHeroClickable();
		if (window.isInCombat() && combatCommenceTimer != 0)
		{
			document.getElementById('combat-countdown').style.display = '';
		}
		if (document.getElementById('tab-container-combat').style.display != 'none')
		{
			window.combatNotFightingTick();
		}
		if (currentOpenTab == 'brewing')
		{
			window.processBrewingTab();
		}
		if (currentOpenTab == 'cooksBook')
		{
			window.processCooksBookTab();
		}
	};

	// fix elements of scrollText (e.g. when joining the game and receiving xp at that moment)
	const textEls = document.querySelectorAll('div.scroller');
	for (let i = 0; i < textEls.length; i++)
	{
		const scroller = textEls[i];
		if (scroller.style.position != 'absolute')
		{
			scroller.style.display = 'none';
		}
	}

	// fix style of tooltips
	addStyle(`
body > div.tooltip > h2:first-child
{
	margin-top: 0;
	font-size: 20pt;
	font-weight: normal;
}
	`);

	// fix buiulding magic table dynamically
	window.refreshLoadMagicTable = true;
	const _processMagicTab = window.processMagicTab;
	window.processMagicTab = () =>
	{
		const _refreshLoadCraftingTable = window.refreshLoadCraftingTable;
		window.refreshLoadCraftingTable = window.refreshLoadMagicTable;
		_processMagicTab();
		window.refreshLoadCraftingTable = _refreshLoadCraftingTable;
	};

	// update hero being clickable in combat
	function setHeroClickable()
	{
		const heroArea = document.getElementById('hero-area');
		const equipment = heroArea.lastElementChild;
		equipment.style.pointerEvents = window.isInCombat() ? 'none' : '';
	}

	// fix crafting level of giant drills
	const _processCraftingTab = window.processCraftingTab;
	window.processCraftingTab = () =>
	{
		const reinit = !!window.refreshLoadCraftingTable;
		_processCraftingTab();

		if (reinit)
		{
			craftingRecipes.giantDrills.levelReq = 35;
			document.getElementById('recipe-level-req-giantDrills').textContent = '35';
		}
	};
}



/**
 * improve timer
 */

function improveTimer()
{
	window.formatTime = (seconds) =>
	{
		return formatTimer(seconds);
	};
	window.formatTimeShort2 = (seconds) =>
	{
		return formatTimer(seconds);
	};

	addStyle(`
#notif-smelting > span:not(.timer)
{
	display: none;
}
	`);
	const smeltingNotifBox = document.getElementById('notif-smelting');
	const smeltingTimerEl = document.createElement('span');
	smeltingTimerEl.className = 'timer';
	smeltingNotifBox.appendChild(smeltingTimerEl);
	function updateSmeltingTimer()
	{
		const totalTime = parseInt(window.smeltingPercD, 10);
		const elapsedTime = parseInt(window.smeltingPercN, 10);
		smeltingTimerEl.textContent = formatTimer(Math.max(totalTime - elapsedTime, 0));
	}
	observe('smeltingPercD', () => updateSmeltingTimer());
	observe('smeltingPercN', () => updateSmeltingTimer());
	updateSmeltingTimer();

	// add tree grow timer
	addStyle(`
/* hide timer elements of DH2QoL, because I can :P */
.woodcutting-tree > span,
.woodcutting-tree > br
{
	display: none;
}
.woodcutting-tree > div.timer
{
	color: white;
	margin-top: 5px;
	pointer-events: none;
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
}
	`);
	const treeInfo = {
		1: {
			name: 'Normal tree'
			// 3h = 10800s
			, growTime: 3 * 60 * 60
		}
		, 2: {
			name: 'Oak tree'
			// 6h = 21600s
			, growTime: 6 * 60 * 60
		}
		, 3: {
			name: 'Willow tree'
			 // 8h = 28800s
			, growTime: 8 * 60 * 60
		}
		, 4: {
			name: 'Maple tree'
			// 12h = 43200s
			, growTime: 12 * 60 * 60
		}
	};
	function updateTreeInfo(place, infoElId, init)
	{
		const infoEl = document.getElementById(infoElId);
		const nameEl = infoEl.firstElementChild;
		const timerEl = infoEl.lastElementChild;
		const idKey = 'treeId' + place;
		const growTimerKey = 'treeGrowTimer' + place;
		const lockedKey = 'treeUnlocked' + place;

		const info = treeInfo[window[idKey]];
		if (!info)
		{
			const isLocked = place > 4 && window[lockedKey] == 0;
			nameEl.textContent = isLocked ? 'Locked' : 'Empty';
			timerEl.textContent = '';
		}
		else
		{
			nameEl.textContent = info.name;
			const remainingTime = info.growTime - parseInt(window[growTimerKey], 10);
			timerEl.textContent = remainingTime > 0 ? '(' + formatTimer(remainingTime) + ')' : 'Fully grown';
		}

		if (init)
		{
			observe(
				[idKey, growTimerKey, lockedKey]
				, () => updateTreeInfo(place, infoElId, false)
			);
		}
	}
	for (let i = 0; i < 6; i++)
	{
		const treePlace = i+1;
		const infoElId = 'wc-tree-timer-' + treePlace;
		const treeContainer = document.getElementById('wc-div-tree-' + treePlace);
		treeContainer.style.position = 'relative';
		const infoEl = document.createElement('div');
		infoEl.className = 'timer';
		infoEl.id = infoElId;
		const treeName = document.createElement('div');
		treeName.style.fontSize = '1.2rem';
		infoEl.appendChild(treeName);
		const treeTimer = document.createElement('div');
		infoEl.appendChild(treeTimer);
		treeContainer.appendChild(infoEl);

		updateTreeInfo(treePlace, infoElId, true);
	}

	// fix tooltip of whale/rainbowfish
	const tooltipTemplate = document.getElementById('tooltip-rawShark');
	function createRawFishTooltip(id, name)
	{
		const newTooltip = tooltipTemplate.cloneNode(true);
		newTooltip.id = 'tooltip-' + id;
		newTooltip.firstChild.textContent = name;
		newTooltip.lastChild.firstChild.textContent = '+? ';
		tooltipTemplate.parentElement.appendChild(newTooltip);
	}
	createRawFishTooltip('rawWhale', 'Raw Whale');
	createRawFishTooltip('rawRainbowFish', 'Raw Rainbowfish');
}



/**
 * improve smelting dialog
 */

const smeltingRequirements = {
	'glass': {
		sand: 1
		, oil: 10
	}
	, 'bronzeBar': {
		copper: 1
		, tin: 1
		, oil: 10
	}
	, 'ironBar': {
		iron: 1
		, oil: 100
	}
	, 'silverBar': {
		silver: 1
		, oil: 300
	}
	, 'goldBar': {
		gold: 1
		, oil: 1e3
	}
};
function improveSmelting()
{
	const amountInput = document.getElementById('input-smelt-bars-amount');
	amountInput.type = 'number';
	amountInput.min = 0;
	amountInput.step = 5;
	function onValueChange(event)
	{
		smeltingValue = null;
		window.selectBar('', '', amountInput, document.getElementById('smelting-furnace-capacity').value);
	}
	amountInput.addEventListener('mouseup', onValueChange);
	amountInput.addEventListener('keyup', onValueChange);
	amountInput.setAttribute('onkeyup', '');

	const _selectBar = window.selectBar;
	let smeltingValue = null;
	window.selectBar = (bar, inputElement, inputBarsAmountEl, capacity) =>
	{
		const requirements = smeltingRequirements[bar];
		let maxAmount = capacity;
		for (let key in requirements)
		{
			maxAmount = Math.min(Math.floor(window[key] / requirements[key]), maxAmount);
		}
		const value = parseInt(amountInput.value, 10);
		if (value > maxAmount)
		{
			smeltingValue = value;
			amountInput.value = maxAmount;
		}
		else if (smeltingValue != null)
		{
			amountInput.value = Math.min(smeltingValue, maxAmount);
			if (smeltingValue <= maxAmount)
			{
				smeltingValue = null;
			}
		}
		return _selectBar(bar, inputElement, inputBarsAmountEl, capacity);
	};

	const _openFurnaceDialogue = window.openFurnaceDialogue;
	window.openFurnaceDialogue = (furnace) =>
	{
		if (smeltingBarType == 0)
		{
			amountInput.max = getFurnaceCapacity(furnace);
		}
		return _openFurnaceDialogue(furnace);
	};
}



/**
 * add chance to time calculator
 */

/**
 * calculates the number of seconds until the event with the given chance happened at least once with the given
 * probability p (in percent)
 */
function calcSecondsTillP(chancePerSecond, p)
{
	return Math.round(Math.log(1 - p/100) / Math.log(1 - chancePerSecond));
}
function addChanceTooltip(headline, chancePerSecond, elId, targetEl)
{
	// ensure tooltip exists and is correctly binded
	const tooltipEl = ensureTooltip('chance-' + elId, targetEl);

	// set elements content
	const percValues = [1, 10, 20, 50, 80, 90, 99];
	let percRows = '';
	for (let p of percValues)
	{
		percRows += `
			<tr>
				<td>${p}%</td>
				<td>${formatTime2NearestUnit(calcSecondsTillP(chancePerSecond, p), true)}</td>
			</tr>`;
	}
	tooltipEl.innerHTML = `<h2>${headline}</h2>
		<table class="chance">
			<tr>
				<th>Probability</th>
				<th>Time</th>
			</tr>
			${percRows}
		</table>
	`;
}
function chance2TimeCalculator()
{
	addStyle(`
table.chance
{
	border-spacing: 0;
}
table.chance th
{
	border-bottom: 1px solid gray;
}
table.chance td:first-child
{
	border-right: 1px solid gray;
	text-align: center;
}
table.chance th,
table.chance td
{
	padding: 4px 8px;
}
table.chance tr:nth-child(2n) td
{
	background-color: white;
}
	`);

	const _clicksShovel = window.clicksShovel;
	window.clicksShovel = () =>
	{
		_clicksShovel();

		const shovelChance = document.getElementById('dialogue-shovel-chance');
		const titleEl = shovelChance.parentElement;
		const chance = 1/window.getChanceOfDiggingSand();
		addChanceTooltip('One sand every:', chance, 'shovel', titleEl);
	};

	// depends on fishingXp
	const _clicksFishingRod = window.clicksFishingRod;
	window.clicksFishingRod = () =>
	{
		_clicksFishingRod();

		const fishList = ['shrimp', 'sardine', 'tuna', 'swordfish', 'shark'];
		for (let fish of fishList)
		{
			const rawFish = 'raw' + fish[0].toUpperCase() + fish.substr(1);
			const row = document.getElementById('dialogue-fishing-rod-tr-' + rawFish);
			const chance = row.cells[4].textContent
				.replace(/[^\d\/]/g, '')
				.split('/')
				.reduce((p, c) => p / parseInt(c, 10), 1)
			;
			addChanceTooltip(`One raw ${fish} every:`, chance, rawFish, row);
		}
	};
}



/**
 * add tooltips for recipes
 */

function updateRecipeTooltips(recipeKey, recipes)
{
	const table = document.getElementById('table-' + recipeKey + '-recipe');
	const rows = table.rows;
	for (let i = 1; i < rows.length; i++)
	{
		const row = rows[i];
		const key = row.id.replace(recipeKey + '-', '');
		const recipe = recipes[key];
		const requirementCell = row.cells[3];
		requirementCell.title = recipe.recipe
			.map((name, i) =>
			{
				return formatNumber(recipe.recipeCost[i]) + ' '
					+ name.replace(/[A-Z]/g, (match) => ' ' + match.toLowerCase())
				;
			})
			.join(' + ')
		;
		window.$(requirementCell).tooltip();
	}
}
function updateTooltipsOnReinitRecipes(key)
{
	const capitalKey = key[0].toUpperCase() + key.substr(1);
	const processKey = 'process' + capitalKey + 'Tab';
	const _processTab = window[processKey];
	window[processKey] = () =>
	{
		const reinit = !!window['refreshLoad' + capitalKey + 'Table'];
		_processTab();

		if (reinit)
		{
			updateRecipeTooltips(key, window[key + 'Recipes']);
		}
	};
}
function addRecipeTooltips()
{
	updateTooltipsOnReinitRecipes('crafting');
	updateTooltipsOnReinitRecipes('brewing');
	updateTooltipsOnReinitRecipes('magic');
	updateTooltipsOnReinitRecipes('cooksBook');
}



/**
 * fix formatting of numbers
 */

function prepareRecipeForTable(recipe)
{
	// create a copy of the recipe to prevent requirement check from failing
	const newRecipe = JSON.parse(JSON.stringify(recipe));
	newRecipe.recipeCost = recipe.recipeCost.map(cost => formatNumber(cost));
	newRecipe.xp = formatNumber(recipe.xp);
	return newRecipe;
}
function fixNumberFormat()
{
	const _addRecipeToBrewingTable = window.addRecipeToBrewingTable;
	window.addRecipeToBrewingTable = (brewingRecipe) =>
	{
		_addRecipeToBrewingTable(prepareRecipeForTable(brewingRecipe));
	};

	const _addRecipeToMagicTable = window.addRecipeToMagicTable;
	window.addRecipeToMagicTable = (magicRecipe) =>
	{
		_addRecipeToMagicTable(prepareRecipeForTable(magicRecipe));
	};
}



/**
 * style tweaks
 */

function addTweakStyle(setting, style)
{
	const prefix = 'body.' + setting;
	addStyle(
		style
			.replace(/(^\s*|,\s*|\}\s*)([^\{\},]+)(,|\s*\{)/g, '$1' + prefix + ' $2$3')
	);
	document.body.classList.add(setting);
}
function tweakStyle()
{
	// tweak oil production/consumption
	addTweakStyle('tweak-oil', `
span#oil-flow-values
{
	margin-left: .5em;
	padding-left: 2rem;
	position: relative;
}
#oil-flow-values > span:nth-child(-n+2)
{
	font-size: 0px;
	position: absolute;
	left: 0;
	top: -0.75rem;
	visibility: hidden;
}
#oil-flow-values > span:nth-child(-n+2) > span
{
	font-size: 1rem;
	visibility: visible;
}
#oil-flow-values > span:nth-child(2)
{
	top: 0.75rem;
}
#oil-flow-values span[data-item-display="oilIn"]::before
{
	content: '+';
}
#oil-flow-values span[data-item-display="oilOut"]::before
{
	content: '-';
}
	`);
	// make room for oil cell on small devices
	const oilFlowValues = document.getElementById('oil-flow-values');
	oilFlowValues.parentElement.style.width = '30%';

	addTweakStyle('no-select', `
table.tab-bar,
span.item-box,
div.farming-patch,
div.farming-patch-locked,
div#tab-sub-container-combat > span,
table.top-links a,
#hero-area > div:last-child
{
	-webkit-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
}
	`);
}



/**
 * init
 */

function init()
{
	initSettings();

	temporaryFixes();

	hideCraftedRecipes();
	improveItemBoxes();
	fixWoodcutting();
	initChat();
	improveTimer();
	improveSmelting();
	chance2TimeCalculator();
	addRecipeTooltips();

	fixNumberFormat();
	tweakStyle();
}
document.addEventListener('DOMContentLoaded', () =>
{
	const _doCommand = window.doCommand;
	window.doCommand = (data) =>
	{
		const values = data.split('=')[1];
		if (data.startsWith('REFRESH_ITEMS='))
		{
			const itemDataValues = values.split(';');
			const itemArray = [];
			for (var i = 0; i < itemDataValues.length; i++)
			{
				const [key, newValue] = itemDataValues[i].split('~');
				if (updateValue(key, newValue))
				{
					itemArray.push(key);
				}
			}

			window.refreshItemValues(itemArray, false);

			if (window.firstLoadGame)
			{
				window.loadInitial();
				window.firstLoadGame = false;
				init();
			}
			else
			{
				window.clientGameLoop();
			}
			return;
		}
		else if (data.startsWith('CHAT='))
		{
			var parts = data.substr(5).split('~');
			return newAddToChatBox(parts[0], parts[1], parts[2], parts[3], 0);
		}
		return _doCommand(data);
	};
});



/**
 * fix web socket errors
 */

function webSocketLoaded(event)
{
	if (window.webSocket == null)
	{
		console.error('no webSocket instance found!');
		return;
	}

	const messageQueue = [];
	const _onMessage = webSocket.onmessage;
	webSocket.onmessage = (event) => messageQueue.push(event);
	document.addEventListener('DOMContentLoaded', () =>
	{
		messageQueue.forEach(event => onMessage(event));
		webSocket.onmessage = _onMessage;
	});

	const commandQueue = [];
	const _sendBytes = window.sendBytes;
	window.sendBytes = (command) => commandQueue.push(command);
	const _onOpen = webSocket.onopen;
	webSocket.onopen = (event) =>
	{
		window.sendBytes = _sendBytes;
		commandQueue.forEach(command => window.sendBytes(command));
		return _onOpen(event);
	};
}
function isWebSocketScript(script)
{
	return script.src.includes('socket.js');
}
function fixWebSocketScript()
{
	if (!document.head)
	{
		return;
	}

	const scripts = document.head.querySelectorAll('script');
	let found = false;
	for (let i = 0; i < scripts.length; i++)
	{
		if (isWebSocketScript(scripts[i]))
		{
			// does this work?
			scripts[i].onload = webSocketLoaded;
			return;
		}
	}

	// create an observer instance
	const mutationObserver = new MutationObserver((mutationList) =>
	{
		mutationList.forEach((mutation) =>
		{
			if (mutation.addedNodes.length === 0)
			{
				return;
			}

			for (let i = 0; i < mutation.addedNodes.length; i++)
			{
				const node = mutation.addedNodes[i];
				if (node.tagName == 'SCRIPT' && isWebSocketScript(node))
				{
					mutationObserver.disconnect();
					node.onload = webSocketLoaded;
					return;
				}
			}
		});
	});
	mutationObserver.observe(document.head, {
		childList: true
	});
}
fixWebSocketScript();

// fix scrollText (e.g. when joining the game and receiving xp at that moment)
window.mouseX = window.innerWidth / 2;
window.mouseY = window.innerHeight / 2;
})();