DH2 Fixed

Improve Diamond Hunt 2

目前为 2017-05-29 提交的版本。查看 最新版本

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

/**
 * ISC License (ISC)
 * 
 * Copyright (c) 2017, Martin Boekhoff
 * 
 * Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
 * granted, provided that the above copyright notice and this permission notice appear in all copies.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 * 
 * Source: http://opensource.org/licenses/ISC
 */

(function ()
{
'use strict';
var version = '0.197.9';

/**
 * observer
 */
var observer;
(function (observer)
{
	observer.GAME_TICK_KEY = 'dh2.gameTick';
	var observedKeys = new Map();

	function add(key, fn)
	{
		if (key instanceof Array)
		{
			for (var _i = 0, key_1 = key; _i < key_1.length; _i++)
			{
				var k = key_1[_i];
				add(k, fn);
			}
		}
		else
		{
			if (!observedKeys.has(key))
			{
				observedKeys.set(key, new Set());
			}
			observedKeys.get(key).add(fn);
		}
		return fn;
	}
	observer.add = add;

	function notify(key, oldValue)
	{
		var newValue = getGameValue(key);
		if (observedKeys.has(key))
		{
			observedKeys.get(key).forEach(function (fn)
			{
				return fn(key, oldValue, newValue);
			});
		}
	}
	observer.notify = notify;

	function notifyTick()
	{
		notify(observer.GAME_TICK_KEY, Math.floor(now() / 1000));
	}
	observer.notifyTick = notifyTick;

	function remove(key, fn)
	{
		if (key instanceof Array)
		{
			var ret = [];
			for (var _i = 0, key_2 = key; _i < key_2.length; _i++)
			{
				var k = key_2[_i];
				ret.push(remove(k, fn));
			}
			return ret;
		}
		if (!observedKeys.has(key))
		{
			return false;
		}
		return observedKeys.get(key).delete(fn);
	}
	observer.remove = remove;

	function addTick(fn)
	{
		return add(observer.GAME_TICK_KEY, fn);
	}
	observer.addTick = addTick;
})(observer || (observer = {}));
/**
 * global constants
 */
var PLUS_MINUS_SIGN = String.fromCharCode(177);
var TIER_LEVELS = ['empty', 'sapphire', 'emerald', 'ruby', 'diamond'];
var TIER_NAMES = ['Standard', 'Sapphire', 'Emerald', 'Ruby', 'Diamond'];
var TIER_ITEMS = ['pickaxe', 'shovel', 'hammer', 'axe', 'rake', 'trowel', 'fishingRod', 'chisel'];
var ORB_ITEMS = ['pickaxe', 'shovel', 'hammer', 'axe', 'rake', 'trowel', 'fishingRod', 'chisel', 'oilPipe'];
var TIER_ITEMS_NOT_BINDABLE = ['rake', 'trowel'];
var FURNACE_LEVELS = ['stone', 'bronze', 'iron', 'silver', 'gold', 'promethium'];
var OVEN_LEVELS = ['bronze', 'iron', 'silver', 'gold', 'promethium'];
var WAND_LEVELS = ['wooden', 'oak', 'willow', 'maple', 'stardust'];
var OIL_STORAGE_SIZES = [10e3, 50e3, 100e3, 300e3, 600e3, 2e6];
var HIDE_RECIPES = {
	'drills':
	{
		max: 10
	}
	, 'crushers':
	{
		max: 10
	}
	, 'giantDrills':
	{
		max: 10
	}
	, 'excavators':
	{
		max: 10
	}
	, 'oilPipe':
	{
		max: 1
	}
	, 'pumpjacks':
	{
		max: 10
	}
	, 'rowBoat':
	{
		max: 1
	}
	, 'canoe':
	{
		max: 1
	}
	// thanks aguyd
	, 'bonemealBin':
	{
		extraKeys: ['boundFilledBonemealBin']
		, max: 1
	}
	, 'oilFactory':
	{
		max: 1
	}
	, 'brewingKit':
	{
		max: 1
	}
	, 'rocket':
	{
		max: 1
	}
};
var SMELTING_REQUIREMENTS = {
	'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
	}
	, 'promethiumBar':
	{
		promethium: 1
		, charcoal: 1
	}
};
var PLANT_NAME = {
	'1': 'Dark Mushrooms'
	, '2': 'Red Mushrooms'
	, '3': 'Dotted Green Leafs'
	, '4': 'Green Leafs'
	, '5': 'Lime Leafs'
	, '6': 'Gold Leafs'
	, '7': 'Striped Gold Leafs'
	, '8': 'Crystal Leafs'
	, '9': 'Striped Crystal Leafs'
	, '10': 'Blewit Mushrooms'
	, '11': 'Snapegrass'
	, '12': 'Tree'
	, '13': 'Oak Tree'
	, '14': 'Wheat'
	, '15': 'Willow Tree'
	, '16': 'Grass'
	, '17': 'Maple Tree'
	, '18': 'Stardust Tree'
	, '19': 'Carrots'
	, '20': 'Tomatoes'
	, '21': 'Potatoes'
};
var SKILL_LIST = ['mining', 'crafting', 'woodcutting', 'farming', 'brewing', 'combat', 'fishing', 'cooking', 'magic'];
var AREA_LIST = ['fields', 'forests', 'caves', 'volcano', 'northFields', 'hauntedMansion'];
var AREA_NAMES = ['Fields', 'Forests', 'Caves', 'Volcano', 'Northern Fields', 'Haunted Mansion'];
var MONSTER_NAMES = ['Chicken', 'Rat', 'Bee', 'Snake', 'Tree', 'Thief', 'Bear', 'Bat', 'Skeleton', 'Little Demon', 'Fire Bird', 'Healer', 'Zombie Goblin', 'Dead Tree', 'Ice Bird', 'Phantom', 'Ghost', 'The Death'];
var FISH_XP = {
	'rawShrimp': 50
	, 'rawSardine': 500
	, 'rawSalmon': 1e3
	, 'rawTuna': 3e3
	, 'rawLobster': 3e3
	, 'rawSwordfish': 5e3
	, 'rawEel': 9e3
	, 'rawShark': 12e3
};
var BOAT_LIST = ['rowBoat', 'canoe'];

var format;
(function (format)
{
	var UNITS = [
	{
		threshold: 10e3
		, factor: 1e3
		, token: 'k'
	}
	, {
		threshold: 1e6
		, factor: 1e6
		, token: 'M'
	}
	, {
		threshold: 1e9
		, factor: 1e9
		, token: 'B'
	}
	, {
		threshold: 1e12
		, factor: 1e12
		, token: 'T'
	}
	, {
		threshold: 1e15
		, factor: 1e15
		, token: 'Q'
	}];
	var TIME_STEPS = [
	{
		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 zeroPadLeft(num)
	{
		return (num < 10 ? '0' : '') + num;
	}

	function ensureNumber(num)
	{
		return (typeof num === 'number' ? num : Number(num));
	}

	function number(num, shorten)
	{
		if (shorten === void 0)
		{
			shorten = false;
		}
		num = ensureNumber(num);
		if (shorten)
		{
			for (var i = UNITS.length - 1; i >= 0; i--)
			{
				var unit = UNITS[i];
				if (num >= unit.threshold)
				{
					return number(Math.round(num / unit.factor)) + unit.token;
				}
			}
		}
		return num.toLocaleString('en');
	}
	format.number = number;

	function numbersInText(text)
	{
		return text.replace(/\d(?:[\d',\.]*\d)?/g, function (numStr)
		{
			return number(numStr.replace(/\D/g, ''));
		});
	}
	format.numbersInText = numbersInText;
	// use time format established in DHQoL (https://greasyfork.org/scripts/16041-dhqol)
	function timer(timer, shorten)
	{
		if (shorten === void 0)
		{
			shorten = true;
		}
		if (typeof timer === 'string')
		{
			timer = parseInt(timer, 10);
		}
		timer = Math.max(timer, 0);
		var days = Math.floor(timer / 86400); // 24 * 60 * 60
		var hours = Math.floor((timer % 86400) / 3600); // 60 * 60
		var minutes = Math.floor((timer % 3600) / 60);
		var seconds = timer % 60;
		return (shorten && days === 0 ? '' : days + 'd ')
			+ (shorten && days === 0 && hours === 0 ? '' : zeroPadLeft(hours) + ':')
			+ zeroPadLeft(minutes) + ':'
			+ zeroPadLeft(seconds);
	}
	format.timer = timer;

	function time2NearestUnit(time, long)
	{
		if (long === void 0)
		{
			long = false;
		}
		var step = TIME_STEPS[0];
		for (var i = TIME_STEPS.length - 1; i > 0; i--)
		{
			if (time >= TIME_STEPS[i].threshold)
			{
				step = TIME_STEPS[i];
				break;
			}
		}
		var factor = Math.pow(10, step.padp);
		var num = Math.round(time / step.threshold * factor) / factor;
		var unit = long ? step.name + (num === 1 ? '' : 's') : step.short;
		return num + ' ' + unit;
	}
	format.time2NearestUnit = time2NearestUnit;

	function sec2Str(seconds)
	{
		seconds = Number(seconds);
		if (seconds < 0)
		{
			return seconds.toString();
		}
		var s = seconds % 60;
		var m = Math.floor(seconds / 60) % 60;
		var h = Math.floor(seconds / 3600);
		var strs = [];
		if (h > 0)
		{
			strs.push(h + ' hour' + (h == 1 ? '' : 's'));
		}
		if (m > 0)
		{
			strs.push(m + ' minute' + (m == 1 ? '' : 's'));
		}
		if (s > 0)
		{
			strs.push(s + ' second' + (s == 1 ? '' : 's'));
		}
		if (strs.length > 1)
		{
			var glue = ' and ';
			for (var i = strs.length - 2; i >= 0; i--)
			{
				strs[i] = strs[i] + glue + strs[i + 1];
				glue = ', ';
			}
			return strs[0];
		}
		else
		{
			return strs[0] || '';
		}
	}
	format.sec2Str = sec2Str;

	function min2Str(minutes)
	{
		return sec2Str(Number(minutes) * 60);
	}
	format.min2Str = min2Str;
})(format || (format = {}));

/**
 * general functions
 */
function getStyle(elId)
{
	var id = elId != null ? 'style-' + elId : null;
	var styleElement = id != null ? document.getElementById(id) : null;
	if (styleElement == null)
	{
		styleElement = document.createElement('style');
		if (id != null)
		{
			styleElement.id = id;
		}
		styleElement.type = 'text/css';
		document.head.appendChild(styleElement);
	}
	return styleElement;
}

function addStyle(styleCode, elId)
{
	var styleElement = getStyle(elId);
	styleElement.innerHTML += styleCode;
}

function capitalize(str)
{
	return str[0].toUpperCase() + str.substr(1);
}

function key2Name(key, lowerCase)
{
	if (lowerCase === void 0)
	{
		lowerCase = false;
	}
	var name = key.replace(/[A-Z]/g, function (c)
	{
		return ' ' + (lowerCase ? c.toLowerCase() : c);
	});
	return lowerCase ? name : capitalize(name);
}

function split2Words(str, char)
{
	if (char === void 0)
	{
		char = ' ';
	}
	return str.replace(/[A-Z]/g, char + '$&');
}

function getBoundKey(key)
{
	return 'bound' + capitalize(key);
}

function getTierKey(key, tierLevel)
{
	return TIER_LEVELS[tierLevel] + capitalize(key);
}

function getWikiaKey(key)
{
	return key2Name(key.replace(/^bound-?|^special-case-/i, '').replace(/\d+[km]?$/i, ''))
		.replace(/^\s/, '').replace(/[ -]/g, '_')
		.replace(/^(?:Empty|Sapphire|Emerald|Ruby|Diamond|Raw|Uncooked|Filled)_/, '')
		.replace(/^(?:Bronze|Iron|Silver|Gold|Promethium|Runite)_(?!Bar)/, '')
		.replace(/^Npc_/, 'Monster_')
		.replace(/_(?:Unlocked|Quest)$/, '');
}

function getWikiaLink(key)
{
	return 'http://diamondhuntonline.wikia.com/wiki/' + getWikiaKey(key);
}

function now()
{
	return (new Date()).getTime();
}

function ensureTooltip(id, target)
{
	var tooltipId = 'tooltip-' + id;
	var tooltipEl = document.getElementById(tooltipId);
	if (!tooltipEl)
	{
		tooltipEl = document.createElement('div');
		tooltipEl.id = tooltipId;
		tooltipEl.style.display = 'none';
		var tooltipList = document.getElementById('tooltip-list');
		tooltipList.appendChild(tooltipEl);
	}
	// ensure binded events to show the tooltip
	if (target.dataset.tooltipId == null)
	{
		target.dataset.tooltipId = tooltipId;
		window.$(target).bind(
		{
			mousemove: window.changeTooltipPosition
			, mouseenter: window.showTooltip
			, mouseleave: function (event)
			{
				var target = event.target;
				var parent = target.parentElement;
				// ensure tooltips inside an tooltip element is possible
				if (!!target.dataset.tooltipId && parent && !!parent.dataset.tooltipId)
				{
					window.showTooltip.call(parent, event);
				}
				else
				{
					window.hideTooltip(event);
				}
			}
		});
	}
	return tooltipEl;
}
var timeStr2Sec = (function ()
{
	var unitFactors = {
		'd': 24 * 60 * 60
		, 'h': 60 * 60
		, 'm': 60
		, 's': 1
	};
	return function timeStr2Sec(str)
	{
		return str
			.replace(/(\d+)([hms])/g, function (wholeMatch, num, unit)
			{
				return parseInt(num) * (unitFactors[unit] || 1) + '+';
			})
			.split('+')
			.map(function (s)
			{
				return parseInt(s, 10);
			})
			.filter(function (n)
			{
				return !isNaN(n);
			})
			.reduce(function (p, c)
			{
				return p + c;
			}, 0);
	};
})();

function getGameValue(key)
{
	return window[key];
}

function getFurnaceLevel()
{
	for (var i = FURNACE_LEVELS.length - 1; i >= 0; i--)
	{
		if (getGameValue(getBoundKey(FURNACE_LEVELS[i] + 'Furnace')) > 0)
		{
			return i;
		}
	}
	return -1;
}

function getFurnaceLevelName()
{
	return FURNACE_LEVELS[getFurnaceLevel()] || '';
}

function getPrice(item)
{
	var price = window.getPrice(item);
	if (typeof price === 'number')
	{
		return price;
	}
	var match = price.match(/(\d+)([kM])/);
	if (!match)
	{
		return parseInt(price, 10);
	}
	var FACTORS = {
		'k': 1e3
		, 'M': 1e6
	};
	return parseInt(match[1], 10) * (FACTORS[match[2]] || 1);
}

function doGet(url)
{
	return new Promise(function (resolve, reject)
	{
		var request = new XMLHttpRequest();
		request.onreadystatechange = function (event)
		{
			if (request.readyState != XMLHttpRequest.DONE)
			{
				return;
			}
			if (request.status != 200)
			{
				return reject(event);
			}
			resolve(request.responseText);
		};
		request.open('GET', url);
		request.send();
	});
}
/**
 * persistence store
 */
var store;
(function (store)
{
	var oldPrefix = 'dh2-';
	var storePrefix = 'dh2.';

	function update(key, keepOldValue)
	{
		if (keepOldValue === void 0)
		{
			keepOldValue = true;
		}
		if (localStorage.hasOwnProperty(oldPrefix + key))
		{
			if (keepOldValue)
			{
				localStorage.setItem(storePrefix + key, localStorage.getItem(oldPrefix + key));
			}
			localStorage.removeItem(oldPrefix + key);
		}
	}
	var changeListener = new Map();

	function changeDetected(key, oldValue, newValue)
	{
		if (changeListener.has(key))
		{
			setTimeout(function ()
			{
				changeListener.get(key).forEach(function (fn)
				{
					return fn(key, oldValue, newValue);
				});
			});
		}
	}

	function watchFn(fnName)
	{
		var _fn = localStorage[fnName];
		localStorage[fnName] = function (key)
		{
			var args = [];
			for (var _i = 1; _i < arguments.length; _i++)
			{
				args[_i - 1] = arguments[_i];
			}
			var oldValue = localStorage.getItem(key);
			_fn.apply(localStorage, [key].concat(args));
			var newValue = localStorage.getItem(key);
			if (oldValue !== newValue)
			{
				changeDetected(key, oldValue, newValue);
			}
		};
	}
	watchFn('setItem');
	watchFn('removeItem');
	var _clear = localStorage.clear;
	localStorage.clear = function ()
	{
		var oldValues = new Map();
		for (var i = 0; i < localStorage.length; i++)
		{
			var key = localStorage.key(i);
			oldValues.set(key, localStorage.getItem(key));
		}
		_clear();
		for (var key in oldValues)
		{
			var newValue = localStorage.getItem(key);
			if (oldValues.get(key) !== newValue)
			{
				changeDetected(key, oldValues.get(key), newValue);
			}
		}
	};

	function addChangeListener(key, fn)
	{
		if (!changeListener.has(key))
		{
			changeListener.set(key, new Set());
		}
		changeListener.get(key).add(fn);
	}
	store.addChangeListener = addChangeListener;

	function removeChangeListener(key, fn)
	{
		if (changeListener.has(key))
		{
			changeListener.get(key).delete(fn);
		}
	}
	store.removeChangeListener = removeChangeListener;

	function get(key)
	{
		update(key);
		var value = localStorage.getItem(storePrefix + key);
		if (value != null)
		{
			try
			{
				return JSON.parse(value);
			}
			catch (e)
			{}
		}
		return value;
	}
	store.get = get;

	function has(key)
	{
		update(key);
		return localStorage.hasOwnProperty(storePrefix + key);
	}
	store.has = has;

	function remove(key)
	{
		update(key, false);
		localStorage.removeItem(storePrefix + key);
	}
	store.remove = remove;

	function set(key, value)
	{
		update(key, false);
		localStorage.setItem(storePrefix + key, JSON.stringify(value));
	}
	store.set = set;
})(store || (store = {}));

var settings;
(function (settings)
{
	settings.name = 'settings';
	var DIALOG_WIDTH = 450;
	var KEY;
	(function (KEY)
	{
		KEY[KEY["hideCraftingRecipes"] = 0] = "hideCraftingRecipes";
		KEY[KEY["hideUselessItems"] = 1] = "hideUselessItems";
		KEY[KEY["useNewChat"] = 2] = "useNewChat";
		KEY[KEY["colorizeChat"] = 3] = "colorizeChat";
		KEY[KEY["intelligentScrolling"] = 4] = "intelligentScrolling";
		KEY[KEY["showTimestamps"] = 5] = "showTimestamps";
		KEY[KEY["showIcons"] = 6] = "showIcons";
		KEY[KEY["showTags"] = 7] = "showTags";
		KEY[KEY["enableSpamDetection"] = 8] = "enableSpamDetection";
		KEY[KEY["showNotifications"] = 9] = "showNotifications";
		KEY[KEY["showEssencePopup"] = 10] = "showEssencePopup";
		KEY[KEY["wikiaLinks"] = 11] = "wikiaLinks";
		KEY[KEY["newXpAnimation"] = 12] = "newXpAnimation";
		KEY[KEY["amountSymbol"] = 13] = "amountSymbol";
		KEY[KEY["showTabTimer"] = 14] = "showTabTimer";
		KEY[KEY["showLootTab"] = 15] = "showLootTab";
		KEY[KEY["useEfficiencyStyle"] = 16] = "useEfficiencyStyle";
	})(KEY = settings.KEY || (settings.KEY = {}));;
	var CFG = (_a = {}
		, _a[KEY.hideCraftingRecipes] = {
			name: 'Hide crafting recipes of finished items'
			, description: "Hides crafting recipes of:\n\t\t\t\t<ul style=\"margin: .5rem 0 0;\">\n\t\t\t\t\t<li>furnace, oil storage and oven recipes if they aren't better than the current level</li>\n\t\t\t\t\t<li>machines if the user has the maximum amount of this type (counts bound and unbound items)</li>\n\t\t\t\t\t<li>non-stackable items which the user already owns (counts bound and unbound items)</li>\n\t\t\t\t</ul>"
			, defaultValue: true
		}
		, _a[KEY.hideUselessItems] = {
			name: 'Hide useless items'
			, description: "Hides <em>unbound</em> items which may has been crafted accidentially and are of no use for the player:\n\t\t\t\t<ul style=\"margin: .5rem 0 0;\">\n\t\t\t\t\t<li>furnace, oil storage and oven recipes if they aren't better than the current level</li>\n\t\t\t\t\t<li>machines if the user has already bound the maximum amount of this type</li>\n\t\t\t\t\t<li>non-stackable items which the user has already bound</li>\n\t\t\t\t</ul>"
			, defaultValue: false
		}
		, _a[KEY.useNewChat] = {
			name: 'Use the new chat'
			, description: "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
		}
		, _a[KEY.colorizeChat] = {
			name: 'Colorize chat messages'
			, description: "Colorize chat messages according to a unique color for each user"
			, defaultValue: false
			, sub:
			{
				'colorizer':
				{
					defaultValue: 0
					, label: ['Equally Distributed', 'Random (light colors)', 'Random (dark colors)']
					, options: ['equallyDistributed', 'random1', 'random2']
				}
			}
		}
		, _a[KEY.intelligentScrolling] = {
			name: 'Intelligent scrolling'
			, description: "Autoscroll gets disabled when you scroll up and gets enabled again when you scroll all the way down to the bottom of the chat."
			, defaultValue: true
		}
		, _a[KEY.showTimestamps] = {
			name: 'Show timestamps'
			, description: "Enables showing timestamps in chat"
			, defaultValue: true
		}
		, _a[KEY.showIcons] = {
			name: 'Show user-icons'
			, description: "Enables showing icons (formerly sigils) for each user in chat"
			, defaultValue: true
		}
		, _a[KEY.showTags] = {
			name: 'Show user-tags'
			, description: "Enables showing tags (Dev, Mod, Contributor) and colors for messages in chat"
			, defaultValue: true
		}
		, _a[KEY.enableSpamDetection] = {
			name: 'Enable spam detection'
			, description: "Enables simple spam detection"
			, defaultValue: true
		}
		, _a[KEY.showNotifications] = {
			name: 'Show browser notifications'
			, description: "Shows browser notifications for enabled events (click the little gear for more options)"
			, defaultValue: true
			, sub:
			{
				'showType':
				{
					defaultValue: 0
					, label: ['only when window inactive', 'always']
					, options: ['whenInactive', 'always']
				}
				, 'smelting':
				{
					defaultValue: true
					, label: 'Smelting finishes'
				}
				, 'chopping':
				{
					defaultValue: true
					, label: 'A tree is fully grown'
				}
				, 'harvest':
				{
					defaultValue: true
					, label: 'A plant can be harvested'
				}
				, 'potionEffect':
				{
					defaultValue: true
					, label: 'A potion\'s effect ends'
				}
				, 'boatReturned':
				{
					defaultValue: true
					, label: 'The boat or canoe returns'
				}
				, 'heroReady':
				{
					defaultValue: true
					, label: 'The hero is fully recovered and ready to fight'
				}
				, 'itemsSold':
				{
					defaultValue: true
					, label: 'Items are sold on the market'
				}
				, 'pirate':
				{
					defaultValue: true
					, label: 'A pirate has found a treasure map'
				}
				, 'essence':
				{
					defaultValue: true
					, label: 'An essence was found'
				}
				, 'pm':
				{
					defaultValue: true
					, label: 'A private messages (pm) arrives'
				}
				, 'mention':
				{
					defaultValue: true
					, label: 'The username is mentioned in chat'
				}
				, 'keyword':
				{
					defaultValue: true
					, label: 'A keyword is mentioned in chat'
				}
				, 'serverMsg':
				{
					defaultValue: true
					, label: 'Server messages (like <em>Server is restarting...</em>)'
				}
			}
		}
		, _a[KEY.showEssencePopup] = {
			name: 'Show essence popup'
			, description: "Shown a popup (like the ones when a diamond is found or the server is restarting) for finding an essence"
			, defaultValue: false
		}
		, _a[KEY.wikiaLinks] = {
			name: 'Show wikia links'
			, description: "Show wikia links for every item on hover (the little icon in the upper left corner)"
			, defaultValue: true
		}
		, _a[KEY.newXpAnimation] = {
			name: 'New XP-gain animation'
			, description: "Show gained xp on top skill bar instead on the position of the mouse"
			, defaultValue: true
		}
		, _a[KEY.amountSymbol] = {
			name: 'Show \u00D7 on items'
			, description: "Show a tiny \u00D7-symbol before amount numbers of items"
			, defaultValue: true
		}
		, _a[KEY.showTabTimer] = {
			name: 'Show tab timer and info'
			, description: "Show timer on tabs for trees, plants and hero"
			, defaultValue: true
		}
		, _a[KEY.showLootTab] = {
			name: 'Show sub tab for loot table'
			, description: "Show a sub tab for combat drop table in combat"
			, defaultValue: true
			, requiresReload: true
		}
		, _a[KEY.useEfficiencyStyle] = {
			name: 'Use space efficient style'
			, description: "Use a space efficient style with less blank space"
			, defaultValue: false
		}
		, _a);
	var SETTINGS_TABLE_ID = 'dh2-settings';
	var SETTING_ID_PREFIX = 'dh2-setting-';
	var settings2Init = Object.keys(CFG);
	/**
	 * settings
	 */
	function toName(key, subKey)
	{
		var name = typeof key === 'string' ? key : KEY[key];
		if (subKey !== undefined)
		{
			return name + '.' + subKey;
		}
		return name;
	}

	function getStoreKey(key, subKey)
	{
		return 'setting.' + toName(key, subKey);
	}
	var observedSettings = new Map();
	var observedSubSettings = new Map();

	function observe(key, fn)
	{
		var n = toName(key);
		if (!observedSettings.has(n))
		{
			observedSettings.set(n, new Set());
		}
		observedSettings.get(n).add(fn);
	}
	settings.observe = observe;

	function observeSub(key, subKey, fn)
	{
		var n = toName(key, subKey);
		if (!observedSubSettings.has(n))
		{
			observedSubSettings.set(n, new Set());
		}
		observedSubSettings.get(n).add(fn);
	}
	settings.observeSub = observeSub;

	function unobserve(key, fn)
	{
		var n = toName(key);
		if (!observedSettings.has(n))
		{
			return false;
		}
		return observedSettings.get(n).delete(fn);
	}
	settings.unobserve = unobserve;

	function unobserveSub(key, subKey, fn)
	{
		var n = toName(key, subKey);
		if (!observedSubSettings.has(n))
		{
			return false;
		}
		return observedSubSettings.get(n).delete(fn);
	}
	settings.unobserveSub = unobserveSub;
	var settingsProxies = new Map();

	function get(key)
	{
		if (!CFG.hasOwnProperty(key))
		{
			return false;
		}
		if (settingsProxies.has(key))
		{
			var proxy = settingsProxies.get(key);
			return proxy.get(key);
		}
		var name = getStoreKey(key);
		return store.has(name) ? store.get(name) : CFG[key].defaultValue;
	}
	settings.get = get;

	function getSub(key, subKey)
	{
		if (!CFG.hasOwnProperty(key))
		{
			return null;
		}
		var name = getStoreKey(key, subKey);
		var def = CFG[key].sub[subKey].defaultValue;
		if (store.has(name))
		{
			var stored = store.get(name);
			if (def instanceof Array)
			{
				for (var i = 0; i < def.length; i++)
				{
					if (stored.indexOf(def[i]) === -1)
					{
						stored.push(def[i]);
					}
				}
				for (var i = 0; i < stored.length; i++)
				{
					if (def.indexOf(stored[i]) === -1)
					{
						stored.splice(i, 1);
						i--;
					}
				}
			}
			return stored;
		}
		else
		{
			return def;
		}
	}
	settings.getSub = getSub;

	function set(key, newValue)
	{
		if (!CFG.hasOwnProperty(key))
		{
			return;
		}
		var oldValue = get(key);
		var n = toName(key);
		if (settingsProxies.has(key))
		{
			var proxy = settingsProxies.get(key);
			proxy.set(key, oldValue, newValue);
		}
		else
		{
			store.set(getStoreKey(key), newValue);
		}
		if (oldValue !== newValue && observedSettings.has(n))
		{
			observedSettings.get(n).forEach(function (fn)
			{
				return fn(key, oldValue, newValue);
			});
		}
	}
	settings.set = set;

	function setSub(key, subKey, newValue)
	{
		if (!CFG.hasOwnProperty(key))
		{
			return;
		}
		var oldValue = getSub(key, subKey);
		var n = toName(key, subKey);
		store.set(getStoreKey(key, subKey), newValue);
		if (oldValue !== newValue && observedSubSettings.has(n))
		{
			observedSubSettings.get(n).forEach(function (fn)
			{
				return fn(key, subKey, oldValue, newValue);
			});
		}
	}
	settings.setSub = setSub;

	function getSubCfg(key)
	{
		if (!CFG.hasOwnProperty(key))
		{
			return;
		}
		return CFG[key].sub;
	}
	settings.getSubCfg = getSubCfg;

	function initSettingsStyle()
	{
		addStyle("\ntable.table-style1 tr:not([onclick])\n{\n\tcursor: initial;\n}\n#tab-container-profile h2.section-title\n{\n\tcolor: orange;\n\tline-height: 1.2rem;\n\tmargin-top: 2rem;\n}\n#tab-container-profile h2.section-title > a.version\n{\n\tcolor: orange;\n\tfont-size: 1.2rem;\n\ttext-decoration: none;\n}\n#tab-container-profile h2.section-title > a.version:hover\n{\n\tcolor: white;\n\ttext-decoration: underline;\n}\n#tab-container-profile h2.section-title > span.note\n{\n\tfont-size: 0.9rem;\n}\n#" + SETTINGS_TABLE_ID + " tr.reload td:first-child::after\n{\n\tcontent: '*';\n\tfont-weight: bold;\n\tmargin-left: 3px;\n}\n#" + SETTINGS_TABLE_ID + " tr.sub td\n{\n\tposition: relative;\n}\n#" + SETTINGS_TABLE_ID + " tr.sub td button:last-child\n{\n\tmargin: -1px;\n\tposition: absolute;\n\tright: 0;\n}\n\n.ui-dialog-content > h2:first-child\n{\n\tmargin-top: 0;\n}\n\n.settings-container\n{\n\tlist-style: none;\n\tmargin: 5px 30px;\n\tpadding: 0;\n}\n.ui-dialog-content .settings-container\n{\n\tmargin: 5px 0;\n}\n.settings-container > li.setting\n{\n\tbackground-color: silver;\n\tborder: 1px solid black;\n\tborder-left: 0;\n\tborder-right: 0;\n\tborder-top-width: 0;\n\tdisplay: flex;\n}\n.settings-container > li.setting:first-child\n{\n\tborder-top-width: 1px;\n}\n.ui-dialog-content .settings-container > li.setting,\n.ui-dialog-content .settings-container > li.setting:hover\n{\n\tbackground-color: transparent;\n\tborder: 0;\n\tmargin: .25rem 0;\n}\n.settings-container > li.setting,\n.settings-container > li.setting *\n{\n\tcursor: pointer;\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n.settings-container > li.setting:hover\n{\n\tbackground-color: gray;\n}\n.settings-container > li.setting > input[type=\"checkbox\"]\n{\n\tdisplay: none;\n}\n.settings-container > li.setting > label\n{\n\tdisplay: block;\n\tflex-grow: 1;\n\tpadding: .25rem .5rem;\n}\n.settings-container > li.setting > label.ui-checkboxradio-label\n{\n\ttext-align: left;\n}\n.settings-container > li.setting > label.ui-checkboxradio-label .ui-checkboxradio-icon-space\n{\n\tmargin-right: .25rem;\n}\n.settings-container > li.setting > input + label:not(.ui-checkboxradio-label)::before\n{\n\tbackground-image: url(images/icons/x.png);\n\tbackground-size: 20px;\n\tcontent: '';\n\tdisplay: inline-block;\n\theight: 20px;\n\tmargin: 0 .25rem;\n\twidth: 20px;\n\tvertical-align: middle;\n}\n.settings-container > li.setting > input:checked + label:not(.ui-checkboxradio-label)::before\n{\n\tbackground-image: url(images/icons/check.png);\n}\n.ui-dialog-content .settings-container > li.setting > label + button\n{\n\tmargin-left: -.2rem;\n\tz-index: 1;\n}\n.settings-container.sortable > li.setting > span.ui-icon.handle\n{\n\tfloat: left;\n\tmargin: 6px 10px;\n\tz-index: 10;\n}\n.settings-container > li.setting span.ui-selectmenu-button\n{\n\twidth: calc(100% - 2em - 2*3px + 2*.1em);\n}\n\t\t");
	}

	function getSettingId(key, subKey)
	{
		var name = toName(key) + (subKey !== undefined ? '-' + subKey : '');
		return SETTING_ID_PREFIX + split2Words(name, '-').toLowerCase();
	}

	function initSettingTable()
	{
		function insertAfter(newChild, oldChild)
		{
			var 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';
		}
		var profileTable = document.getElementById('profile-toggleTable');
		if (!profileTable)
		{
			return;
		}
		var settingsHeader = document.createElement('h2');
		settingsHeader.className = 'section-title';
		settingsHeader.innerHTML = "Userscript \"DH2 Fixed\" <a class=\"version\" href=\"https://greasyfork.org/scripts/27642-dh2-fixed\" target=\"_blank\">v" + version + "</a><br>\n\t\t\t<span class=\"note\" style=\"display: none;\">(* changes require reloading the tab)</span>";
		var requiresReloadNote = settingsHeader.querySelector('.note');
		insertAfter(settingsHeader, profileTable);
		var settingsTable = document.createElement('table');
		settingsTable.id = SETTINGS_TABLE_ID;
		settingsTable.className = 'table-style1';
		settingsTable.width = '40%';
		settingsTable.innerHTML = "\n\t\t<tr style=\"background-color:grey;\">\n\t\t\t<th>Setting</th>\n\t\t\t<th>Enabled</th>\n\t\t</tr>\n\t\t";

		function addRowClickListener(row, key, settingId)
		{
			row.addEventListener('click', function ()
			{
				var newValue = !get(key);
				set(key, newValue);
				document.getElementById(settingId).src = getCheckImageSrc(newValue);
			});
		}

		function addSubClickListener(btn, dialog)
		{
			btn.addEventListener('click', function (event)
			{
				initJQueryDialog(dialog);
				event.stopPropagation();
				event.preventDefault();
			});
		}
		for (var _i = 0, settings2Init_1 = settings2Init; _i < settings2Init_1.length; _i++)
		{
			var k = settings2Init_1[_i];
			// convert it into a KEY
			var key = parseInt(k, 10);
			var setting = CFG[key];
			if (setting == null)
			{
				console.error('missing setting entry:', key, toName(key));
				continue;
			}
			var settingId = getSettingId(key);
			var row = settingsTable.insertRow(-1);
			row.classList.add('setting');
			if (setting.requiresReload)
			{
				row.classList.add('reload');
				requiresReloadNote.style.display = '';
			}
			row.setAttribute('onclick', '');
			row.innerHTML = "\n\t\t\t<td>" + setting.name + "</td>\n\t\t\t<td><img src=\"" + getCheckImageSrc(get(key)) + "\" id=\"" + settingId + "\" class=\"image-icon-20\"></td>\n\t\t\t";
			if (setting.sub)
			{
				row.classList.add('sub');
				var subBtn = document.createElement('button');
				subBtn.innerHTML = "<img src=\"images/icons/gearOff.gif\" class=\"image-icon-15\">";
				row.cells.item(0).appendChild(subBtn);
				var dialog = createSubSettingDialog(key);
				addSubClickListener(subBtn, dialog);
			}
			var tooltipEl = ensureTooltip(settingId, row);
			tooltipEl.innerHTML = setting.description;
			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 changes to this setting.</span>";
			}
			addRowClickListener(row, key, settingId);
		}
		insertAfter(settingsTable, settingsHeader);
	}

	function initProxies()
	{
		var row = document.querySelector('tr[data-tooltip-id="tooltip-profile-removeCraftingFilter"]');
		if (row)
		{
			var valueCache_1 = getGameValue('profileRemoveCraftingFilter') != 1;
			settingsProxies.set(KEY.hideCraftingRecipes
			, {
				get: function (key)
				{
					return getGameValue('profileRemoveCraftingFilter') != 1;
				}
				, set: function (key, oldValue, newValue)
				{
					if (valueCache_1 != newValue)
					{
						row.click();
						valueCache_1 = newValue;
					}
				}
			});
			observer.add('profileRemoveCraftingFilter', function ()
			{
				set(KEY.hideCraftingRecipes, getGameValue('profileRemoveCraftingFilter') != 1);
			});
		}
	}
	var subDialog;
	(function (subDialog)
	{
		function defaultHandler(key, dialog)
		{
			var setting = CFG[key];
			var subSettings = setting.sub;
			var settingContainer = createSubSettingsContainer(key, subSettings);
			dialog.appendChild(settingContainer);
		}

		function colorizeChat(dialog)
		{
			defaultHandler(KEY.colorizeChat, dialog);
		}
		subDialog.colorizeChat = colorizeChat;

		function showNotifications(dialog)
		{
			dialog.appendChild(document.createTextNode('Show notifications\u2026'));
			defaultHandler(KEY.showNotifications, dialog);
			dialog.appendChild(document.createTextNode('Events for which notifications are shown:'));
			var ulNotifType = dialog.lastElementChild;
			var ulEvents = ulNotifType.cloneNode(false);
			while (ulNotifType.children.length > 1)
			{
				ulEvents.appendChild(ulNotifType.children.item(1));
			}
			dialog.appendChild(ulEvents);
		}
		subDialog.showNotifications = showNotifications;
	})(subDialog || (subDialog = {}));

	function createSubSettingDialog(key)
	{
		var settingId = getSettingId(key);
		var setting = CFG[key];
		var dialog = document.createElement('div');
		dialog.id = 'dialog-' + settingId;
		dialog.style.display = 'none';
		dialog.innerHTML = "<h2>" + setting.name + "</h2>";
		var name = toName(key);
		if (subDialog.hasOwnProperty(name))
		{
			subDialog[name](dialog);
		}
		else
		{
			console.warn('missing setting handler for "%s"', name);
			var todoEl = document.createElement('span');
			todoEl.textContent = 'TODO';
			dialog.appendChild(todoEl);
		}
		document.body.appendChild(dialog);
		return dialog;
	}

	function createSubSettingsContainer(parentKey, subSettings)
	{
		var settingsContainer = document.createElement('ul');
		settingsContainer.className = 'settings-container';

		function addCheckbox(listEl, subKey, id, setting)
		{
			var checkbox = document.createElement('input');
			checkbox.type = 'checkbox';
			checkbox.id = id;
			checkbox.name = id;
			checkbox.checked = getSub(parentKey, subKey);
			var label = document.createElement('label');
			label.htmlFor = id;
			label.innerHTML = setting.label;
			checkbox.addEventListener('change', function ()
			{
				return setSub(parentKey, subKey, checkbox.checked);
			});
			listEl.appendChild(checkbox);
			listEl.appendChild(label);
		}

		function addSelectmenu(listEl, subKey, id, setting)
		{
			var select = document.createElement('select');
			select.id = id;
			select.name = id;
			var options = setting.options;
			var selectedIndex = getSub(parentKey, subKey);
			for (var i = 0; i < options.length; i++)
			{
				var option = document.createElement('option');
				option.value = options[i];
				if (setting.label)
				{
					option.innerHTML = setting.label[i];
				}
				else
				{
					option.innerHTML = key2Name(options[i]);
				}
				option.selected = i == selectedIndex;
				select.appendChild(option);
			}
			select.addEventListener('change', function ()
			{
				return setSub(parentKey, subKey, select.selectedIndex);
			});
			listEl.appendChild(select);
		}
		var keyList = Object.keys(subSettings);
		var orderIndex = keyList.findIndex(function (k)
		{
			return subSettings[k].defaultValue instanceof Array;
		});
		var isSortable = orderIndex != -1;
		if (isSortable)
		{
			keyList = getSub(parentKey, keyList[orderIndex]);
		}
		for (var _i = 0, keyList_1 = keyList; _i < keyList_1.length; _i++)
		{
			var subKey = keyList_1[_i];
			var settingId = getSettingId(parentKey, subKey);
			var setting = subSettings[subKey];
			var listEl = document.createElement('li');
			listEl.classList.add('setting');
			if (isSortable)
			{
				listEl.dataset.subKey = subKey;
				var sortableIcon = document.createElement('span');
				sortableIcon.className = 'ui-icon ui-icon-arrowthick-2-n-s handle';
				listEl.appendChild(sortableIcon);
			}
			if (setting.options)
			{
				addSelectmenu(listEl, subKey, settingId, setting);
			}
			else
			{
				addCheckbox(listEl, subKey, settingId, setting);
			}
			settingsContainer.appendChild(listEl);
		}
		return settingsContainer;
	}

	function initJQueryDialog(dialog)
	{
		var $dialog = window.$(dialog);
		$dialog.dialog(
		{
			width: DIALOG_WIDTH + 'px'
		});
		$dialog.find('input[type="checkbox"]').checkboxradio()
			.next().children(':first-child').removeClass('ui-state-hover');
		$dialog.find('select').selectmenu(
		{
			change: function (event, ui)
			{
				var changeEvent = document.createEvent('HTMLEvents');
				changeEvent.initEvent('change', false, true);
				event.target.dispatchEvent(changeEvent);
			}
		});
		$dialog.find('.sortable').sortable(
		{
			handle: '.handle'
			, update: function (event, ui)
			{
				var newOrder = [];
				var children = event.target.children;
				for (var i = 0; i < children.length; i++)
				{
					var child = children[i];
					newOrder.push(child.dataset.subKey);
				}
				var updateEvent = new CustomEvent('sortupdate'
				, {
					detail: newOrder
				});
				event.target.dispatchEvent(updateEvent);
			}
		});
		return $dialog;
	}

	function createSettingsContainer(settingList)
	{
		var settingsContainer = document.createElement('ul');
		settingsContainer.className = 'settings-container';

		function addOpenDialogClickListener(el, dialog)
		{
			el.addEventListener('click', function (event)
			{
				initJQueryDialog(dialog);
				event.stopPropagation();
				event.preventDefault();
			});
		}

		function addChangeListener(key, checkbox)
		{
			checkbox.addEventListener('change', function ()
			{
				set(key, checkbox.checked);
			});
		}
		for (var _i = 0, settingList_1 = settingList; _i < settingList_1.length; _i++)
		{
			var key = settingList_1[_i];
			var settingId = getSettingId(key);
			var setting = CFG[key];
			var index = settings2Init.indexOf(key.toString());
			if (index != -1)
			{
				settings2Init.splice(index, 1);
			}
			var listEl = document.createElement('li');
			listEl.classList.add('setting');
			if (setting.requiresReload)
			{
				listEl.classList.add('reload');
			}
			var checkbox = document.createElement('input');
			checkbox.type = 'checkbox';
			checkbox.id = settingId;
			checkbox.checked = get(key);
			var label = document.createElement('label');
			label.htmlFor = settingId;
			label.textContent = setting.name;
			addChangeListener(key, checkbox);
			listEl.appendChild(checkbox);
			listEl.appendChild(label);
			if (setting.sub)
			{
				var moreBtn = document.createElement('button');
				moreBtn.innerHTML = "<img src=\"images/icons/gearOff.gif\" class=\"image-icon-20\" />";
				listEl.appendChild(moreBtn);
				var dialog = createSubSettingDialog(key);
				addOpenDialogClickListener(moreBtn, dialog);
			}
			settingsContainer.appendChild(listEl);
			var tooltipEl = ensureTooltip(settingId, listEl);
			tooltipEl.innerHTML = setting.description;
			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 changes to this setting.</span>";
			}
		}
		return settingsContainer;
	}

	function initCraftingSettings()
	{
		var craftingItems = document.getElementById('tab-sub-container-crafting');
		if (!craftingItems)
		{
			return;
		}
		var br = craftingItems.nextElementSibling;
		var after = br.nextElementSibling;
		var parent = after.parentElement;
		var settingList = [KEY.hideCraftingRecipes, KEY.hideUselessItems];
		var settingsContainer = createSettingsContainer(settingList);
		parent.insertBefore(settingsContainer, after);
	}

	function initChatSettings()
	{
		var controlDiv = document.querySelector('#div-chat > div:first-child');
		if (!controlDiv)
		{
			return;
		}
		var btn = document.createElement('button');
		btn.textContent = 'Chat Settings';
		controlDiv.appendChild(btn);
		var dialog = document.createElement('div');
		dialog.id = 'dialog-chat-settings';
		dialog.style.display = 'none';
		dialog.innerHTML = "<h2>Chat Settings</h2>";
		var settingList = [KEY.useNewChat, KEY.colorizeChat, KEY.intelligentScrolling, KEY.showTimestamps, KEY.showIcons, KEY.showTags, KEY.enableSpamDetection];
		var settingsContainer = createSettingsContainer(settingList);
		dialog.appendChild(settingsContainer);
		document.body.appendChild(dialog);
		btn.addEventListener('click', function ()
		{
			initJQueryDialog(dialog);
		});
	}

	function init()
	{
		initProxies();
		initSettingsStyle();
		initCraftingSettings();
		initChatSettings();
		initSettingTable();
	}
	settings.init = init;
	var _a;
})(settings || (settings = {}));
/**
 * Code from https://github.com/davidmerfield/randomColor
 */
var colorGenerator;
(function (colorGenerator)
{
	// seed to get repeatable colors
	var seed = null;
	var COLOR_NOT_FOUND = {
		hueRange: []
		, lowerBounds: []
		, saturationRange: []
		, brightnessRange: []
	};
	var COLOR_BOUNDS = {
		'monochrome':
		{
			hueRange: []
			, lowerBounds: [
				[0, 0]
				, [100, 0]
			]
		}
		, 'red':
		{
			hueRange: [-26, 18]
			, lowerBounds: [
				[20, 100]
				, [30, 92]
				, [40, 89]
				, [50, 85]
				, [60, 78]
				, [70, 70]
				, [80, 60]
				, [90, 55]
				, [100, 50]
			]
		}
		, 'orange':
		{
			hueRange: [19, 46]
			, lowerBounds: [
				[20, 100]
				, [30, 93]
				, [40, 88]
				, [50, 86]
				, [60, 85]
				, [70, 70]
				, [100, 70]
			]
		}
		, 'yellow':
		{
			hueRange: [47, 62]
			, lowerBounds: [
				[25, 100]
				, [40, 94]
				, [50, 89]
				, [60, 86]
				, [70, 84]
				, [80, 82]
				, [90, 80]
				, [100, 75]
			]
		}
		, 'green':
		{
			hueRange: [63, 178]
			, lowerBounds: [
				[30, 100]
				, [40, 90]
				, [50, 85]
				, [60, 81]
				, [70, 74]
				, [80, 64]
				, [90, 50]
				, [100, 40]
			]
		}
		, 'blue':
		{
			hueRange: [179, 257]
			, lowerBounds: [
				[20, 100]
				, [30, 86]
				, [40, 80]
				, [50, 74]
				, [60, 60]
				, [70, 52]
				, [80, 44]
				, [90, 39]
				, [100, 35]
			]
		}
		, 'purple':
		{
			hueRange: [258, 282]
			, lowerBounds: [
				[20, 100]
				, [30, 87]
				, [40, 79]
				, [50, 70]
				, [60, 65]
				, [70, 59]
				, [80, 52]
				, [90, 45]
				, [100, 42]
			]
		}
		, 'pink':
		{
			hueRange: [283, 334]
			, lowerBounds: [
				[20, 100]
				, [30, 90]
				, [40, 86]
				, [60, 84]
				, [80, 80]
				, [90, 75]
				, [100, 73]
			]
		}
	};
	// shared color dictionary
	var colorDictionary = {};

	function defineColor(name, hueRange, lowerBounds)
	{
		var _a = lowerBounds[0]
			, sMin = _a[0]
			, bMax = _a[1];
		var _b = lowerBounds[lowerBounds.length - 1]
			, sMax = _b[0]
			, bMin = _b[1];
		colorDictionary[name] = {
			hueRange: hueRange
			, lowerBounds: lowerBounds
			, saturationRange: [sMin, sMax]
			, brightnessRange: [bMin, bMax]
		};
	}

	function loadColorBounds()
	{
		for (var name_1 in COLOR_BOUNDS)
		{
			defineColor(name_1, COLOR_BOUNDS[name_1].hueRange, COLOR_BOUNDS[name_1].lowerBounds);
		}
	}

	function randomWithin(min, max)
	{
		if (min === void 0)
		{
			min = 0;
		}
		if (max === void 0)
		{
			max = 0;
		}
		if (seed === null)
		{
			return Math.floor(min + Math.random() * (max + 1 - min));
		}
		else
		{
			// seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/
			seed = (seed * 9301 + 49297) % 233280;
			var rnd = seed / 233280.0;
			return Math.floor(min + rnd * (max - min));
		}
	}

	function getColorInfo(hue)
	{
		// maps red colors to make picking hue easier
		if (hue >= 334 && hue <= 360)
		{
			hue -= 360;
		}
		for (var colorName in colorDictionary)
		{
			var color = colorDictionary[colorName];
			if (color.hueRange.length > 0
				&& hue >= color.hueRange[0]
				&& hue <= color.hueRange[1])
			{
				return colorDictionary[colorName];
			}
		}
		return COLOR_NOT_FOUND;
	}

	function getHueRange(colorInput)
	{
		var number = typeof colorInput === 'undefined' ? Number.NaN : colorInput;
		if (typeof number === 'string')
		{
			number = parseInt(number, 10);
		}
		if (colorInput && isNaN(number) && colorDictionary.hasOwnProperty(colorInput))
		{
			var color = colorDictionary[colorInput];
			if (color.hueRange.length > 0)
			{
				return color.hueRange;
			}
		}
		else if (!isNaN(number) && number < 360 && number > 0)
		{
			return [number, number];
		}
		return [0, 360];
	}

	function pickHue(options)
	{
		var hueRange = getHueRange(options.hue);
		var hue = randomWithin(hueRange[0], hueRange[1]);
		// instead of storing red as two seperate ranges, we group them, using negative numbers
		if (hue < 0)
		{
			return 360 + hue;
		}
		return hue;
	}

	function getSaturationRange(hue)
	{
		return getColorInfo(hue).saturationRange;
	}

	function pickSaturation(hue, options)
	{
		if (options.luminosity === 'random')
		{
			return randomWithin(0, 100);
		}
		if (options.hue === 'monochrome')
		{
			return 0;
		}
		var _a = getSaturationRange(hue)
			, sMin = _a[0]
			, sMax = _a[1];
		switch (options.luminosity)
		{
		case 'bright':
			sMin = 55;
			break;
		case 'dark':
			sMin = sMax - 10;
			break;
		case 'light':
			sMax = 55;
			break;
		}
		return randomWithin(sMin, sMax);
	}

	function getMinimumBrightness(H, S)
	{
		var lowerBounds = getColorInfo(H).lowerBounds;
		for (var i = 0; i < lowerBounds.length - 1; i++)
		{
			var _a = lowerBounds[i]
				, s1 = _a[0]
				, v1 = _a[1];
			var _b = lowerBounds[i + 1]
				, s2 = _b[0]
				, v2 = _b[1];
			if (S >= s1 && S <= s2)
			{
				var m = (v2 - v1) / (s2 - s1);
				var b = v1 - m * s1;
				return m * S + b;
			}
		}
		return 0;
	}

	function pickBrightness(H, S, options)
	{
		var bMin = getMinimumBrightness(H, S);
		var bMax = 100;
		switch (options.luminosity)
		{
		case 'dark':
			bMax = bMin + 20;
			break;
		case 'light':
			bMin = (bMax + bMin) / 2;
			break;
		case 'random':
			bMin = 0;
			bMax = 100;
			break;
		}
		return randomWithin(bMin, bMax);
	}

	function HSVtoHSL(hsv)
	{
		var h = hsv[0];
		var s = hsv[1] / 100;
		var v = hsv[2] / 100;
		var k = (2 - s) * v;
		return [
			h
			, Math.round(s * v / (k < 1 ? k : 2 - k) * 10000) / 100
			, k / 2 * 100
		];
	}

	function HSVtoRGB(hsv)
	{
		// this doesn't work for the values of 0 and 360 here's the hacky fix
		var h = Math.min(Math.max(hsv[0], 1), 359);
		// Rebase the h,s,v values
		h = h / 360;
		var s = hsv[1] / 100;
		var v = hsv[2] / 100;
		var h_i = Math.floor(h * 6);
		var f = h * 6 - h_i;
		var p = v * (1 - s);
		var q = v * (1 - f * s);
		var t = v * (1 - (1 - f) * s);
		var r = 256;
		var g = 256;
		var b = 256;
		switch (h_i)
		{
		case 0:
			r = v;
			g = t;
			b = p;
			break;
		case 1:
			r = q;
			g = v;
			b = p;
			break;
		case 2:
			r = p;
			g = v;
			b = t;
			break;
		case 3:
			r = p;
			g = q;
			b = v;
			break;
		case 4:
			r = t;
			g = p;
			b = v;
			break;
		case 5:
			r = v;
			g = p;
			b = q;
			break;
		}
		return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)];
	}

	function HSVtoHex(hsv)
	{
		function componentToHex(c)
		{
			var hex = c.toString(16);
			return hex.length == 1 ? '0' + hex : hex;
		}
		var rgb = HSVtoRGB(hsv);
		return '#' + componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
	}

	function setFormat(hsv, options)
	{
		switch (options.format)
		{
		case 'hsvArray':
			return hsv;
		case 'hslArray':
			return HSVtoHSL(hsv);
		case 'hsl':
			var hsl = HSVtoHSL(hsv);
			return 'hsl(' + hsl[0] + ', ' + hsl[1] + '%, ' + hsl[2] + '%)';
		case 'hsla':
			var hslColor = HSVtoHSL(hsv);
			var alpha = options.alpha || Math.random();
			return 'hsla(' + hslColor[0] + ', ' + hslColor[1] + '%, ' + hslColor[2] + '%, ' + alpha + ')';
		case 'rgbArray':
			return HSVtoRGB(hsv);
		case 'rgb':
			var rgb = HSVtoRGB(hsv);
			return 'rgb(' + rgb.join(', ') + ')';
		case 'rgba':
			var rgbColor = HSVtoRGB(hsv);
			var alpha = options.alpha || Math.random();
			return 'rgba(' + rgbColor.join(', ') + ', ' + alpha + ')';
		case 'hex':
		default:
			return HSVtoHex(hsv);
		}
	}

	function generateColor(options)
	{
		// pick a hue (H)
		var H = pickHue(options);
		// use H to determine saturation (S)
		var S = pickSaturation(H, options);
		// use S and H to determine brightness (B)
		var B = pickBrightness(H, S, options);
		// return the HSB color in the desired format
		return setFormat([H, S, B], options);
	}

	function getRandom(options)
	{
		options = options ||
		{};
		seed = options.seed == null ? null : options.seed;
		// check if we need to generate multiple colors
		if (options.count !== null && options.count !== undefined)
		{
			var totalColors = options.count;
			var colors = [];
			options.count = null;
			while (totalColors > colors.length)
			{
				// Since we're generating multiple colors, the seed has to be incrememented.
				// Otherwise we'd just generate the same color each time...
				if (seed !== null)
				{
					seed += 1;
				}
				colors.push(generateColor(options));
			}
			options.count = totalColors;
			return colors;
		}
		return generateColor(options);
	}
	colorGenerator.getRandom = getRandom;
	var ColorInterval = (function ()
	{
		function ColorInterval(start, end)
		{
			this.start = start;
			this.end = end;
			this.left = null;
			this.right = null;
			this.value = null;
		}
		ColorInterval.prototype.getNextValue = function ()
		{
			if (this.value == null)
			{
				this.value = (this.start + this.end) / 2;
				return this.value;
			}
			if (this.left == null)
			{
				this.left = new ColorInterval(this.start, this.value);
				return this.left.getNextValue();
			}
			if (this.right == null)
			{
				this.right = new ColorInterval(this.value, this.end);
				return this.right.getNextValue();
			}
			if (this.left.getHeight() <= this.right.getHeight())
			{
				return this.left.getNextValue();
			}
			else
			{
				return this.right.getNextValue();
			}
		};
		ColorInterval.prototype.getHeight = function ()
		{
			return 1
				+ (this.left == null ? 0 : this.left.getHeight())
				+ (this.right == null ? 0 : this.right.getHeight());
		};
		return ColorInterval;
	}());
	var rootInterval = new ColorInterval(0, 360);

	function getEquallyDistributed()
	{
		return 'hsl(' + rootInterval.getNextValue() + ', 100%, 80%)';
	}
	colorGenerator.getEquallyDistributed = getEquallyDistributed;
	// populate the color dictionary
	loadColorBounds();
})(colorGenerator || (colorGenerator = {}));

/**
 * notifications
 */
var notifications;
(function (notifications)
{
	notifications.name = 'notifications';

	function event(title, options)
	{
		if ((!options || options.whenActive !== true)
			&& !document.hidden && document.hasFocus()
			&& settings.getSub(settings.KEY.showNotifications, 'showType') !== 1)
		{
			return;
		}
		if (!settings.get(settings.KEY.showNotifications))
		{
			// notifications disabled: return stub notification
			return Promise.resolve(
			{
				close: function () {}
			});
		}
		if (!("Notification" in window))
		{
			return Promise.reject('Your browser does not support notifications.');
		}
		return Notification.requestPermission()
			.then(function (permission)
			{
				if (permission === 'granted')
				{
					var n_1 = new Notification(title, options);
					n_1.onclick = function (event)
					{
						if (options && options.autoFocus !== false)
						{
							window.focus();
						}
						if (options && options.autoClose !== false)
						{
							n_1.close();
						}
						if (options && options.onclick)
						{
							options.onclick(n_1, event);
						}
					};
					return Promise.resolve(n_1);
				}
				else
				{
					return Promise.reject('Notification permission denied');
				}
			});
	}
	notifications.event = event;

	function requestPermission()
	{
		if (settings.get(settings.KEY.showNotifications))
		{
			Notification.requestPermission();
		}
	}

	function init()
	{
		requestPermission();
		settings.observe(settings.KEY.showNotifications, function ()
		{
			return requestPermission();
		});
	}
	notifications.init = init;
})(notifications || (notifications = {}));

/**
 * process commands
 */
var commands;
(function (commands)
{
	var XP_GAIN_KEY = 'xpGain';
	var MAX_XP_GAIN_HISTORY_LENGTH = 100;
	var IMAGE2SKILL = {
		// mining = #cc0000
		'icons/pickaxe': 'mining'
			// crafting = #cc0000
		, 'icons/anvil': 'crafting'
			// woodcutting = cyan
		, 'icons/woodcutting': 'woodcutting'
			// farming = green
		, 'icons/watering-can': 'farming'
			// brewing = #800080
		, 'vialOfWater': 'brewing'
		, 'largeVialOfWater': 'brewing'
		, 'hugeVialOfWater': 'brewing'
			// combat = lime
		, 'icons/combat': 'combat'
			// magic = blue
		, 'icons/wizardhat': 'magic'
			// fishing = blue
		, 'tuna': 'fishing'
			// cooking = yellow
		, 'icons/cooking': 'cooking'
	};
	var xpGainHistory = store.has(XP_GAIN_KEY) ? store.get(XP_GAIN_KEY) :
	{};
	addStyle("\n.scroller.xp\n{\n\tfont-size: 18pt;\n\tposition: absolute;\n\ttext-align: center;\n}\n\t");

	function fishingXp2Fish(xp)
	{
		for (var fish in FISH_XP)
		{
			if (FISH_XP[fish] == xp)
			{
				return fish;
			}
		}
		return 'raw unknown fish (worth ' + format.number(xp) + 'xp)';
	}

	function minutes2String(data)
	{
		return data.replace(/Your account has been running for: (\d+) minutes./, function (wholeMatch, minutes)
		{
			return 'Your account has been running for ' + format.min2Str(minutes) + '.';
		});
	}
	var LOOT_MSG_PREFIX = 'SHOW_LOOT_DIAG=';

	function processLoot(data)
	{
		if (!/^SM=Your boat found nothing\.$|^SHOW_LOOT_DIAG=/.test(data))
		{
			return false;
		}
		var loot = {
			type: 'loot'
			, title: ''
			, itemList: []
		};
		if (data.startsWith('SM='))
		{
			loot.title = 'Boat';
			loot.emptyText = 'Your boat found nothing.';
		}
		else if (data.startsWith(LOOT_MSG_PREFIX))
		{
			var split = data.substr(LOOT_MSG_PREFIX.length).split('~');
			loot.title = split[0];
			for (var i = 1; i < split.length; i += 2)
			{
				loot.itemList.push(
				{
					icon: split[i]
					, text: split[i + 1]
				});
			}
		}
		log.add(loot);
		return true;
	}
	var XP_GAIN_REGEX = /^ST=([^~]+)\.png~([^~]+)~\+(\d+)\s*xp(.*)$/;
	var animationQueue = {};

	function queueXpAnimation(skill, cell, color, xpAmount)
	{
		if (!settings.get(settings.KEY.newXpAnimation))
		{
			return;
		}
		animationQueue[skill] = animationQueue[skill] || [];
		animationQueue[skill].push(
		{
			cell: cell
			, color: color
			, xpAmount: xpAmount
		});
		if (animationQueue[skill].length === 1)
		{
			nextAnimation(skill);
		}
	}

	function nextAnimation(skill)
	{
		var entry = animationQueue[skill][0];
		if (!entry || !settings.get(settings.KEY.newXpAnimation))
		{
			return;
		}
		var cell = entry.cell
			, color = entry.color
			, xpAmount = entry.xpAmount;
		var rect = cell.getBoundingClientRect();
		var $el = window.$("<div class=\"scroller xp\" style=\"color: " + color + "; left: " + (rect.left + 50) + "px; top: " + (document.body.scrollTop + rect.top) + "px; width: " + (rect.width - 2 * 20 - 50) + "px;\">+" + format.number(xpAmount) + "</div>")
			.appendTo('body');
		// ensure the existence of $el, so the complete-function can be called instantly if the window is hidden
		$el
			.animate(
			{
				top: '-=15px'
			}
			, {
				duration: 1500
				, easing: 'easeOutQuad'
				, complete: function ()
				{
					animationQueue[skill].shift();
					nextAnimation(skill);
				}
			})
			.fadeOut(
			{
				duration: 2500
				, queue: false
				, complete: function ()
				{
					return $el.remove();
				}
			});
	}

	function processXpGain(data)
	{
		var match = data.match(XP_GAIN_REGEX);
		if (!match)
		{
			return false;
		}
		var icon = match[1];
		var skill = IMAGE2SKILL[icon] || '';
		var color = match[2];
		var xpAmount = Number(match[3]);
		var extra = match[4];
		var cell = document.getElementById('top-bar-level-td-' + skill);
		if (!cell)
		{
			console.debug('match (no cell found):', match);
			return false;
		}
		var entry = {
			time: now()
			, amount: xpAmount
		};
		if (match[4])
		{
			entry.extra = match[4];
		}
		if (skill == 'fishing')
		{
			var caughtFish = fishingXp2Fish(xpAmount);
			log.add(
			{
				type: 'fish'
				, fish: caughtFish
			});
		}
		if (extra)
		{
			var extraMatch = extra.match(/^\s*\(<img[^>]+src=(['"])images\/([^']+)\.png\1[^>]+>\s*(.+)\)$/);
			if (extraMatch)
			{
				var icon_1 = extraMatch[2];
				var text = extraMatch[3];
				if (icon_1 == 'brewingKit')
				{
					text = '+' + text;
				}
				window.scrollText(icon_1, color, text);
			}
			else
			{
				window.scrollText('none', color, extra);
			}
		}
		// save the xp event
		var list = xpGainHistory[skill] || [];
		list.push(entry);
		xpGainHistory[skill] = list.slice(-MAX_XP_GAIN_HISTORY_LENGTH);
		store.set(XP_GAIN_KEY, xpGainHistory);
		if (settings.get(settings.KEY.newXpAnimation))
		{
			queueXpAnimation(skill, cell, color, xpAmount);
		}
		return true;
	}

	function processLevelUp(data)
	{
		if (!data.startsWith('LVL_UP='))
		{
			return false;
		}
		var skill = data.substr('LVL_UP='.length);
		var xp = getGameValue(skill + 'Xp');
		var oldLvl = window.getLevel(xp);
		log.add(
		{
			type: 'lvlup'
			, skill: skill
			, newLevel: oldLvl + 1
		});
		return true;
	}

	function processCombat(data)
	{
		var match = data.match(/^STHS=(.+)~(.+)~(\d+)~img-(.+)~(melee|heal)$/);
		if (!match)
		{
			return false;
		}
		// keep track of different battles and add the data to the current battle
		log.add(
		{
			type: 'combat'
			, what: match[5]
			, who: match[4]
			, number: Number(match[3])
		});
		return true;
	}

	function processEnergy(data)
	{
		var match = data.match(/^ST=steak\.png~orange~\+([\d',]+)$/);
		if (!match)
		{
			return false;
		}
		log.add(
		{
			type: 'energy'
			, energy: Number(match[1].replace(/\D/g, ''))
		});
		return true;
	}

	function processHeat(data)
	{
		var match = data.match(/^ST=icons\/fire\.png~red~\+([\d',]+)$/);
		if (!match)
		{
			return false;
		}
		log.add(
		{
			type: 'heat'
			, heat: Number(match[1].replace(/\D/g, ''))
		});
		return true;
	}

	function processMarket(data)
	{
		if (data === 'ST=icons/shop.png~orange~Item Purchased')
		{
			log.add(
			{
				type: 'market'
			});
			return true;
		}
		var match = data.match(/^ST=coins\.png~yellow~\+([\d',]+)$/);
		if (!match)
		{
			return false;
		}
		var coins = Number(match[1].replace(/\D/g, ''));
		log.add(
		{
			type: 'market'
			, coins: coins
		});
		return true;
	}

	function processBonemeal(data)
	{
		var match = data.match(/^ST=filledBonemealBin\.png~white~\+([\d',]+)$/);
		if (!match)
		{
			return false;
		}
		var bonemeal = Number(match[1].replace(/\D/g, ''));
		log.add(
		{
			type: 'bonemeal'
			, bonemeal: bonemeal
		});
		return true;
	}

	function processCrafting(data)
	{
		if (data === 'ST=none~#806600~Item Crafted')
		{
			log.add(
			{
				type: 'crafting'
			});
			return true;
		}
		return false;
	}

	function processStardust(data)
	{
		var match = data.match(/^ST=icons\/stardust\.png~yellow~\+([\d',]+)$/);
		if (!match)
		{
			return false;
		}
		var stardust = Number(match[1].replace(/\D/g, ''));
		log.add(
		{
			type: 'stardust'
			, stardust: stardust
		});
		return true;
	}
	var RUNNING_ACCOUNT_STR = 'Your account has been running for:';

	function formatData(data)
	{
		if (data.startsWith('STHS=')
			|| data.startsWith('STE=')
			|| data.startsWith('SM=')
			|| data.startsWith('ST=')
			|| data.startsWith('SHOW_LOOT_DIAG='))
		{
			if (data.indexOf(RUNNING_ACCOUNT_STR) != -1)
			{
				data = minutes2String(data);
			}
			data = format.numbersInText(data);
		}
		return data;
	}
	commands.formatData = formatData;

	function process(data)
	{
		// prepare for logging events in an activity log
		if (processLoot(data))
		{
			return;
		}
		else if (processXpGain(data))
		{
			// return undefined to let the original function be called
			return settings.get(settings.KEY.newXpAnimation) ? null : void 0;
		}
		else if (processLevelUp(data)
			|| processCombat(data)
			|| processEnergy(data)
			|| processHeat(data)
			|| processMarket(data)
			|| processBonemeal(data)
			|| processCrafting(data)
			|| processStardust(data))
		{
			return;
		}
		else if (data.startsWith('SM='))
		{}
		else if (data.startsWith('STHS=') || data.startsWith('STE=') || data.startsWith('ST='))
		{}
		// notifications for this kind of message: "SM=An update has been scheduled for today."
		if (data.startsWith('SM='))
		{
			if (settings.getSub(settings.KEY.showNotifications, 'serverMsg'))
			{
				var msg = data.substr(3)
					.replace(/<br\s*\/?>/g, '\n')
					.replace(/<img src='images\/(.+?)\.png'.+?\/?> (\d+)/g, function (wholeMatch, key, amount)
					{
						return format.number(amount) + ' ' + split2Words(key) + ', ';
					})
					.replace(/<.+?>/g, '')
					.replace(/(\s)\1+/g, '$1')
					.replace(/, $/, '');
				notifications.event('Message from server'
				, {
					body: minutes2String(msg)
				});
			}
		}
		return;
	}
	commands.process = process;
})(commands || (commands = {}));

/**
 * log activities and stuff
 */
var log;
(function (log)
{
	log.name = 'log';
	var LOG_KEY = 'activityLog';
	var MAX_LOG_SIZE = 100;
	var logList = store.has(LOG_KEY) ? store.get(LOG_KEY) : [];
	var fightStarted = false;
	var currentCombat = null;
	var currentCombatEl = null;
	var logEl;

	function saveLog()
	{
		store.set(LOG_KEY, logList);
	}

	function createLi(entry)
	{
		var entryEl = document.createElement('li');
		entryEl.dataset.time = (new Date(entry.time || 0)).toLocaleString();
		entryEl.dataset.type = entry.type;
		return entryEl;
	}

	function appendLi(entryEl)
	{
		if (logEl.firstChild)
		{
			logEl.insertBefore(entryEl, logEl.firstChild);
		}
		else
		{
			logEl.appendChild(entryEl);
		}
	}

	function setGenericEntry(entry)
	{
		var el = createLi(entry);;
		el.innerHTML = typeof entry.data === 'string' ? format.numbersInText(entry.data) : JSON.stringify(entry.data);
		appendLi(el);
	}

	function setLootEntry(entry)
	{
		var el = createLi(entry);
		var header = document.createElement('h1');
		header.className = 'container-title';
		header.textContent = entry.title;
		el.appendChild(header);
		var itemContainer = document.createElement('span');
		if (entry.itemList.length === 0)
		{
			itemContainer.innerHTML = "<span class=\"dialogue-loot\">" + entry.emptyText + "</span>";
		}
		else
		{
			var update = false;
			for (var _i = 0, _a = entry.itemList; _i < _a.length; _i++)
			{
				var item = _a[_i];
				if (item.hasOwnProperty('key'))
				{
					item.icon = item.key;
					delete item.key;
					update = true;
				}
				if (item.hasOwnProperty('amount'))
				{
					item.text = (item.amount || Number.NaN).toString();
					delete item.amount;
					update = true;
				}
				var itemEl = document.createElement('span');
				itemEl.className = 'dialogue-loot';
				itemEl.innerHTML = "<img src=\"" + item.icon + "\" class=\"image-icon-50\"> " + format.numbersInText(item.text);
				itemContainer.appendChild(itemEl);
				itemContainer.appendChild(document.createTextNode(' '));
			}
			if (update)
			{
				saveLog();
			}
		}
		el.appendChild(itemContainer);
		appendLi(el);
	}

	function setFishEntry(entry)
	{
		var el = createLi(entry);
		el.innerHTML = "You caught a " + key2Name(entry.fish, true) + ".";
		appendLi(el);
	}

	function setEnergyEntry(entry)
	{
		var el = createLi(entry);
		el.innerHTML = "Your hero gained " + format.number(entry.energy) + " energy.";
		appendLi(el);
	}

	function setHeatEntry(entry)
	{
		var el = createLi(entry);
		el.innerHTML = "You added " + format.number(entry.heat) + " heat to your oven.";
		appendLi(el);
	}

	function setLevelUpEntry(entry)
	{
		var el = createLi(entry);
		el.innerHTML = "You advanced your " + entry.skill + " skill to level " + entry.newLevel + ".";
		appendLi(el);
	}

	function setCombatEntry(entry)
	{
		var created = currentCombatEl == null;
		if (currentCombatEl == null)
		{
			currentCombatEl = createLi(entry);
		}
		var HTML = '';
		// support old log format
		if (!entry.hasOwnProperty('ticks'))
		{
			var info = {
				hero:
				{
					heal: 0
					, melee: 0
				}
				, monster:
				{
					heal: 0
					, melee: 0
				}
			};
			for (var i = 0; i < entry.parts.length; i++)
			{
				var part = entry.parts[i];
				info[part.who][part.type] += part.number;
			}
			HTML = "<div>Hero: <span style=\"color: green;\">+" + info.hero.heal + "</span> <span style=\"color: red;\">-" + info.hero.melee + "</span></div>\n\t\t\t<div>Monster: <span style=\"color: green;\">+" + info.monster.heal + "</span> <span style=\"color: red;\">-" + info.monster.melee + "</span></div>";
		}
		else
		{
			var currentTick = Math.max(entry.ticks, 0);
			var width_1 = logEl.scrollWidth - 4 * 12.8 - 2;
			var scaleX_1 = currentTick === 0 ? 0 : width_1 / currentTick;

			function getInfo(data, initHp)
			{
				var points = [];
				var startHp = -1;
				var hp = initHp;
				for (var tick in data)
				{
					hp = data[tick];
					if (startHp === -1)
					{
						startHp = hp;
					}
					points.push((scaleX_1 * Number(tick)) + ' ' + hp);
				}
				if (points.length === 0)
				{
					points.push('0 ' + initHp);
				}
				points.push(width_1 + ' ' + hp, width_1 + ' 0', '0 0');
				return {
					points: points
					, startHp: startHp === -1 ? initHp : startHp
					, endHp: hp
				};
			}

			function getHTML(info, name)
			{
				return "<div class=\"combat-log-graph\">\n\t\t\t\t\t<span>" + name + " (" + info.startHp + " Hp to " + info.endHp + " Hp):</span><br>\n\t\t\t\t\t<svg style=\"height: " + info.startHp + "px;\"><polygon points=\"" + info.points.join(',') + "\"></polygon></svg>\n\t\t\t\t</div>";
			}
			var hero = getInfo(entry.hero, window.heroHp);
			var monster = getInfo(entry.monster, window.fightMonsterHp);
			// TODO: who won?
			HTML = "The fight took " + format.sec2Str(currentTick) + ".\n\t\t\t" + getHTML(hero, 'Hero') + "\n\t\t\t" + getHTML(monster, 'Monster') + "\n\t\t\t";
		}
		// map monster name and area name from monster id (the ids are starting at 1)
		var mId = entry.monsterId - 1;
		var monsterName = MONSTER_NAMES[mId] || '(' + (mId % 3 + 1) + ')';
		var areaId = Math.floor(mId / 3);
		var areaName = AREA_NAMES[areaId] || '(' + (areaId + 1) + ')';
		currentCombatEl.innerHTML = "<h2>Combat against " + monsterName + " in " + areaName + "</h2>\n\t\t" + HTML;
		if (created)
		{
			appendLi(currentCombatEl);
		}
		if (!fightStarted)
		{
			currentCombatEl = null;
		}
	}

	function setMarketEntry(entry)
	{
		var el = createLi(entry);
		if (entry.coins)
		{
			el.innerHTML = "You collected " + format.number(entry.coins) + " from market.";
		}
		else
		{
			el.innerHTML = "You purchased an item on market.";
		}
		appendLi(el);
	}

	function setBonemealEntry(entry)
	{
		var el = createLi(entry);
		el.innerHTML = "You added " + format.number(entry.bonemeal) + " bonemeal to your bonemeal bin.";
		appendLi(el);
	}

	function setCraftingEntry(entry)
	{
		var el = createLi(entry);
		el.innerHTML = "You crafted an item.";
		appendLi(el);
	}

	function setStardustEntry(entry)
	{
		var el = createLi(entry);
		el.innerHTML = "You got " + format.number(entry.stardust) + " stardust.";
		appendLi(el);
	}
	var entryType2Fn = {
		'loot': setLootEntry
		, 'fish': setFishEntry
		, 'energy': setEnergyEntry
		, 'heat': setHeatEntry
		, 'lvlup': setLevelUpEntry
		, 'combat': setCombatEntry
		, 'market': setMarketEntry
		, 'bonemeal': setBonemealEntry
		, 'crafting': setCraftingEntry
		, 'stardust': setStardustEntry
	};

	function updateLog(entry)
	{
		if (!logEl)
		{
			return;
		}
		if (entry.type && entryType2Fn.hasOwnProperty(entry.type))
		{
			entryType2Fn[entry.type](entry);
		}
		else
		{
			setGenericEntry(entry);
		}
	}

	function add2Log(entry)
	{
		logList.push(entry);
		logList = logList.slice(-MAX_LOG_SIZE);
		saveLog();
	}

	function add(entry)
	{
		if (!entry.time)
		{
			entry.time = now();
		}
		if (entry.type == 'combat')
		{
			if (!currentCombat)
			{
				console.warn('TODO: use the last stored combat, compare monster id and health state to check whether this combat might be interrupted last time (hero health != 0 && monster health != 0) and continue logging to that fight');
				return;
			}
			// skip entries without further information
			if (entry.number === 0)
			{
				return;
			}
			var hp = entry.who == 'hero' ? window.heroHp : window.fightMonsterHp;
			// the hp values are updated after this event, so I have to calculate the new value by myself
			hp += (entry.what == 'heal' ? 1 : -1) * entry.number;
			currentCombat[entry.who][currentCombat.ticks] = hp;
			saveLog();
			updateLog(currentCombat);
		}
		else
		{
			add2Log(entry);
			updateLog(entry);
		}
	}
	log.add = add;

	function init()
	{
		addStyle("\n#show-activity-log\n{\n\tdisplay: none;\n}\nbody\n{\n\toverflow-y: scroll;\n}\n#activity-log-label\n{\n\tcolor: pink;\n\tcursor: pointer;\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n#activity-log-overlay\n{\n\tbackground-color: transparent;\n\tcolor: transparent;\n\tpointer-events: none;\n\tposition: fixed;\n\tbottom: 0;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\ttransition: background-color .3s ease-out;\n\tz-index: 1000;\n}\n#show-activity-log:checked ~ #activity-log-overlay\n{\n\tbackground-color: rgba(0, 0, 0, 0.4);\n\tpointer-events: all;\n}\n#activity-log\n{\n\tbackground-color: white;\n\tcolor: black;\n\tlist-style: none;\n\tmargin: 0;\n\toverflow-y: scroll;\n\tpadding: .4rem .8rem;\n\tposition: fixed;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\ttransform: translateX(100%);\n\ttransition: transform .3s ease-out;\n\tmin-width: 15rem;\n\twidth: 40%;\n\tmax-width: 30rem;\n\tz-index: 1000;\n}\n#show-activity-log:checked ~ #activity-log\n{\n\ttransform: translateX(0%);\n}\n#activity-log::before\n{\n\tcontent: 'Activity Log';\n\tdisplay: block;\n\tfont-size: 1rem;\n\tfont-weight: bold;\n\tmargin-bottom: 0.8rem;\n}\n#activity-log:empty::after\n{\n\tcontent: 'Activities will be listed here.';\n}\n#activity-log li\n{\n\tborder: 1px solid gray;\n\tborder-radius: .2rem;\n\tmargin: .2rem 0;\n\tpadding: .4rem .8rem;\n}\n#activity-log li::before\n{\n\tcolor: gray;\n\tcontent: attr(data-time);\n\tdisplay: block;\n\tfont-size: 0.8rem;\n\tmargin: -4px 0 4px -4px;\n}\n.combat-log-graph > svg\n{\n\ttransform: scaleY(-1);\n\twidth: 100%;\n}\n.combat-log-graph > svg polygon\n{\n\tfill: green;\n\tstroke: black;\n\tstroke-width: 1px;\n}\n\t\t");
		// add new tab "Activity Log"
		var checkboxId = 'show-activity-log';
		var activityLogLabel = document.createElement('label');
		activityLogLabel.id = 'activity-log-label';
		activityLogLabel.htmlFor = checkboxId;
		activityLogLabel.textContent = 'Activity Log';
		newTopbar.addTabEntry(activityLogLabel);
		var checkbox = document.createElement('input');
		checkbox.id = checkboxId;
		checkbox.type = 'checkbox';
		checkbox.style.display = 'none';
		document.body.insertBefore(checkbox, document.body.firstChild);
		var label = document.createElement('label');
		label.id = 'activity-log-overlay';
		label.htmlFor = checkboxId;
		document.body.appendChild(label);
		logEl = document.createElement('ul');
		logEl.id = 'activity-log';
		document.body.appendChild(logEl);
		// add all stored elements
		logList.forEach(function (e)
		{
			return updateLog(e);
		});
		observer.add('fightMonsterId', function (key, oldValue, newValue)
		{
			fightStarted = newValue !== 0;
			if (fightStarted)
			{
				currentCombat = {
					type: 'combat'
					, time: now()
					, monsterId: newValue
					, ticks: -5
					, hero:
					{}
					, monster:
					{}
				};
				add2Log(currentCombat);
				updateLog(currentCombat);
			}
			else
			{
				if (currentCombat)
				{
					currentCombat.ticks--;
					saveLog();
				}
				currentCombat = null;
				currentCombatEl = null;
			}
		});
		observer.addTick(function ()
		{
			if (currentCombat !== null)
			{
				currentCombat.ticks++;
				if (currentCombat.ticks === 0)
				{
					currentCombat.hero[0] = window.heroHp;
					currentCombat.monster[0] = window.fightMonsterHp;
				}
				updateLog(currentCombat);
			}
		});
	}
	log.init = init;
})(log || (log = {}));

/**
 * game events
 */
var gameEvents;
(function (gameEvents)
{
	gameEvents.name = 'gameEvents';
	// min time difference between two notifications with the same title (10 seconds)
	var MIN_TIME_DIFFERENCE = 10;
	gameEvents.enabled = {
		smelting: true
		, chopping: true
		, harvest: true
		, boat: true
		, battle: true
		, brewing: true
		, market: true
	};
	var lastTimestamp = new Map();

	function notifyTabClickable(title, body, icon, tabKey, whenActive)
	{
		if (whenActive === void 0)
		{
			whenActive = false;
		}
		var now = (new Date).getTime();
		var timeDiff = now - (lastTimestamp.get(title) || 0);
		if (timeDiff < MIN_TIME_DIFFERENCE * 1e3)
		{
			return;
		}
		var promise = notifications.event(title
		, {
			body: body
			, icon: 'images/' + icon
			, whenActive: whenActive
			, onclick: function ()
			{
				var tabNames = tabKey.split('.');
				window.openTab(tabNames[0]);
				if (tabNames.length > 1)
				{
					window.openSubTab(tabNames[1]);
				}
			}
		});
		if (promise)
		{
			lastTimestamp.set(title, now);
		}
	}

	function observeTimer(k, fn)
	{
		observer.add(k, function (key, oldValue, newValue)
		{
			if (oldValue > 0 && newValue == 0)
			{
				fn(key, oldValue, newValue);
			}
		});
	}

	function smelting()
	{
		observeTimer('smeltingPercD', function (key, oldValue, newValue)
		{
			if (!gameEvents.enabled.smelting || !settings.getSub(settings.KEY.showNotifications, 'smelting'))
			{
				return;
			}
			notifyTabClickable('Hot topic', 'Your smelting has finished.', getFurnaceLevelName() + 'Furnace.png', 'crafting');
		});
	}

	function chopping()
	{
		observer.add([
			'treeStage1'
			, 'treeStage2'
			, 'treeStage3'
			, 'treeStage4'
			, 'treeStage5'
			, 'treeStage6'
		], function (key, oldValue, newValue)
		{
			if (!gameEvents.enabled.chopping || !settings.getSub(settings.KEY.showNotifications, 'chopping'))
			{
				return;
			}
			if (newValue == 4)
			{
				notifyTabClickable('Wood you be mine?', 'One or more of your trees are fully grown and can be chopped.', 'icons/woodcutting.png', 'woodcutting');
			}
		});
	}

	function harvest()
	{
		observer.add([
			'farmingPatchStage1'
			, 'farmingPatchStage2'
			, 'farmingPatchStage3'
			, 'farmingPatchStage4'
			, 'farmingPatchStage5'
			, 'farmingPatchStage6'
		], function (key, oldValue, newValue)
		{
			if (!gameEvents.enabled.harvest || !settings.getSub(settings.KEY.showNotifications, 'harvest'))
			{
				return;
			}
			if (newValue == 4)
			{
				notifyTabClickable('Green thumb', 'One or more of your crops is ready for harvest.', 'icons/watering-can.png', 'farming');
			}
			else if (newValue > 4)
			{
				notifyTabClickable('I didn\'t plant this', 'One or more of your crops died.', 'icons/watering-can.png', 'farming');
			}
		});
	}

	function boat()
	{
		observeTimer([
			'rowBoatTimer'
			, 'canoeTimer'
		], function (k, oldValue, newValue)
		{
			if (!gameEvents.enabled.boat || !settings.getSub(settings.KEY.showNotifications, 'boatReturned'))
			{
				return;
			}
			var key = k.replace(/Timer$/, '');
			notifyTabClickable('Fishy business', 'Your ' + split2Words(key).toLowerCase() + ' returned from its trip.', key + '.png', 'combat');
		});
	}

	function battle()
	{
		observeTimer('combatGlobalCooldown', function (key, oldValue, newValue)
		{
			if (!gameEvents.enabled.battle || !settings.getSub(settings.KEY.showNotifications, 'heroReady'))
			{
				return;
			}
			notifyTabClickable('Ready to work', 'Your hero is eager to fight.', 'icons/combat.png', 'combat');
		});
	}

	function brewing()
	{
		observeTimer([
			'barPotionTimer'
			, 'seedPotionTimer'
			, 'stardustPotionTimer'
			, 'superStardustPotionTimer'
		], function (key, oldValue, newValue)
		{
			if (!gameEvents.enabled.brewing || !settings.getSub(settings.KEY.showNotifications, 'potionEffect'))
			{
				return;
			}
			var potionKey = key.replace(/Timer$/, '');
			if (getGameValue(potionKey) > 0)
			{
				notifyTabClickable('Cheers!', 'You can drink another ' + split2Words(potionKey) + '.', key.replace(/Timer$/, '') + '.png', 'brewing');
			}
		});
	}

	function market()
	{
		var _refreshMarketSlot = window.refreshMarketSlot;
		window.refreshMarketSlot = function (offerId, itemName, amount, price, collectText, slotId, timeLeft)
		{
			if (gameEvents.enabled.market && settings.getSub(settings.KEY.showNotifications, 'itemsSold') && collectText > 0)
			{
				if (amount > 0)
				{
					notifyTabClickable('Ka-ching', 'You\'ve sold some ' + split2Words(itemName).toLowerCase() + ' to the market.', 'icons/shop.png', 'playermarket');
				}
				else
				{
					notifyTabClickable('Sold out', 'You\'ve sold all ' + split2Words(itemName).toLowerCase() + ' to the market.', 'icons/shop.png', 'playermarket');
				}
			}
			_refreshMarketSlot(offerId, itemName, amount, price, collectText, slotId, timeLeft);
		};
	}

	function gameValues()
	{
		observer.add('treasureMap', function (key, oldValue, newValue)
		{
			if (settings.getSub(settings.KEY.showNotifications, 'pirate') && oldValue < newValue)
			{
				notifyTabClickable('Arrrr!', 'Your pirate found a treasure map.', 'treasureMap.png', 'items');
			}
		});
		observer.add('essence', function (key, oldValue, newValue)
		{
			if (oldValue < newValue)
			{
				var diff = newValue - oldValue;
				var num = ['an', 'two', 'three'][diff - 1] || diff;
				var text = 'You found ' + num + ' essence' + (diff > 1 ? 's' : '') + '.';
				if (settings.get(settings.KEY.showEssencePopup))
				{
					window.confirmDialogue(400, text, 'Close', '', '');
				}
				if (settings.getSub(settings.KEY.showNotifications, 'essence'))
				{
					notifyTabClickable('Essence of Life', text, 'essence.png', 'combat.spells');
				}
			}
		});
		var stardustEl = document.querySelector('span[data-item-display="stardust"]');
		var parent = stardustEl && stardustEl.parentElement;
		if (stardustEl && parent)
		{
			addStyle("\n#dh2qol-stardustMonitor\n{\n\tdisplay: none !important;\n}\n#stardust-change\n{\n\tcolor: grey;\n\tdisplay: inline-block;\n\tmargin-left: .25rem;\n\ttext-align: left;\n\twidth: 2.5rem;\n}\n#stardust-change.hide\n{\n\tvisibility: hidden;\n}\n\t\t\t");
			var changeEl_1 = document.createElement('span');
			changeEl_1.id = 'stardust-change';
			parent.appendChild(changeEl_1);
			var HIDE_AFTER_TICKS_1 = 5;
			var ticksSinceSdChange_1 = HIDE_AFTER_TICKS_1;
			observer.add('stardust', function (key, oldValue, newValue)
			{
				var diff = newValue - oldValue;
				var sign = diff > 0 ? '+' : PLUS_MINUS_SIGN;
				changeEl_1.textContent = '(' + sign + format.number(diff) + ')';
				if (diff > 0)
				{
					ticksSinceSdChange_1 = 0;
				}
			});
			observer.addTick(function ()
			{
				var show = ticksSinceSdChange_1 < HIDE_AFTER_TICKS_1;
				changeEl_1.classList[show ? 'remove' : 'add']('hide');
				ticksSinceSdChange_1++;
			});
		}
	}

	function init()
	{
		smelting();
		chopping();
		harvest();
		boat();
		battle();
		brewing();
		market();
		gameValues();
	}
	gameEvents.init = init;
})(gameEvents || (gameEvents = {}));

/**
 * hide crafting recipes of lower tiers or of maxed machines
 */
var crafting;
(function (crafting)
{
	crafting.name = 'crafting';
	/**
	 * hide crafted recipes
	 */
	function setRecipeVisibility(key, visible)
	{
		var recipeRow = document.getElementById('crafting-' + key);
		if (recipeRow)
		{
			recipeRow.style.display = (!settings.get(settings.KEY.hideCraftingRecipes) || visible) ? '' : 'none';
		}
	}

	function hideLeveledRecipes(max, getKey, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var keys2Observe = [];
		var maxLevel = 0;
		for (var i = max - 1; i >= 0; i--)
		{
			var level = i + 1;
			var key = getKey(i);
			var boundKey = getBoundKey(key);
			keys2Observe.push(key);
			keys2Observe.push(boundKey);
			if (getGameValue(key) > 0 || getGameValue(boundKey) > 0)
			{
				maxLevel = Math.max(maxLevel, level);
			}
			setRecipeVisibility(key, level > maxLevel);
		}
		if (init)
		{
			observer.add(keys2Observe, function ()
			{
				return hideLeveledRecipes(max, getKey, false);
			});
		}
	}

	function hideToolRecipe(key, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var emptyKey = getTierKey(key, 0);
		var keys2Observe = [emptyKey];
		var hasTool = getGameValue(emptyKey) > 0;
		for (var i = 0; i < TIER_LEVELS.length; i++)
		{
			var boundKey = getBoundKey(getTierKey(key, i));
			hasTool = hasTool || getGameValue(boundKey) > 0;
			keys2Observe.push(boundKey);
		}
		setRecipeVisibility(emptyKey, !hasTool);
		if (init)
		{
			observer.add(keys2Observe, function ()
			{
				return hideToolRecipe(key, false);
			});
		}
	}

	function hideRecipe(key, hideInfo, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var maxValue = typeof hideInfo.max === 'function' ? hideInfo.max() : hideInfo.max;
		var boundKey = getBoundKey(key);
		var unbound = getGameValue(key);
		var bound = getGameValue(boundKey);
		var extra = (hideInfo.extraKeys || []).map(function (k)
		{
			return getGameValue(k);
		}).reduce(function (p, c)
		{
			return p + c;
		}, 0);
		setRecipeVisibility(key, (bound + unbound + extra) < maxValue);
		if (init)
		{
			observer.add([key, boundKey], function ()
			{
				return hideRecipe(key, hideInfo, false);
			});
		}
	}
	/**
	 * hide useless items
	 */
	function setItemVisibility(key, visible)
	{
		var itemBox = document.getElementById('item-box-' + key);
		if (itemBox)
		{
			itemBox.style.display = getGameValue(key) > 0 && (!settings.get(settings.KEY.hideUselessItems) || visible) ? '' : 'none';
		}
	}

	function hideLeveledItems(max, getKey, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var keys2Observe = [];
		var maxLevel = 0;
		for (var i = max - 1; i >= 0; i--)
		{
			var level = i + 1;
			var key = getKey(i);
			var boundKey = getBoundKey(key);
			keys2Observe.push(key);
			keys2Observe.push(boundKey);
			if (getGameValue(boundKey) > 0)
			{
				maxLevel = Math.max(maxLevel, level);
			}
			setItemVisibility(key, level > maxLevel);
		}
		if (init)
		{
			observer.add(keys2Observe, function ()
			{
				return hideLeveledItems(max, getKey, false);
			});
		}
	}

	function hideItem(key, hideInfo, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var maxValue = typeof hideInfo.max === 'function' ? hideInfo.max() : hideInfo.max;
		var boundKey = getBoundKey(key);
		var bound = getGameValue(boundKey);
		var extra = (hideInfo.extraKeys || []).map(function (k)
		{
			return getGameValue(k);
		}).reduce(function (p, c)
		{
			return p + c;
		}, 0);
		setItemVisibility(key, (bound + extra) < maxValue);
		if (init)
		{
			observer.add([key, boundKey], function ()
			{
				return hideItem(key, hideInfo, false);
			});
		}
	}

	function init()
	{
		function processRecipes(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			// furnace
			hideLeveledRecipes(FURNACE_LEVELS.length, function (i)
			{
				return FURNACE_LEVELS[i] + 'Furnace';
			}, init);
			// oil storage
			hideLeveledRecipes(OIL_STORAGE_SIZES.length, function (i)
			{
				return 'oilStorage' + (i + 1);
			}, init);
			// oven recipes
			hideLeveledRecipes(OVEN_LEVELS.length, function (i)
			{
				return OVEN_LEVELS[i] + 'Oven';
			}, init);
			// tools
			for (var _i = 0, TIER_ITEMS_1 = TIER_ITEMS; _i < TIER_ITEMS_1.length; _i++)
			{
				var tool = TIER_ITEMS_1[_i];
				hideToolRecipe(tool, init);
			}
			// other stuff
			for (var key in HIDE_RECIPES)
			{
				hideRecipe(key, HIDE_RECIPES[key], init);
			}
			if (init)
			{
				settings.observe(settings.KEY.hideCraftingRecipes, function ()
				{
					return processRecipes(false);
				});
			}
		}
		processRecipes(true);
		var _processCraftingTab = window.processCraftingTab;
		window.processCraftingTab = function ()
		{
			var reinit = !!window.refreshLoadCraftingTable;
			_processCraftingTab();
			if (reinit)
			{
				processRecipes(false);
			}
		};

		function processItems(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			// furnace
			hideLeveledItems(FURNACE_LEVELS.length, function (i)
			{
				return FURNACE_LEVELS[i] + 'Furnace';
			}, init);
			// oil storage
			hideLeveledItems(OIL_STORAGE_SIZES.length, function (i)
			{
				return 'oilStorage' + (i + 1);
			}, init);
			// oven recipes
			hideLeveledItems(OVEN_LEVELS.length, function (i)
			{
				return OVEN_LEVELS[i] + 'Oven';
			}, init);
			// other stuff
			for (var key in HIDE_RECIPES)
			{
				hideItem(key, HIDE_RECIPES[key], init);
			}
			if (init)
			{
				settings.observe(settings.KEY.hideUselessItems, function ()
				{
					return processItems(false);
				});
			}
		}
		processItems(true);
	}
	crafting.init = init;
})(crafting || (crafting = {}));

/**
 * improve item boxes
 */
var itemBoxes;
(function (itemBoxes)
{
	itemBoxes.name = 'itemBoxes';

	function hideNumberInItemBox(key, setVisibility)
	{
		if (setVisibility === void 0)
		{
			setVisibility = false;
		}
		var itemBox = document.getElementById('item-box-' + key);
		if (!itemBox)
		{
			return;
		}
		var numberElement = itemBox.lastElementChild;
		if (!numberElement)
		{
			return;
		}
		if (setVisibility)
		{
			numberElement.style.visibility = 'hidden';
		}
		else
		{
			numberElement.style.display = 'none';
		}
	}

	function addSpan2ItemBox(key)
	{
		hideNumberInItemBox(key);
		var itemBox = document.getElementById('item-box-' + key);
		if (!itemBox)
		{
			return;
		}
		var span = document.createElement('span');
		itemBox.appendChild(span);
		return span;
	}

	function setOilPerSecond(span, oil)
	{
		span.innerHTML = "+ " + format.number(oil) + " L/s <img src=\"images/oil.png\" class=\"image-icon-20\" style=\"margin-top: -2px;\">";
	}
	// show capacity of furnace
	function addFurnaceCaption()
	{
		for (var i = 0; i < FURNACE_LEVELS.length; i++)
		{
			var key = FURNACE_LEVELS[i] + 'Furnace';
			var boundKey = getBoundKey(key);
			var capacitySpan = addSpan2ItemBox(boundKey);
			if (capacitySpan)
			{
				capacitySpan.className = 'capacity';
				capacitySpan.textContent = 'Capacity: ' + format.number(window.getFurnaceCapacity(boundKey), true);
			}
		}
		// charcoal foundry
		var foundryCapacitySpan = addSpan2ItemBox('charcoalFoundry');
		if (foundryCapacitySpan)
		{
			foundryCapacitySpan.className = 'capacity';
			foundryCapacitySpan.textContent = 'Capacity: 100';
		}
	}
	// show oil cap of oil storage
	function addOilStorageCaption()
	{
		for (var i = 0; i < OIL_STORAGE_SIZES.length; i++)
		{
			var key = 'oilStorage' + (i + 1);
			var capSpan = addSpan2ItemBox(getBoundKey(key));
			if (capSpan)
			{
				capSpan.className = 'oil-cap';
				capSpan.textContent = 'Oil cap: ' + format.number(OIL_STORAGE_SIZES[i], true);
			}
		}
	}
	// show oil per second
	function addOilCaption()
	{
		var handheldOilSpan = addSpan2ItemBox('handheldOilPump');
		if (handheldOilSpan)
		{
			setOilPerSecond(handheldOilSpan, 1 * window.miner);
			observer.add('miner', function ()
			{
				return setOilPerSecond(handheldOilSpan, 1 * window.miner);
			});
		}
		var oilPipeSpan = addSpan2ItemBox('boundOilPipe');
		if (oilPipeSpan)
		{
			var oilPipeOrbKey_1 = 'boundBlueOilPipeOrb';

			function setCaption(span)
			{
				setOilPerSecond(span, 50 + getGameValue(oilPipeOrbKey_1) * 50);
			}
			setCaption(oilPipeSpan);
			observer.add(oilPipeOrbKey_1, function ()
			{
				return setCaption(oilPipeSpan);
			});
		}
		// add number of workers as caption to oil factory
		var workerSpan = addSpan2ItemBox('boundOilFactory');
		if (workerSpan)
		{
			var setCaption_1 = function ()
			{
				return workerSpan.textContent = 'Workers: ' + format.number(window.oilFactoryCheapWorkers, true);
			};
			setCaption_1();
			observer.add('oilFactoryCheapWorkers', function ()
			{
				return setCaption_1();
			});
		}
	}

	function addWandCaption()
	{
		for (var i = 0; i < WAND_LEVELS.length; i++)
		{
			var level = WAND_LEVELS[i];
			var key = level + 'Wand';
			var wandSpan = addSpan2ItemBox(key);
			if (wandSpan)
			{
				wandSpan.textContent = capitalize(level) + ' Wand';
			}
		}
	}

	function addVariousCaptions()
	{
		var key2Name = {
			'emptyAnvil': 'Anvil'
			, 'tap': 'Tree Tap'
			, 'farmer': 'Farmer'
			, 'gardener': 'Gardener'
			, 'planter': 'Planter'
			, 'boundBrewingKit': 'Brewing Kit'
			, 'cooksBook': 'Cooks Book'
			, 'cooksPage': 'Cooks Page'
			, 'combatDropTable': 'Loot Table'
			, 'magicBook': 'Spell Book'
		};
		for (var key in key2Name)
		{
			var span = addSpan2ItemBox(key);
			if (span)
			{
				span.textContent = key2Name[key];
			}
		}
	}
	// show current tier
	function addTierCaption()
	{
		addStyle("\nspan.item-box > span.orb::before\n{\n\tbackground-color: aqua;\n\tborder: 1px solid silver;\n\tborder-radius: 100%;\n\tcontent: '';\n\tdisplay: inline-block;\n\tmargin-left: -5px;\n\tmargin-right: 5px;\n\twidth: 10px;\n\theight: 10px;\n}\n\t\t");

		function addOrbObserver(key, spanList)
		{
			var boundOrbKey = getBoundKey('Blue' + capitalize(key) + 'Orb');

			function checkOrb()
			{
				var classAction = getGameValue(boundOrbKey) > 0 ? 'add' : 'remove';
				for (var _i = 0, spanList_1 = spanList; _i < spanList_1.length; _i++)
				{
					var span = spanList_1[_i];
					span.classList[classAction]('orb');
				}
			}
			checkOrb();
			observer.add(boundOrbKey, function ()
			{
				return checkOrb();
			});
		}
		var remainingOrbItems = ORB_ITEMS;
		for (var _i = 0, TIER_ITEMS_2 = TIER_ITEMS; _i < TIER_ITEMS_2.length; _i++)
		{
			var tierItem = TIER_ITEMS_2[_i];
			var isBindable = TIER_ITEMS_NOT_BINDABLE.indexOf(tierItem) === -1;
			var spanList = [];
			for (var i = 0; i < TIER_LEVELS.length; i++)
			{
				var key = getTierKey(tierItem, i);
				var toolKey = isBindable ? getBoundKey(key) : key;
				var tierSpan = addSpan2ItemBox(toolKey);
				if (tierSpan)
				{
					tierSpan.className = 'tier';
					tierSpan.textContent = TIER_NAMES[i];
					spanList.push(tierSpan);
				}
			}
			var orbIndex = remainingOrbItems.indexOf(tierItem);
			if (orbIndex !== -1)
			{
				addOrbObserver(tierItem, spanList);
				remainingOrbItems.splice(orbIndex, 1);
			}
		}
		for (var _a = 0, remainingOrbItems_1 = remainingOrbItems; _a < remainingOrbItems_1.length; _a++)
		{
			var itemKey = remainingOrbItems_1[_a];
			var captionSpan = document.querySelector('#item-box-' + getBoundKey(itemKey) + ' > span:last-of-type');
			if (!captionSpan)
			{
				continue;
			}
			addOrbObserver(itemKey, [captionSpan]);
		}
	}
	var boatKeys = ['rowBoat', 'canoe'];
	var boatTimerKeys = boatKeys.map(function (k)
	{
		return k + 'Timer';
	});

	function checkBoat(span, timerKey, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var isInTransit = getGameValue(timerKey) > 0;
		var otherInTransit = boatTimerKeys.some(function (k)
		{
			return k != timerKey && getGameValue(k) > 0;
		});
		span.textContent = isInTransit ? 'In transit' : 'Ready';
		span.style.visibility = otherInTransit ? 'hidden' : '';
		if (init)
		{
			observer.add(boatTimerKeys, function ()
			{
				return checkBoat(span, timerKey, false);
			});
		}
	}
	// show boat progress
	function addBoatCaption()
	{
		for (var i = 0; i < boatKeys.length; i++)
		{
			var span = addSpan2ItemBox(getBoundKey(boatKeys[i]));
			if (span)
			{
				checkBoat(span, boatTimerKeys[i], true);
			}
		}
	}
	// show bonemeal
	function addBonemealCaption()
	{
		var noBonemealSpan = addSpan2ItemBox('boundBonemealBin');
		if (!noBonemealSpan)
		{
			return;
		}
		noBonemealSpan.textContent = 'Bonemeal: 0';
		var bonemealSpan = addSpan2ItemBox('boundFilledBonemealBin');
		if (!bonemealSpan)
		{
			return;
		}
		bonemealSpan.dataset.itemDisplay = 'bonemeal';
		bonemealSpan.textContent = format.number(window.bonemeal);
		var captionSpan = document.createElement('span');
		captionSpan.textContent = 'Bonemeal: ';
		bonemealSpan.parentElement.insertBefore(captionSpan, bonemealSpan);
	}

	function warningBeforeSellingGems()
	{
		var _sellNPCItemDialogue = window.sellNPCItemDialogue;
		window.sellNPCItemDialogue = function (item, amount)
		{
			if (item == 'sapphire' || item == 'emerald' || item == 'ruby' || item == 'diamond' || item == 'bloodDiamond')
			{
				var itemName = key2Name(amount == 1 ? item : item.replace(/y$/, 'ie') + 's', true);
				if (amount == 0
					|| !window.confirm('Gems are precious and rare. Please consider carefully:\nDo you really want to sell ' + amount + ' ' + itemName + '?'))
				{
					return;
				}
			}
			else if (item == 'logs' || item == 'oakLogs' || item == 'willowLogs' || item == 'mapleLogs' || item == 'stardustLogs' || item == 'ancientLogs')
			{
				var itemName = key2Name(amount == 1 ? item.replace(/s$/, '') : item, true);
				if (amount == 0
					|| !window.confirm('Logs are time consuming to collect. Please consider carefully:\nDo you really want to sell ' + amount + ' ' + itemName + '?'))
				{
					return;
				}
			}
			_sellNPCItemDialogue(item, amount);
		};
	}

	function addWikiaLinks()
	{
		var WIKIA_CLASS = 'wikia-links';
		addStyle("\n." + WIKIA_CLASS + " .item-box\n{\n\tposition: relative;\n}\n.item-box > .wikia-link\n{\n\tbackground: black url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"30\" height=\"30\" viewBox=\"-2 -2 26 27\"><defs><linearGradient id=\"a\" x1=\"0%\" x2=\"63.85%\" y1=\"100%\" y2=\"32.54%\"><stop stop-color=\"#94D11F\" offset=\"0%\"/><stop stop-color=\"#09D3BF\" offset=\"100%\"/></linearGradient></defs><path fill=\"url(#a)\" fill-rule=\"evenodd\" d=\"M10.18 16.8c0 .2-.05.46-.26.67l-.8.7-7.38-6.95v-2.7l8.1 7.62c.12.12.33.36.33.66zm11.2-8.1v2.53l-9.15 8.86a.67.67 0 0 1-.5.2.73.73 0 0 1-.5-.2l-.85-.77 11-10.62zm-6.97 4.5l-2.53 2.43-8.04-7.67a2 2 0 0 1 0-2.9l2.53-2.43 8.04 7.67c.84.8.84 2.1 0 2.9zm-1.5-6.68L15.56 4c.4-.4.94-.6 1.52-.6.57 0 1.1.2 1.52.6l2.72 2.6-4.16 3.98-1.52-1.45-2.73-2.6zm10.18-.4l-6-5.8L17 .2l-.14.12-5.22 5.03L6.96.87l-.6-.48-.12-.1-.1.1-6.1 5.7-.04.06v5.76l.05.05 11.4 10.87.12.1.12-.1 11.37-10.87.05-.05V6.17l-.05-.05z\"/></svg>') no-repeat;\n\tdisplay: none;\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 30px;\n\theight: 30px;\n}\n." + WIKIA_CLASS + " .item-box:hover > .wikia-link\n{\n\tdisplay: block;\n}\n\t\t");

		function setWikiaLinksVisibility(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			var show = settings.get(settings.KEY.wikiaLinks);
			document.body.classList[show ? 'add' : 'remove'](WIKIA_CLASS);
			if (init)
			{
				settings.observe(settings.KEY.wikiaLinks, function ()
				{
					return setWikiaLinksVisibility();
				});
			}
		}
		setWikiaLinksVisibility(true);
		var boxes = document.getElementsByClassName('item-box');

		function disableClickPropagation(el)
		{
			el.addEventListener('click', function (event)
			{
				event.stopPropagation();
			});
		}
		for (var i = 0; i < boxes.length; i++)
		{
			var box = boxes.item(i);
			var key = box.id.replace(/^item-box-/, '');
			var linkArea = document.createElement('a');
			linkArea.className = 'wikia-link';
			linkArea.href = getWikiaLink(key);
			linkArea.target = '_blank';
			disableClickPropagation(linkArea);
			box.appendChild(linkArea);
			var tooltipEl = ensureTooltip('wikiLink', linkArea);
			if (tooltipEl.innerHTML === '')
			{
				tooltipEl.innerHTML = "Click to open the wikia page about this item.";
			}
		}
	}

	function init()
	{
		addFurnaceCaption();
		addOilStorageCaption();
		addOilCaption();
		addWandCaption();
		addVariousCaptions();
		addTierCaption();
		addBoatCaption();
		addBonemealCaption();
		warningBeforeSellingGems();
		addWikiaLinks();
	}
	itemBoxes.init = init;
})(itemBoxes || (itemBoxes = {}));

/**
 * add new chat
 */
var chat;
(function (chat)
{
	chat.name = 'chat';
	// min time difference between repeated messages to not be considered as spam
	var MIN_DIFF_REPEATED_MSG = 5e3;
	var KEYWORD_LIST_KEY = 'keywordList';
	var keywordList = store.has(KEYWORD_LIST_KEY) ? store.get(KEYWORD_LIST_KEY) : [];
	var CHAT_HISTORY_KEY = 'chatHistory';
	var MAX_CHAT_HISTORY_LENGTH = 100;
	var Type;
	(function (Type)
	{
		Type[Type["reload"] = -1] = "reload";
		Type[Type["normal"] = 0] = "normal";
		Type[Type["pmReceived"] = 1] = "pmReceived";
		Type[Type["pmSent"] = 2] = "pmSent";
		Type[Type["serverMsg"] = 3] = "serverMsg";
	})(Type || (Type = {}));;
	var Tag;
	(function (Tag)
	{
		Tag[Tag["none"] = 0] = "none";
		Tag[Tag["donor"] = 1] = "donor";
		Tag[Tag["contributor"] = 2] = "contributor";
		Tag[Tag["mod"] = 3] = "mod";
		Tag[Tag["dev"] = 4] = "dev";
		Tag[Tag["server"] = 5] = "server";
	})(Tag || (Tag = {}));;
	/**
	 * 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.
	 */
	var CHUNK_HIDING_MIN_CHUNKS = 10;
	var MSG_CHUNK_SIZE = 100;
	var RELOADED_CHAT_DATA = {
		timestamp: 0
		, username: ''
		, userlevel: 0
		, icon: 0
		, tag: 0
		, type: Type.reload
		, msg: '[...]'
	};
	var CHAT_BOX_ID = 'div-chat';
	var DEFAULT_CHAT_DIV_ID = 'div-chat-area';
	var GENERAL_CHAT_DIV_ID = 'div-chat-general';
	var PM_CHAT_TAB_PREFIX = 'tab-chat-pm-';
	var PM_CHAT_DIV_PREFIX = 'div-chat-pm-';
	var CHAT_TABS_ID = 'chat-tabs';
	var CHAT_INPUT_ID = 'chat-input-text';
	var CHAT_CLASS = 'div-chat-area';
	var COLORIZE_CLASS = 'colorize';
	var SpecialTab;
	(function (SpecialTab)
	{
		SpecialTab[SpecialTab["default"] = 0] = "default";
		SpecialTab[SpecialTab["general"] = 1] = "general";
		SpecialTab[SpecialTab["filler"] = 2] = "filler";
	})(SpecialTab || (SpecialTab = {}));;
	var CHAT_SPECIAL_TAB_ID = (_a = {}
		, _a[SpecialTab.default] = 'tab-chat-default'
		, _a[SpecialTab.general] = 'tab-chat-general'
		, _a[SpecialTab.filler] = 'tab-chat-filler'
		, _a);
	var CONTEXTMENU_ID = 'player-contextmenu';
	var CHAT_ICONS = [
		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'
		}
	];
	var CHAT_TAGS = [
		null
		, {
			key: 'donor'
			, name: ''
		}
		, {
			key: 'contributor'
			, name: 'Contributor'
		}
		, {
			key: 'mod'
			, name: 'Moderator'
		}
		, {
			key: 'dev'
			, name: 'Dev'
		}
		, {
			key: 'yell'
			, name: 'Server Message'
		}
	];
	var LOCALE = 'en-US';
	var LOCALE_OPTIONS = {
		hour12: false
		, year: 'numeric'
		, month: 'long'
		, day: 'numeric'
		, hour: '2-digit'
		, minute: '2-digit'
		, second: '2-digit'
	};
	// game commands
	var COMMANDS = [
		'pm'
		, 'mute'
		, 'clear'
		, 'ipmute'
	];
	var CLEAR_CMD = 'clear';
	var TUTORIAL_CMD = 'tutorial';
	var KEYWORD_ADD_CMD = 'keyword add';
	var KEYWORD_REMOVE_CMD = 'keyword remove';
	// load chat history
	var chatHistory = store.get(CHAT_HISTORY_KEY) || [];
	// store chat colors for each user
	var user2Color;
	var usedColors;
	// reserve color for special messages (e.g. server messages): white
	var reservedColors = ['#ffffff'];
	// message chunks
	var msgChunkMap = new Map();
	// for adding elements at startup
	var chatboxFragments = new Map();
	var chatInitialized = false;
	// find index of last message which is not a pm
	var isLastMsgNotReload = false;
	for (var i = chatHistory.length - 1; i >= 0; i--)
	{
		if (!isDataPM(chatHistory[i]))
		{
			isLastMsgNotReload = chatHistory[i].type != Type.reload;
			break;
		}
	}
	// insert a placeholder for a reloaded chat
	if (isLastMsgNotReload)
	{
		RELOADED_CHAT_DATA.timestamp = (new Date()).getTime();
		chatHistory.push(RELOADED_CHAT_DATA);
	}

	function isMuted(user)
	{
		return user !== window.username
			&& window.mutedPeople.some(function (name)
			{
				return user.indexOf(name) > -1;
			});
	}

	function isSpam(data)
	{
		// allow all messages from contributors, mods, devs and all server messages
		if (data.tag != Tag.none)
		{
			return false;
		}
		/**
		 * get last message of current user
		 */
		var isThisPm = isDataPM(data);
		var msgUsername = data.type === Type.pmSent ? window.username : data.username;
		var historyIndex = chatHistory.indexOf(data);
		if (historyIndex == -1)
		{
			historyIndex = chatHistory.length;
		}
		var lastData = null;
		for (var i = historyIndex - 1; i >= 0 && (lastData === null); i--)
		{
			var dataBefore = chatHistory[i];
			if (isThisPm === isDataPM(dataBefore))
			{
				var beforeUsername = dataBefore.type == Type.pmSent ? window.username : dataBefore.username;
				if (beforeUsername === msgUsername)
				{
					lastData = dataBefore;
				}
			}
		}
		/**
		 * compare message and don't allow the same message twice
		 */
		if (lastData
			&& lastData.msg === data.msg
			&& (data.timestamp - lastData.timestamp) < MIN_DIFF_REPEATED_MSG)
		{
			return true;
		}
		return false;
	}

	function handleScrolling(chatbox)
	{
		if (window.isAutoScrolling)
		{
			setTimeout(function ()
			{
				return chatbox.scrollTop = chatbox.scrollHeight;
			});
		}
	}
	// for chat messages which arrive before DOMContentLoaded and can not be displayed since the DOM isn't ready
	function processChatData(username, iconString, tagString, msg, isPM)
	{
		var tag = parseInt(tagString, 10);
		var userlevel = 0;
		var type = Type.normal;
		if (isPM == 1)
		{
			var match = msg.match(/^\s*\[(PM from|Sent to) ([A-Za-z0-9 ]+)\]: (.+?)\s*$/) || ['', '', username, msg];
			type = match[1] == 'Sent to' ? Type.pmSent : Type.pmReceived;
			username = match[2].replace(/_/g, ' ');
			msg = match[3];
		}
		else if (tag == Tag.server)
		{
			type = Type.serverMsg;
		}
		else
		{
			var match = msg.match(/^\s*\((\d+)\): (.+?)\s*$/);
			if (match)
			{
				userlevel = parseInt(match[1], 10);
				msg = match[2];
			}
			else
			{
				userlevel = window.getGlobalLevel();
			}
		}
		// unlinkify when using DH2QoL to store the plain message
		if (window.addToChatBox.toString().includes('linkify(arguments[3])'))
		{
			msg = msg.replace(/<a href='([^']+)' target='_blank'>\1<\/a>/ig, '$1');
		}
		if (type == Type.pmSent)
		{
			// turn some critical characters into HTML entities
			msg = msg.replace(/[<>]/g, function (char)
			{
				return '&#' + char.charCodeAt(0) + ';';
			});
		}
		return {
			timestamp: now()
			, username: username
			, userlevel: userlevel
			, icon: parseInt(iconString, 10)
			, tag: tag
			, type: type
			, msg: msg
		};
	}

	function saveChatHistory()
	{
		store.set(CHAT_HISTORY_KEY, chatHistory);
	}

	function add2ChatHistory(data)
	{
		chatHistory.push(data);
		chatHistory = chatHistory.slice(-MAX_CHAT_HISTORY_LENGTH);
		saveChatHistory();
	}

	function username2Id(username)
	{
		return username.replace(/ /g, '_');
	}

	function setNewCounter(tab, num, force)
	{
		if (force === void 0)
		{
			force = false;
		}
		var panel = getChatPanel(tab.dataset.username || '');
		if (force
			|| !tab.classList.contains('selected')
			|| !window.isAutoScrolling && panel.scrollHeight > panel.scrollTop + panel.offsetHeight)
		{
			tab.dataset.new = num.toString();
		}
	}

	function incrementNewCounter(tab)
	{
		setNewCounter(tab, parseInt(tab.dataset.new || '0', 10) + 1);
	}

	function getChatTab(username, specialTab)
	{
		var id = (specialTab != null)
			? CHAT_SPECIAL_TAB_ID[specialTab]
			: PM_CHAT_TAB_PREFIX + username2Id(username);
		var tab = document.getElementById(id);
		if (!tab)
		{
			tab = document.createElement('div');
			tab.className = 'chat-tab';
			if (specialTab != null)
			{
				tab.classList.add(SpecialTab[specialTab]);
			}
			tab.id = id;
			tab.dataset.username = username;
			setNewCounter(tab, 0, true);
			if (username.length > 2)
			{
				tab.textContent = username;
				// thanks /u/Spino-Prime for pointing out this was missing
				var closeSpan = document.createElement('span');
				closeSpan.className = 'close';
				tab.appendChild(closeSpan);
			}
			var chatTabs = document.getElementById(CHAT_TABS_ID);
			var filler = chatTabs.querySelector('.filler');
			if (filler)
			{
				chatTabs.insertBefore(tab, filler);
			}
			else
			{
				chatTabs.appendChild(tab);
			}
		}
		return tab;
	}

	function getChatPanel(username)
	{
		var id = username == '' ? GENERAL_CHAT_DIV_ID : PM_CHAT_DIV_PREFIX + username2Id(username);
		var panel = document.getElementById(id);
		if (!panel)
		{
			panel = document.createElement('div');
			panel.setAttribute('disabled', 'disabled');
			panel.id = id;
			panel.className = CHAT_CLASS;
			var defaultChat = document.getElementById(DEFAULT_CHAT_DIV_ID);
			var height = defaultChat.style.height;
			panel.style.height = height;
			var chatDiv = defaultChat.parentElement;
			chatDiv.insertBefore(panel, defaultChat);
		}
		return panel;
	}

	function changeChatTab(oldTab, newTab)
	{
		if (oldTab)
		{
			oldTab.classList.remove('selected');
			var oldChatPanel = void 0;
			if (oldTab.classList.contains('default'))
			{
				oldChatPanel = document.getElementById(DEFAULT_CHAT_DIV_ID);
			}
			else
			{
				oldChatPanel = getChatPanel(oldTab.dataset.username || '');
			}
			oldChatPanel.classList.remove('selected');
		}
		newTab.classList.add('selected');
		setNewCounter(newTab, 0, true);
		var newChatPanel;
		if (newTab.classList.contains('default'))
		{
			newChatPanel = document.getElementById(DEFAULT_CHAT_DIV_ID);
		}
		else
		{
			newChatPanel = getChatPanel(newTab.dataset.username || '');
		}
		newChatPanel.classList.add('selected');
		var toUsername = newTab.dataset.username;
		var newTextPlaceholder = toUsername == '' ? window.username + ':' : 'PM to ' + toUsername + ':';
		document.getElementById(CHAT_INPUT_ID).placeholder = newTextPlaceholder;
		handleScrolling(newChatPanel);
	}

	function clearChat(username)
	{
		// delete pms stored for that user
		var isPMChat = username != '';
		for (var i = 0; i < chatHistory.length; i++)
		{
			var data = chatHistory[i];
			if (isPMChat == isDataPM(data) && (!isPMChat || data.username == username))
			{
				chatHistory.splice(i, 1);
				i--;
			}
		}
		saveChatHistory();
		// clear pm-chat panel
		var panel = getChatPanel(username);
		while (panel.children.length > 0)
		{
			panel.removeChild(panel.children[0]);
		}
		msgChunkMap.delete(username);
		return panel;
	}

	function closeChatTab(username)
	{
		// clear pm-chat panel and remove message-history
		clearChat(username);
		// remove pm-tab (and change tab if necessary)
		var selectedTab = getSelectedTab();
		var tab2Close = getChatTab(username, null);
		if (selectedTab.dataset.username == username)
		{
			var generalTab = getChatTab('', SpecialTab.general);
			changeChatTab(tab2Close, generalTab);
		}
		var tabContainer = tab2Close.parentElement;
		tabContainer.removeChild(tab2Close);
	}

	function isDataPM(data)
	{
		return data.type === Type.pmSent || data.type === Type.pmReceived;
	}

	function colorizeMsg(username)
	{
		if (username == '')
		{
			return null;
		}
		if (!user2Color.has(username))
		{
			var color = void 0;
			do {
				var colorizer = settings.getSub(settings.KEY.colorizeChat, 'colorizer');
				if (colorizer == 1)
				{
					color = colorGenerator.getRandom(
					{
						luminosity: 'light'
					});
				}
				else if (colorizer == 2)
				{
					color = colorGenerator.getRandom(
					{
						luminosity: 'dark'
					});
				}
				else
				{
					color = colorGenerator.getEquallyDistributed();
				}
			} while (usedColors.has(color));
			user2Color.set(username, color);
			usedColors.add(color);
			addStyle("\n#" + CHAT_BOX_ID + "." + COLORIZE_CLASS + " .chat-msg[data-username=\"" + username + "\"]\n{\n\tbackground-color: " + color + ";\n}\n\t\t\t", 'name-color');
		}
		return user2Color.get(username);
	}

	function createMessageSegment(data)
	{
		var isThisPm = isDataPM(data);
		var msgUsername = data.type === Type.pmSent ? window.username : data.username;
		var historyIndex = chatHistory.indexOf(data);
		var isSameUser = null;
		var isSameTime = null;
		for (var i = historyIndex - 1; i >= 0 && (isSameUser === null || isSameTime === null); i--)
		{
			var dataBefore = chatHistory[i];
			if (isThisPm === isDataPM(dataBefore))
			{
				if (isSameUser === null)
				{
					var beforeUsername = dataBefore.type == Type.pmSent ? 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;
				}
			}
		}
		var d = new Date(data.timestamp);
		var hour = (d.getHours() < 10 ? '0' : '') + d.getHours();
		var minute = (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
		var icon = CHAT_ICONS[data.icon] ||
		{
			key: ''
			, title: ''
		};
		var tag = CHAT_TAGS[data.tag] ||
		{
			key: ''
			, name: ''
		};
		var formattedMsg = data.msg
			.replace(/<a href='(.+?)' target='_blank'>\1<\/a>/g, '$1')
			.replace(/(https?:\/\/[^\s"<>]+)/g, '<a target="_blank" href="$1">$1</a>');
		colorizeMsg(msgUsername);
		var msgTitle = data.type == Type.reload ? 'Chat loaded on ' + d.toLocaleString(LOCALE, LOCALE_OPTIONS) : '';
		var user = data.type === Type.serverMsg ? 'Server Message' : msgUsername;
		var levelAppendix = data.type == Type.normal ? ' (' + data.userlevel + ')' : '';
		var userTitle = data.tag != Tag.server ? tag.name : '';
		return "<span class=\"chat-msg\" data-type=\"" + data.type + "\" data-tag=\"" + tag.key + "\" data-username=\"" + msgUsername + "\">"
			+ ("<span\n\t\t\t\tclass=\"timestamp\"\n\t\t\t\tdata-timestamp=\"" + data.timestamp + "\"\n\t\t\t\tdata-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;
		}
		var isThisPm = isDataPM(data);
		// don't mute pms (you can just ignore pm-tab if you like)
		if (!isThisPm && isMuted(data.username))
		{
			return;
		}
		var userKey = isThisPm ? data.username : '';
		if (isThisPm)
		{
			window.lastPMUser = data.username;
		}
		// username is 3-12 characters long
		var chatbox = getChatPanel(userKey);
		var msgChunk = msgChunkMap.get(userKey);
		if (!msgChunk || msgChunk.children.length >= MSG_CHUNK_SIZE)
		{
			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);
			}
		}
		var tmp = document.createElement('templateWrapper');
		tmp.innerHTML = createMessageSegment(data);
		msgChunk.appendChild(tmp.children[0]);
		handleScrolling(chatbox);
		// add delay because handleScrolling is will set scrollTop delayed
		setTimeout(function ()
		{
			var chatTab = getChatTab(userKey, isThisPm ? null : SpecialTab.general);
			incrementNewCounter(chatTab);
		});
	}

	function applyChatStyle()
	{
		addStyle("\nspan.chat-msg\n{\n\tdisplay: flex;\n\tmin-height: 21px;\n\tpadding: 1px 0;\n}\n#" + CHAT_BOX_ID + ":not(." + COLORIZE_CLASS + ") span.chat-msg:nth-child(2n)\n{\n\tbackground-color: hsla(0, 0%, 90%, 1);\n}\n.chat-msg[data-type=\"" + Type.reload + "\"]\n{\n\tfont-size: 0.8rem;\n\tline-height: 1.2rem;\n}\n.chat-msg .timestamp\n{\n\tdisplay: none;\n}\n#" + CHAT_BOX_ID + ".showTimestamps .chat-msg:not([data-type=\"" + Type.reload + "\"]) .timestamp\n{\n\tcolor: hsla(0, 0%, 50%, 1);\n\tdisplay: inline-block;\n\tfont-size: .9rem;\n\tmargin: 0;\n\tmargin-right: 5px;\n\tposition: relative;\n\twidth: 2.5rem;\n}\n.chat-msg .timestamp[data-same-time=\"true\"]\n{\n\tcolor: hsla(0, 0%, 50%, .1);\n}\n.chat-msg:not([data-type=\"" + Type.reload + "\"]) .timestamp:hover::after\n{\n\tbackground-color: hsla(0, 0%, 12%, 1);\n\tborder-radius: .2rem;\n\tcontent: attr(data-fulltime);\n\tcolor: hsla(0, 0%, 100%, 1);\n\tline-height: 1.35rem;\n\tpadding: .4rem .8rem;\n\tpointer-events: none;\n\tposition: absolute;\n\tleft: 2.5rem;\n\ttop: -0.4rem;\n\ttext-align: center;\n\twhite-space: nowrap;\n}\n\n#" + CHAT_BOX_ID + ".showTags .chat-msg[data-type=\"" + Type.pmReceived + "\"] { color: purple; }\n#" + CHAT_BOX_ID + ".showTags .chat-msg[data-type=\"" + Type.pmSent + "\"] { color: purple; }\n#" + CHAT_BOX_ID + ".showTags .chat-msg[data-type=\"" + Type.serverMsg + "\"] { color: blue; }\n#" + CHAT_BOX_ID + ".showTags .chat-msg[data-tag=\"contributor\"] { color: green; }\n#" + CHAT_BOX_ID + ".showTags .chat-msg[data-tag=\"mod\"] { color: #669999; }\n#" + CHAT_BOX_ID + ".showTags .chat-msg[data-tag=\"dev\"] { color: #666600; }\n.chat-msg:not([data-type=\"" + Type.reload + "\"]) .user\n{\n\tflex: 0 0 132px;\n\tmargin-right: 5px;\n\twhite-space: nowrap;\n}\n#" + GENERAL_CHAT_DIV_ID + " .chat-msg:not([data-type=\"" + Type.reload + "\"]) .user\n{\n\tflex-basis: 182px;\n}\n#" + CHAT_BOX_ID + ".showIcons #" + GENERAL_CHAT_DIV_ID + " .chat-msg:not([data-type=\"" + Type.reload + "\"]) .user\n{\n\tpadding-left: 22px;\n}\n.chat-msg .user[data-same-user=\"true\"]:not([data-name=\"\"])\n{\n\tcursor: default;\n\topacity: 0;\n}\n\n.chat-msg .user .icon\n{\n\tdisplay: none;\n}\n#" + CHAT_BOX_ID + ".showIcons .chat-msg .user .icon\n{\n\tdisplay: inline-block;\n\tmargin-left: -22px;\n}\n.chat-msg .user .icon::before\n{\n\tbackground-size: 20px 20px;\n\tcontent: '';\n\tdisplay: inline-block;\n\tmargin-right: 2px;\n\twidth: 20px;\n\theight: 20px;\n\tvertical-align: middle;\n}\n.chat-msg .user .icon.halloween2015::before\t{ background-image: url('images/chat-icons/1.png'); }\n.chat-msg .user .icon.christmas2015::before\t{ background-image: url('images/chat-icons/2.png'); }\n.chat-msg .user .icon.easter2016::before\t{ background-image: url('images/chat-icons/3.png'); }\n.chat-msg .user .icon.halloween2016::before\t{ background-image: url('images/chat-icons/4.png'); }\n.chat-msg .user .icon.christmas2016::before\t{ background-image: url('images/chat-icons/5.png'); }\n.chat-msg .user .icon.dh1Max::before\t\t{ background-image: url('images/chat-icons/6.png'); }\n.chat-msg .user .icon.hardcore::before\t\t{ background-image: url('images/chat-icons/7.png'); }\n.chat-msg .user .icon.quest::before\t\t\t{ background-image: url('images/chat-icons/8.png'); }\n\n.chat-msg .user:not([data-same-user=\"true\"]) .name\n{\n\tcolor: rgba(0, 0, 0, 0.7);\n\tcursor: pointer;\n}\n.chat-msg .user .name.chat-tag-donor::before\n{\n\tbackground-image: url('images/chat-icons/donor.png');\n\tbackground-size: 20px 20px;\n\tcontent: '';\n\tdisplay: inline-block;\n\theight: 20px;\n\twidth: 20px;\n\tvertical-align: middle;\n}\n.chat-msg .user .name.chat-tag-yell\n{\n\tcursor: default;\n}\n#" + CHAT_BOX_ID + ".showTags .chat-msg .user .name.chat-tag-contributor,\n#" + CHAT_BOX_ID + ".showTags .chat-msg .user .name.chat-tag-mod,\n#" + CHAT_BOX_ID + ".showTags .chat-msg .user .name.chat-tag-dev,\n#" + CHAT_BOX_ID + ".showTags .chat-msg .user .name.chat-tag-yell\n{\n\tcolor: white;\n\tdisplay: inline-block;\n\tfont-size: 10pt;\n\tmargin-bottom: -1px;\n\tmargin-top: -1px;\n\tpadding-bottom: 2px;\n\ttext-align: center;\n\t/* 2px border, 10 padding */\n\twidth: calc(100% - 2*1px - 2*5px);\n}\n#" + CHAT_BOX_ID + ":not(.showTags) .chat-msg .user .name.chat-tag-contributor,\n#" + CHAT_BOX_ID + ":not(.showTags) .chat-msg .user .name.chat-tag-mod,\n#" + CHAT_BOX_ID + ":not(.showTags) .chat-msg .user .name.chat-tag-dev,\n#" + CHAT_BOX_ID + ":not(.showTags) .chat-msg .user .name.chat-tag-yell\n{\n\tbackground: initial;\n\tborder: inherit;\n\tfont-family: inherit;\n\tfont-size: inherit;\n\tpadding: initial;\n}\n\n.chat-msg[data-type=\"" + Type.reload + "\"] .user > *,\n.chat-msg[data-type=\"" + Type.pmReceived + "\"] .user > .icon,\n.chat-msg[data-type=\"" + Type.pmSent + "\"] .user > .icon\n{\n\tdisplay: none;\n}\n\n.chat-msg .msg\n{\n\tmin-width: 0;\n\toverflow: hidden;\n\tword-wrap: break-word;\n}\n\n#" + CHAT_BOX_ID + " ." + CHAT_CLASS + "\n{\n\twidth: 100%;\n\theight: 130px;\n\tdisplay: none;\n}\n#" + CHAT_BOX_ID + " ." + CHAT_CLASS + ".selected\n{\n\tdisplay: block;\n}\n#" + CHAT_TABS_ID + "\n{\n\tdisplay: flex;\n\tmargin: 10px -6px -6px;\n\tflex-wrap: wrap;\n}\n#" + CHAT_TABS_ID + " .chat-tab\n{\n\tbackground-color: gray;\n\tborder-top: 1px solid black;\n\tborder-right: 1px solid black;\n\tcursor: pointer;\n\tdisplay: inline-block;\n\tfont-weight: normal;\n\tpadding: 0.3rem .6rem;\n\tposition: relative;\n}\n#" + CHAT_TABS_ID + " .chat-tab.selected\n{\n\tbackground-color: transparent;\n\tborder-top-color: transparent;\n}\n#" + CHAT_TABS_ID + " .chat-tab.default\n{\n\tdisplay: none;\n}\n#" + CHAT_TABS_ID + " .chat-tab.filler\n{\n\tbackground-color: hsla(0, 0%, 90%, 1);\n\tborder-right: 0;\n\tbox-shadow: inset 5px 5px 5px -5px rgba(0, 0, 0, 0.5);\n\tcolor: transparent;\n\tcursor: default;\n\tflex-grow: 1;\n}\n#" + CHAT_TABS_ID + " .chat-tab::after\n{\n\tcolor: white;\n\tcontent: '(' attr(data-new) ')';\n\tfont-size: .9rem;\n\tfont-weight: bold;\n\tmargin-left: .4rem;\n}\n#" + CHAT_TABS_ID + " .chat-tab.selected::after\n{\n\tcolor: gray;\n}\n#" + CHAT_TABS_ID + " .chat-tab[data-new=\"0\"]::after\n{\n\tcolor: inherit;\n\tfont-weight: normal;\n}\n#" + CHAT_TABS_ID + " .chat-tab:not(.general).selected::after,\n#" + CHAT_TABS_ID + " .chat-tab:not(.general):hover::after\n{\n\tvisibility: hidden;\n}\n#" + CHAT_TABS_ID + " .chat-tab:not(.general).selected .close::after,\n#" + CHAT_TABS_ID + " .chat-tab:not(.general):hover .close::after\n{\n\tcontent: '\u00D7';\n\tfont-size: 1.5rem;\n\tposition: absolute;\n\ttop: 0;\n\tright: .6rem;\n\tbottom: 0;\n}\n\n#" + CONTEXTMENU_ID + "\n{\n\tbox-shadow: rgba(0, 0, 0, 0.8) 4px 4px 4px -2px;\n\tposition: fixed;\n}\n#" + CONTEXTMENU_ID + " .ui-widget-header\n{\n\tcursor: default;\n\tpadding: .25rem;\n}\n\t\t");
	}

	function initColorizer(init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var usernameList = user2Color && Array.from(user2Color.keys()) || [];
		user2Color = new Map();
		usedColors = new Set();
		for (var _i = 0, reservedColors_1 = reservedColors; _i < reservedColors_1.length; _i++)
		{
			var color = reservedColors_1[_i];
			usedColors.add(color);
		}
		var colorStyle = getStyle('name-color');
		colorStyle.innerHTML = '';
		for (var _a = 0, usernameList_1 = usernameList; _a < usernameList_1.length; _a++)
		{
			var username = usernameList_1[_a];
			colorizeMsg(username);
		}
		if (init)
		{
			settings.observeSub(settings.KEY.colorizeChat, 'colorizer', function ()
			{
				return initColorizer();
			});
		}
	}

	function addIntelligentScrolling()
	{
		// add checkbox instead of button for toggling auto scrolling
		var btn = document.querySelector('input[value="Toggle Autoscroll"]');
		var btnParent = btn.parentElement;
		var checkboxId = 'chat-toggle-autoscroll';
		// create checkbox
		var toggleCheckbox = document.createElement('input');
		toggleCheckbox.type = 'checkbox';
		toggleCheckbox.id = checkboxId;
		toggleCheckbox.checked = true;
		// create label
		var toggleLabel = document.createElement('label');
		toggleLabel.htmlFor = checkboxId;
		toggleLabel.textContent = 'Autoscroll';
		btnParent.insertBefore(toggleCheckbox, btn);
		btnParent.insertBefore(toggleLabel, btn);
		btn.style.display = 'none';
		var chatArea = document.getElementById(GENERAL_CHAT_DIV_ID);
		var showScrollTextTimeout = null;

		function setAutoScrolling(value, full)
		{
			if (full === void 0)
			{
				full = false;
			}
			if (window.isAutoScrolling != value)
			{
				toggleCheckbox.checked = value;
				window.isAutoScrolling = value;
				var icon_2 = 'none';
				var color_1 = value ? 'lime' : 'red';
				var text_1 = (value ? 'En' : 'Dis') + 'abled' + (full ? ' Autoscroll' : '');
				if (full)
				{
					if (showScrollTextTimeout)
					{
						window.clearTimeout(showScrollTextTimeout);
					}
					showScrollTextTimeout = window.setTimeout(function ()
					{
						return window.scrollText(icon_2, color_1, text_1);
					}, 300);
				}
				else
				{
					window.scrollText(icon_2, color_1, text_1);
				}
				setNewCounter(getSelectedTab(), 0, true);
				return true;
			}
			return false;
		}
		toggleCheckbox.addEventListener('change', function ()
		{
			setAutoScrolling(this.checked);
			if (this.checked && settings.get(settings.KEY.intelligentScrolling))
			{
				chatArea.scrollTop = chatArea.scrollHeight - chatArea.clientHeight;
			}
		});
		var placeholderTemplate = document.createElement('div');
		placeholderTemplate.className = 'placeholder';
		var childStore = new WeakMap();

		function scrollHugeChat()
		{
			// # of children
			var chunkNum = chatArea.children.length;
			// start chunk hiding at a specific amount of chunks
			if (chunkNum < CHUNK_HIDING_MIN_CHUNKS)
			{
				return;
			}
			var visibleTop = chatArea.scrollTop;
			var visibleBottom = visibleTop + chatArea.clientHeight;
			var referenceTop = visibleTop - window.innerHeight;
			var referenceBottom = visibleBottom + window.innerHeight;
			var top = 0;
			// never hide the last element since its size may change at any time when a new message gets appended
			for (var i = 0; i < chunkNum - 1; i++)
			{
				var child = chatArea.children[i];
				var height = child.clientHeight;
				var bottom = top + height;
				var isVisible = top >= referenceTop && top <= referenceBottom
					|| bottom >= referenceTop && bottom <= referenceBottom
					|| top < referenceTop && bottom > referenceBottom;
				var isPlaceholder = child.classList.contains('placeholder');
				if (!isVisible && !isPlaceholder)
				{
					var newPlaceholder = placeholderTemplate.cloneNode(false);
					newPlaceholder.style.height = height + 'px';
					chatArea.replaceChild(newPlaceholder, child);
					childStore.set(newPlaceholder, child);
				}
				else if (isVisible && isPlaceholder)
				{
					var oldChild = childStore.get(child);
					chatArea.replaceChild(oldChild, child);
					childStore.delete(child);
				}
				top = bottom;
			}
		}
		var delayedScrollStart = null;
		var delayedScrollTimeout = null;
		// does not consider pm tabs; may be changed in a future version?
		chatArea.addEventListener('scroll', function ()
		{
			if (settings.get(settings.KEY.intelligentScrolling))
			{
				var scrolled2Bottom = (chatArea.scrollTop + chatArea.clientHeight) >= chatArea.scrollHeight;
				setAutoScrolling(scrolled2Bottom, true);
			}
			var n = now();
			if (delayedScrollStart == null)
			{
				delayedScrollStart = n;
			}
			if (delayedScrollStart + 300 > n)
			{
				if (delayedScrollTimeout)
				{
					window.clearTimeout(delayedScrollTimeout);
				}
				delayedScrollTimeout = window.setTimeout(function ()
				{
					delayedScrollStart = null;
					delayedScrollTimeout = null;
					scrollHugeChat();
				}, 50);
			}
		});
	}

	function getSelectedTab()
	{
		return document.querySelector('#' + CHAT_TABS_ID + ' .chat-tab.selected');
	}

	function getSelectedTabUsername()
	{
		var selectedTab = getSelectedTab();
		return selectedTab.dataset.username || '';
	}

	function clickChatTab(newTab)
	{
		var oldTab = getSelectedTab();
		if (newTab == oldTab)
		{
			return;
		}
		changeChatTab(oldTab, newTab);
	}

	function clickCloseChatTab(tab)
	{
		var username = tab.dataset.username || '';
		var chatPanel = getChatPanel(username);
		if (chatPanel.children.length === 0
			|| confirm("Do you want to close the pm tab of \"" + username + "\"?"))
		{
			closeChatTab(username);
		}
	}

	function checkSetting(init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var enabled = settings.get(settings.KEY.useNewChat);
		// dis-/enable chat tabs
		var chatTabs = document.getElementById(CHAT_TABS_ID);
		chatTabs.style.display = enabled ? '' : 'none';
		// dis-/enable checkbox for intelligent scrolling
		var intelScrollId = 'chat-toggle-intelligent-scroll';
		var input = document.getElementById(intelScrollId);
		if (input)
		{
			input.style.display = enabled ? '' : 'none';
		}
		var label = document.querySelector('label[for="' + intelScrollId + '"]');
		if (label)
		{
			label.style.display = enabled ? '' : 'none';
		}
		// virtually click on a tab
		var defaultTab = getChatTab('', SpecialTab.default);
		var generalTab = getChatTab('', SpecialTab.general);
		clickChatTab(enabled ? generalTab : defaultTab);
		if (init)
		{
			settings.observe(settings.KEY.useNewChat, function ()
			{
				return checkSetting(false);
			});
		}
	}

	function addChatTabs()
	{
		var chatBoxArea = document.getElementById(CHAT_BOX_ID);
		var chatTabs = document.createElement('div');
		chatTabs.id = CHAT_TABS_ID;
		chatTabs.addEventListener('click', function (event)
		{
			var 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);
		// default tab (for disabled new chat)
		getChatTab('', SpecialTab.default);
		// general server chat
		var generalTab = getChatTab('', SpecialTab.general);
		generalTab.textContent = 'Server';
		getChatPanel('');
		getChatTab('', SpecialTab.filler);
		var _sendChat = window.sendChat;
		window.sendChat = function (inputEl)
		{
			var msg = inputEl.value;
			var selectedTab = document.querySelector('.chat-tab.selected');
			if (selectedTab.dataset.username != '' && msg[0] != '/')
			{
				inputEl.value = '/pm ' + (selectedTab.dataset.username || '').replace(/ /g, '_') + ' ' + msg;
			}
			_sendChat(inputEl);
		};
	}

	function switch2PmTab(username)
	{
		var newTab = getChatTab(username, null);
		clickChatTab(newTab);
	}

	function notifyPm(data)
	{
		notifications.event('Message from "' + data.username + '"'
		, {
			body: data.msg
			, onclick: function ()
			{
				return switch2PmTab(data.username);
			}
			, whenActive: getSelectedTab().dataset.username != data.username
		});
	}

	function checkMentionAndKeywords(data)
	{
		var lowerMsg = data.msg.toLowerCase();
		var usernameRegex = new RegExp('\\b' + window.username + '\\b', 'i');
		if (settings.getSub(settings.KEY.showNotifications, 'mention') && usernameRegex.test(lowerMsg))
		// if (lowerMsg.indexOf(window.username) > -1)
		{
			notifications.event('You\'ve been mentioned'
			, {
				body: data.msg
			});
		}
		var match = [];
		for (var _i = 0, keywordList_1 = keywordList; _i < keywordList_1.length; _i++)
		{
			var keyword = keywordList_1[_i];
			var regex = new RegExp('\\b' + keyword + '\\b', 'i');
			if (regex.test(lowerMsg))
			// if (lowerMsg.indexOf(keyword) > -1)
			{
				match.push(keyword);
			}
		}
		if (settings.getSub(settings.KEY.showNotifications, 'keyword') && match.length > 0)
		{
			notifications.event('Keyword: "' + match.join('", "') + '"'
			, {
				body: data.msg
			});
		}
	}
	var addToChatBox_ = null;

	function newAddToChatBox(username, icon, tag, msg, isPM)
	{
		var data = processChatData(username, icon, tag, msg, isPM);
		var isThisSpam = false;
		if (isDataPM(data))
		{
			if (data.type == Type.pmSent)
			{
				switch2PmTab(data.username);
			}
			else
			{
				notifyPm(data);
			}
		}
		else
		{
			isThisSpam = settings.get(settings.KEY.enableSpamDetection) && isSpam(data);
			if (!isThisSpam && data.username != window.username)
			{
				// check mentioning and keywords only for non-pms and only for messages from other players
				checkMentionAndKeywords(data);
			}
		}
		if (isThisSpam)
		{
			console.info('detected spam:', data);
		}
		else
		{
			add2ChatHistory(data);
			add2Chat(data);
		}
		var fn = addToChatBox_ == null ? window.addToChatBox : addToChatBox_;
		fn(username, icon, tag, msg, isPM);
	}
	chat.newAddToChatBox = newAddToChatBox;

	function openPmTab(username)
	{
		if (username == window.username || username == '')
		{
			return;
		}
		var userTab = getChatTab(username, null);
		clickChatTab(userTab);
		var input = document.getElementById(CHAT_INPUT_ID);
		input.focus();
	}

	function newChat()
	{
		addChatTabs();
		applyChatStyle();
		initColorizer(true);
		addToChatBox_ = window.addToChatBox;
		window.addToChatBox = newAddToChatBox;
		chatInitialized = true;
		var chatbox = document.getElementById(CHAT_BOX_ID);
		chatbox.addEventListener('click', function (event)
		{
			var target = event.target;
			var userEl = target && target.parentElement;
			if (!target || !userEl || !target.classList.contains('name') || !userEl.classList.contains('user'))
			{
				return;
			}
			if (userEl.dataset.sameUser != 'true')
			{
				openPmTab(userEl.dataset.name || '');
			}
		});
		chatbox.addEventListener('mouseover', function (event)
		{
			var target = event.target;
			if (!target.classList.contains('timestamp') || !target.dataset.timestamp)
			{
				return;
			}
			var timestamp = parseInt(target.dataset.timestamp || '0', 10);
			target.dataset.fulltime = (new Date(timestamp)).toLocaleDateString(LOCALE, LOCALE_OPTIONS);
			target.dataset.timestamp = '';
		});
		// add context menu
		var contextmenu = document.createElement('ul');
		contextmenu.id = CONTEXTMENU_ID;
		contextmenu.style.display = 'none';
		contextmenu.innerHTML = "<li class=\"name ui-widget-header\"><div></div></li>\n\t\t<li class=\"open-pm\"><div>Open pm tab</div></li>\n\t\t<li class=\"stats\"><div>Open stats</div></li>\n\t\t<li class=\"mute\"><div>Mute</div></li>\n\t\t<li class=\"unmute\"><div>Unmute</div></li>";
		document.body.appendChild(contextmenu);
		window.$(contextmenu).menu(
		{
			items: '> :not(.ui-widget-header)'
		});
		var nameListEl = contextmenu.querySelector('.name');
		var nameDivEl = nameListEl.firstElementChild;
		var muteEl = contextmenu.querySelector('.mute');
		var unmuteEl = contextmenu.querySelector('.unmute');
		chatbox.addEventListener('contextmenu', function (event)
		{
			var target = event.target;
			var userEl = target && target.parentElement;
			if (!userEl || !userEl.classList.contains('user'))
			{
				return;
			}
			var username = userEl.dataset.name;
			// ignore clicks on server messages or other special messages
			if (!username || userEl.dataset.sameUser == 'true')
			{
				return;
			}
			contextmenu.style.left = event.clientX + 'px';
			contextmenu.style.top = event.clientY + 'px';
			contextmenu.style.display = '';
			contextmenu.dataset.username = username;
			nameDivEl.textContent = username;
			var isMuted = window.mutedPeople.indexOf(username) !== -1;
			muteEl.style.display = isMuted ? 'none' : '';
			unmuteEl.style.display = isMuted ? '' : 'none';
			event.stopPropagation();
			event.preventDefault();
		});
		// add click listener for context menu and stop propagation
		contextmenu.addEventListener('click', function (event)
		{
			var target = event.target;
			event.stopPropagation();
			while (target && target.id != CONTEXTMENU_ID && target.tagName != 'LI')
			{
				target = target.parentElement;
			}
			if (!target || target.id == CONTEXTMENU_ID)
			{
				return;
			}
			var username = contextmenu.dataset.username || '';
			if (target.classList.contains('open-pm'))
			{
				openPmTab(username);
			}
			else if (target.classList.contains('stats'))
			{
				window.lookup(username);
			}
			else if (target.classList.contains('mute'))
			{
				if (username == '')
				{
					return;
				}
				window.mutedPeople.push(username);
				window.scrollText('none', 'lime', '<em>' + username + '</em> muted');
			}
			else if (target.classList.contains('unmute'))
			{
				if (username == '')
				{
					return;
				}
				var index = window.mutedPeople.indexOf(username);
				if (index !== -1)
				{
					window.mutedPeople.splice(index, 1);
				}
				window.scrollText('none', 'red', '<em>' + username + '</em> unmuted');
			}
			else
			{
				return;
			}
			contextmenu.style.display = 'none';
		});
		// add click listener to hide context menu
		document.addEventListener('click', function (event)
		{
			if (contextmenu.style.display != 'none')
			{
				contextmenu.style.display = 'none';
			}
		});
		window.addEventListener('contextmenu', function (event)
		{
			if (contextmenu.style.display != 'none')
			{
				contextmenu.style.display = 'none';
			}
		});
		// handle settings
		var showSettings = [settings.KEY.showTimestamps, settings.KEY.showIcons, settings.KEY.showTags];

		function setShowSetting(key)
		{
			var enabled = settings.get(key);
			chatbox.classList[enabled ? 'add' : 'remove'](settings.KEY[key]);
		}
		for (var _i = 0, showSettings_1 = showSettings; _i < showSettings_1.length; _i++)
		{
			var key = showSettings_1[_i];
			setShowSetting(key);
			settings.observe(key, function (k)
			{
				return setShowSetting(k);
			});
		}
	}

	function addCommandSuggester()
	{
		var input = document.getElementById(CHAT_INPUT_ID);
		input.addEventListener('keyup', function (event)
		{
			if (event.key == 'Backspace' || event.key == 'Delete' || event.key == 'Enter' || event.key == 'Tab'
				|| input.selectionStart != input.selectionEnd
				|| input.selectionStart != input.value.length
				|| !input.value.startsWith('/'))
			{
				return;
			}
			var value = input.value.substr(1);
			for (var _i = 0, COMMANDS_1 = COMMANDS; _i < COMMANDS_1.length; _i++)
			{
				var cmd = COMMANDS_1[_i];
				if (cmd.startsWith(value))
				{
					input.value = '/' + cmd;
					input.selectionStart = 1 + value.length;
					input.selectionEnd = input.value.length;
					break;
				}
			}
		});
	}

	function addOwnCommands()
	{
		COMMANDS.push(TUTORIAL_CMD);
		COMMANDS.push(KEYWORD_ADD_CMD);
		COMMANDS.push(KEYWORD_REMOVE_CMD);

		function processOwnCommands(value)
		{
			if (!value.startsWith('/'))
			{
				return value;
			}
			var msgPrefix = '/';
			var msg = value.substr(1);
			if (msg.startsWith('pm'))
			{
				var split = msg.split(' ');
				msgPrefix = '/' + split.slice(0, 2).join(' ') + ' ';
				msg = split.slice(2).join(' ');
			}
			if (msg.startsWith(CLEAR_CMD))
			{
				// clear current chat (pm chat, or general chat)
				var username = getSelectedTabUsername();
				clearChat(username);
			}
			else if (msg.startsWith(TUTORIAL_CMD))
			{
				// thanks aguyd (https://greasyfork.org/forum/profile/aguyd) for the idea
				var name_2 = msg.substr(TUTORIAL_CMD.length).trim();
				msgPrefix = '';
				msg = 'https://www.reddit.com/r/DiamondHunt/comments/5vrufh/diamond_hunt_2_starter_faq/';
				if (name_2.length != 0)
				{
					// maybe add '@' before the name?
					msg = name_2 + ', ' + msg;
				}
			}
			else if (msg.startsWith(KEYWORD_ADD_CMD))
			{
				var keyword = msg.substr(KEYWORD_ADD_CMD.length).trim().toLowerCase();
				if (keywordList.indexOf(keyword) == -1)
				{
					keywordList.push(keyword);
					store.set(KEYWORD_LIST_KEY, keywordList);
				}
				window.scrollText('none', 'lime', 'Keyword added: <em>' + keyword + '</em>');
			}
			else if (msg.startsWith(KEYWORD_REMOVE_CMD))
			{
				var keyword = msg.substr(KEYWORD_REMOVE_CMD.length).trim().toLowerCase();
				var index = keywordList.indexOf(keyword);
				if (index != -1)
				{
					keywordList.splice(index, 1);
					store.set(KEYWORD_LIST_KEY, keywordList);
				}
				window.scrollText('none', 'lime', 'Keyword removed: <em>' + keyword + '</em>');
			}
			return msgPrefix + msg;
		}
		var _sendChat = window.sendChat;
		window.sendChat = function (inputEl)
		{
			inputEl.value = processOwnCommands(inputEl.value);
			_sendChat(inputEl);
		};
	}

	function checkColorize(init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var chatDiv = document.getElementById(CHAT_BOX_ID);
		chatDiv.classList[settings.get(settings.KEY.colorizeChat) ? 'add' : 'remove'](COLORIZE_CLASS);
		if (init)
		{
			settings.observe(settings.KEY.colorizeChat, function ()
			{
				return checkColorize(false);
			});
		}
	}

	function init()
	{
		newChat();
		addIntelligentScrolling();
		addCommandSuggester();
		addOwnCommands();
		checkColorize(true);
		checkSetting(true);
		var _enlargeChat = window.enlargeChat;
		var chatBoxArea = document.getElementById(CHAT_BOX_ID);

		function setChatBoxHeight(height)
		{
			var defaultChat = document.getElementById(DEFAULT_CHAT_DIV_ID);
			defaultChat.style.height = height;
			var generalChat = document.getElementById(GENERAL_CHAT_DIV_ID);
			generalChat.style.height = height;
			var chatDivs = chatBoxArea.querySelectorAll('div[id^="' + PM_CHAT_DIV_PREFIX + '"]');
			for (var i = 0; i < chatDivs.length; i++)
			{
				chatDivs[i].style.height = height;
			}
		}
		window.enlargeChat = function (enlargeB)
		{
			_enlargeChat(enlargeB);
			var defaultChatDiv = document.getElementById(DEFAULT_CHAT_DIV_ID);
			var height = defaultChatDiv.style.height;
			store.set('chat.height', height);
			setChatBoxHeight(height);
			handleScrolling(defaultChatDiv);
		};
		setChatBoxHeight(store.get('chat.height'));
		// add history to chat
		chatHistory.forEach(function (d)
		{
			return add2Chat(d);
		});
		if (chatboxFragments)
		{
			chatboxFragments.forEach(function (fragment, key)
			{
				var chatbox = getChatPanel(key);
				chatbox.appendChild(fragment);
			});
			chatboxFragments = null;
		}
		// reset the new counter for all tabs
		var tabs = document.querySelectorAll('.chat-tab');
		for (var i = 0; i < tabs.length; i++)
		{
			setNewCounter(tabs[i], 0, true);
		}
	}
	chat.init = init;
	var _a;
})(chat || (chat = {}));

/**
 * hopefully only temporary fixes
 */
var temporaryFixes;
(function (temporaryFixes)
{
	temporaryFixes.name = 'temporaryFixes';
	// update spells being clickable in combat
	function setSpellsClickable()
	{
		var spellbox = document.getElementById('fight-spellboox');
		if (spellbox)
		{
			spellbox.style.pointerEvents = window.isInCombat() ? '' : 'none';
		}
	}
	// warn before unloading/reloading the tab if combat is in progress
	function combatWarnOnUnload()
	{
		if (!window.isInCombat())
		{
			window.onbeforeunload = null;
		}
		else
		{
			if (window.onbeforeunload == null)
			{
				window.onbeforeunload = function ()
				{
					return 'You are in a fight!';
				};
			}
		}
	}

	function fixCombatCountdown()
	{
		var el = document.getElementById('combat-countdown');
		if (!el)
		{
			return;
		}
		if (window.isInCombat())
		{
			el.style.display = '';
			var visible = window.combatCommenceTimer != 0;
			el.style.visibility = visible ? '' : 'hidden';
		}
	}
	// fix exhaustion timer and updating brewing and cooking recipes
	function fixExhaustionTimer()
	{
		if (document.getElementById('tab-container-combat').style.display != 'none')
		{
			window.combatNotFightingTick();
		}
	}

	function fixClientGameLoop()
	{
		var _clientGameLoop = window.clientGameLoop;
		window.clientGameLoop = function ()
		{
			_clientGameLoop();
			setSpellsClickable();
			combatWarnOnUnload();
			fixCombatCountdown();
			fixExhaustionTimer();
		};
	}
	// fix elements of scrollText (e.g. when joining the game and receiving xp at that moment)
	function fixScroller()
	{
		var textEls = document.querySelectorAll('div.scroller');
		for (var i = 0; i < textEls.length; i++)
		{
			var scroller = textEls[i];
			if (scroller.style.position != 'absolute')
			{
				scroller.style.display = 'none';
			}
		}
	}
	// fix style of tooltips
	function fixTooltipStyle()
	{
		addStyle("\nbody > div.tooltip > h2:first-child\n{\n\tmargin-top: 0;\n\tfont-size: 20pt;\n\tfont-weight: normal;\n}\n\t\t");
	}
	// fix buiulding magic table dynamically
	function fixRefreshingMagicRecipes()
	{
		window.refreshLoadMagicTable = true;
		var _processMagicTab = window.processMagicTab;
		window.processMagicTab = function ()
		{
			var _refreshLoadCraftingTable = window.refreshLoadCraftingTable;
			window.refreshLoadCraftingTable = window.refreshLoadMagicTable;
			_processMagicTab();
			window.refreshLoadCraftingTable = _refreshLoadCraftingTable;
		};
	}
	// move the strange leaf to brewing tab (thanks lasse_brus for this idea)
	function moveStrangeLeafs()
	{
		var strangeLeafBox = document.getElementById('item-box-strangeLeaf');
		var brewingContainer = document.getElementById('tab-sub-container-brewing');
		brewingContainer.appendChild(strangeLeafBox);
		// remove event listeners before binding the tooltip to it
		var $strangeLeafBox = window.$(strangeLeafBox);
		$strangeLeafBox.off('mouseover').off('mouseleave');
		strangeLeafBox.title = '';
		// bind tooltip to item box
		ensureTooltip('ingredient-secondary', strangeLeafBox);
		// change color
		var color1 = '#800080';
		var color2 = '#990099';
		strangeLeafBox.style.background = 'linear-gradient(' + color1 + ', ' + color2 + ')';
		$strangeLeafBox
			.mouseover(function ()
			{
				strangeLeafBox.style.background = 'none';
				strangeLeafBox.style.backgroundColor = color2;
			})
			.mouseleave(function ()
			{
				strangeLeafBox.style.background = 'linear-gradient(' + color1 + ', ' + color2 + ')';
			});
	}
	// fix height of map item
	function fixTreasureMap()
	{
		var mapBox = document.getElementById('item-box-treasureMap');
		var numSpan = mapBox.lastElementChild;
		numSpan.style.display = '';
		numSpan.style.visibility = 'hidden';
	}
	// fix wobbling tree places on hover (in wood cutting)
	function fixWoodcutting()
	{
		addStyle("\nimg.woodcutting-tree-img\n{\n\tborder: 1px solid transparent;\n}\n\t\t");
	}
	// fix wobbling quest rows on hover (in quest book)
	function fixQuestBook()
	{
		addStyle("\n#table-quest-list tr\n{\n\tborder: 1px solid transparent;\n}\n\t\t");
	}

	function fixScrollImages()
	{
		function fixIcon(icon)
		{
			return icon + (icon != 'none' && !/\..{3,4}$/.test(icon) ? '.png' : '');
		}
		var _scrollTextHitSplat = window.scrollTextHitSplat;
		window.scrollTextHitSplat = function (icon, color, text, elId, cbType)
		{
			_scrollTextHitSplat(fixIcon(icon), color, text, elId, cbType);
		};
		var _scrollText = window.scrollText;
		window.scrollText = function (icon, color, text)
		{
			_scrollText(fixIcon(icon), color, text);
		};
	}

	function fixQuest8BraveryRecipe()
	{
		observer.add([
			'quest8'
			, 'braveryPotion'
		], function ()
		{
			var show = window.quest8 > 0 && window.braveryPotion == 0;
			var recipe = document.getElementById('brewing-braveryPotion');
			if (recipe)
			{
				recipe.style.display = show ? '' : 'none';
			}
		});
	}

	function fixHitText()
	{
		window.scrollTextHitSplat = function (icon, color, text, elId, cbType)
		{
			var imgTag = icon != 'none' ? "<img src=\"images/" + icon + "\" class=\"image-icon-50\" />" : '';
			var elementChosen = document.getElementById(elId);
			if (!elementChosen)
			{
				return;
			}
			var rect = elementChosen.getBoundingClientRect();
			var xCoord = (rect.left + rect.right) / 2;
			var yCoord = (rect.bottom + rect.top) / 2;
			var extraStyle = '';
			if (cbType == 'melee')
			{
				extraStyle = 'border: 1px solid red; background-color: #4d0000;';
			}
			else if (cbType == 'heal')
			{
				extraStyle = 'border: 1px solid green; background-color: lime;';
			}
			var $elementToAppend = window.$("<div class=\"scroller\" style=\"" + extraStyle + " color: " + color + "; position: fixed;\">" + imgTag + text + "</div>").appendTo('body');
			if (xCoord == 0 && yCoord == 0)
			{
				var tab = document.getElementById('tab-container-bar-combat');
				var tabRect = tab.getBoundingClientRect();
				var boxRect = $elementToAppend.get(0).getBoundingClientRect();
				xCoord = elId == 'img-hero' ? (tabRect.left - boxRect.width) : tabRect.right;
				yCoord = tabRect.top;
			}
			$elementToAppend
				.css(
				{
					left: xCoord
					, top: yCoord
				})
				.animate(
				{
					top: '-=50px'
				}, function ()
				{
					return $elementToAppend.fadeOut(1000, function ()
					{
						return $elementToAppend.remove();
					});
				});
		};
	}

	function fixBoatTooltips()
	{
		var boatBox = document.getElementById('item-box-boundRowBoat');
		var boatTooltip = boatBox && document.getElementById(boatBox.dataset.tooltipId || '');
		var canoeBox = document.getElementById('item-box-boundCanoe');
		if (!boatBox || !canoeBox || !boatTooltip)
		{
			return;
		}
		var canoeTooltip = boatTooltip.cloneNode(true);
		canoeTooltip.id = 'tooltip-boundCanoe';
		var header = canoeTooltip.firstElementChild;
		header.textContent = 'Canoe';
		boatTooltip.parentElement.appendChild(canoeTooltip);
		canoeBox.dataset.tooltipId = 'tooltip-boundCanoe';
		boatTooltip.appendChild(document.createElement('br'));
		var boatDuration = document.createElement('span');
		boatDuration.innerHTML = '<strong>Trip duration:</strong> 3 hours';
		boatTooltip.appendChild(boatDuration);
		canoeTooltip.appendChild(document.createElement('br'));
		var canoeDuration = document.createElement('span');
		canoeDuration.innerHTML = '<strong>Trip duration:</strong> 6 hours';
		canoeTooltip.appendChild(canoeDuration);
	}

	function fixAlignments()
	{
		addStyle("\nspan.item-box[id^=\"item-box-\"] > img:not(.image-icon-100),\nspan.item-box[id^=\"item-box-\"] > span > img\n{\n\tmargin-top: -2px;\n}\n\n#tab-container-crafting .settings-container\n{\n\tmargin: 5px 30px;\n}\n#table-crafting-recipe,\n#table-brewing-recipe,\n#table-magic-recipe\n{\n\twidth: calc(100% - 2*20px - 2*10px);\n}\n#tab-sub-container-magic-items\n{\n\tmargin: 5px 0px;\n}\n#table-magic-recipe\n{\n\twidth: calc(100% - 2*10px);\n}\n\n#tab-container-farming\n{\n\tpadding: 0 20px;\n}\n#tab-sub-container-farming\n{\n\tmargin: 5px 0;\n\tmargin-bottom: -10px;\n}\ndiv.farming-patch,\ndiv.farming-patch-locked\n{\n\tmargin: 10px;\n}\n\n#combat-table-area\n{\n\tborder-spacing: 0;\n}\n#combat-table-area > tbody > tr > td\n{\n\tvertical-align: top;\n}\ndiv#hero-area.hero,\ndiv#monster-area.monster\n{\n\tmargin-left: 20px;\n\tmargin-right: 20px;\n\tmargin-top: 10px;\n}\ntable.table-hero-stats,\ndiv.hp-bar,\n#hero-area div.fight-spellbook\n{\n\tmargin-left: 0;\n}\ndiv.hp-bar\n{\n\tmin-width: calc(100% - 2px);\n}\n#hero-area span.fight-spell\n{\n\tmargin-bottom: 0;\n\tmargin-top: 0;\n}\n#hero-area span.fight-spell:first-child\n{\n\tmargin-left: 0;\n}\n#hero-area span.fight-spell:last-child\n{\n\tmargin-right: 0;\n}\n#hero-area > div:last-child,\n.imageMonster\n{\n\theight: 556px !important;\n\tmargin-top: -50px;\n}\n#monster-area div.hp-bar\n{\n\tmargin-top: 66px;\n\tmargin-bottom: 74px;\n}\n#monster-area > br:first-child,\n#monster-area table.table-hero-stats + br\n{\n\tdisplay: none;\n}\n.imageMonster\n{\n\talign-items: flex-end;\n\tdisplay: flex;\n\tposition: relative;\n}\n#combat-table-area[style$=\"auto;\"]\n{\n\tborder-color: transparent;\n}\n#img-monster\n{\n\tposition: absolute;\n}\n#img-monster[src$=\"/1.png\"]\n{\n\theight: 250px;\n}\n#img-monster[src$=\"/2.png\"]\n{\n\ttransform: translateY(30px);\n}\n#img-monster[src$=\"/3.png\"]\n{\n\theight: 180px;\n\ttransform: translateY(-350px);\n}\n#img-monster[src$=\"/4.png\"]\n{\n\theight: 180px;\n}\n#img-monster[src$=\"/5.png\"]\n{\n\theight: 700px;\n\ttransform: translateY(130px);\n}\n#img-monster[src$=\"/7.png\"]\n{\n\theight: 450px;\n\ttransform: translateY(30px);\n}\n#img-monster[src$=\"/8.png\"]\n{\n\theight: 280px;\n\ttransform: translateY(-260px);\n}\n#img-monster[src$=\"/9.png\"]\n{\n\theight: 450px;\n\ttransform: translateY(-10px);\n}\n#img-monster[src$=\"/11.png\"],\n#img-monster[src$=\"/15.png\"]\n{\n\ttransform: translateY(-180px);\n}\n#img-monster[src$=\"/14.png\"]\n{\n\theight: 500px;\n\tmargin-left: -50px;\n\tmargin-right: -50px;\n}\n#img-monster[src$=\"/100.png\"]\n{\n\theight: 300px;\n}\n#img-monster[src$=\"/101.png\"]\n{\n\ttransform: translateY(-10px);\n}\n#tab-sub-container-combat > .large-button > .image-icon-50\n{\n\theight: 70px;\n\tmargin-top: -10px;\n\twidth: 70px;\n}\n#combat-table-area span.large-button,\n#combat-table-area span.medium-button\n{\n\tmargin: 10px;\n}\n#combat-table-area span.large-button\n{\n\tfont-size: 3rem;\n}\n#combat-table-area span.medium-button + br + br\n{\n\tdisplay: none;\n}\n\t\t");
	}

	function addHeroStatTooltips()
	{
		var table = document.querySelector('#hero-area table.table-hero-stats');
		if (!table)
		{
			return;
		}
		var statRow = table.rows.item(0);
		var attackCell = statRow.cells.item(0);
		attackCell.title = 'Attack Damage';
		window.$(attackCell).tooltip();
		var accuracyCell = statRow.cells.item(1);
		accuracyCell.title = 'Attack Accuracy';
		window.$(accuracyCell).tooltip();
		var speedCell = statRow.cells.item(2);
		speedCell.title = 'Attack Speed';
		window.$(speedCell).tooltip();
		var defenseCell = statRow.cells.item(3);
		defenseCell.title = 'Defense';
		window.$(defenseCell).tooltip();
	}

	function unifyTooltips()
	{
		function getLastNonEmptyChild(parent)
		{
			for (var i = parent.childNodes.length - 1; i >= 0; i--)
			{
				var child = parent.childNodes.item(i);
				if (child.nodeType === Node.TEXT_NODE
					&& (child.textContent || '').trim() !== '')
				{
					return null;
				}
				else if (child.nodeType === Node.ELEMENT_NODE)
				{
					return child;
				}
			}
			return null;
		}
		// clean unnecessary br-tags in tooltips
		var tooltips = document.querySelectorAll('#tooltip-list > div[id^="tooltip-"]');
		for (var i = 0; i < tooltips.length; i++)
		{
			var tooltip = tooltips[i];
			var lneChild = void 0;
			while ((lneChild = getLastNonEmptyChild(tooltip)) && lneChild.tagName == 'BR')
			{
				tooltip.removeChild(lneChild);
			}
		}

		function getTooltip(item)
		{
			return document.getElementById('tooltip-' + item);
		}
		var boldify = [
			'oilBarrel'
			, 'boundEmptyPickaxe'
			, 'boundEmptyShovel'
			, 'boundEmptyChisel'
			, 'ashes'
			, 'iceBones'
		];
		var lastDotRegex = /\.\s*$/;
		for (var _i = 0, boldify_1 = boldify; _i < boldify_1.length; _i++)
		{
			var item = boldify_1[_i];
			var tooltip = getTooltip(item);
			if (!tooltip)
			{
				continue;
			}
			var textNode = tooltip.lastChild;
			while (textNode && (textNode.nodeType != Node.TEXT_NODE || (textNode.textContent || '').trim() === ''))
			{
				textNode = textNode.previousSibling;
			}
			if (!textNode)
			{
				continue;
			}
			var text = textNode.textContent || '';
			var split = text.split(/\.(?=\s*\S+)/);
			var clickText = split[split.length - 1];
			textNode.textContent = text.replace(clickText, '');
			if (split.length > 1)
			{
				tooltip.appendChild(document.createElement('br'));
				tooltip.appendChild(document.createElement('br'));
			}
			var boldText = document.createElement('b');
			boldText.textContent = clickText;
			tooltip.appendChild(boldText);
		}

		function prepareTooltip(item, editText, createOnMissing)
		{
			if (createOnMissing === void 0)
			{
				createOnMissing = false;
			}
			var tooltip = getTooltip(item);
			if (!tooltip)
			{
				return;
			}
			// try to find the b-node:
			var bNode = getLastNonEmptyChild(tooltip);
			if (bNode && bNode.tagName === 'SPAN')
			{
				bNode = getLastNonEmptyChild(bNode);
			}
			if (!bNode || bNode.tagName !== 'B')
			{
				if (!createOnMissing)
				{
					bNode = null;
				}
				else
				{
					tooltip.appendChild(document.createElement('br'));
					tooltip.appendChild(document.createElement('br'));
					bNode = document.createElement('b');
					tooltip.appendChild(bNode);
				}
			}
			if (bNode)
			{
				bNode.textContent = editText(bNode);
			}
		}
		// remove dots
		for (var i = 0; i < tooltips.length; i++)
		{
			var item = tooltips.item(i).id.replace(/^tooltip-/, '');
			prepareTooltip(item, function (bNode)
			{
				var text = bNode.textContent || '';
				if (/Click to /.test(text))
				{
					return text.replace(lastDotRegex, '');
				}
				return text;
			});
		}
		// add click texts
		function setText(item, text)
		{
			prepareTooltip(item, function ()
			{
				return text;
			}, true);
		}
		for (var _a = 0, FURNACE_LEVELS_1 = FURNACE_LEVELS; _a < FURNACE_LEVELS_1.length; _a++)
		{
			var furnaceLevel = FURNACE_LEVELS_1[_a];
			var furnaceItem = getBoundKey(furnaceLevel + 'Furnace');
			setText(furnaceItem, 'Click to operate');
			var ovenItem = getBoundKey(furnaceLevel + 'Oven');
			setText(ovenItem, 'Click to operate');
		}
		// fix tooltip of quests-book
		var questBookTooltip = getTooltip('quests-book');
		if (questBookTooltip)
		{
			var childNodes = questBookTooltip.childNodes;
			for (var i = 0; i < childNodes.length; i++)
			{
				var node = childNodes[i];
				if (node.nodeType === Node.TEXT_NODE
					&& (node.textContent || '').indexOf('Click to see a list of quests.') > -1)
				{
					var next = node.nextSibling;
					if (next)
					{
						questBookTooltip.removeChild(next);
					}
					questBookTooltip.removeChild(node);
				}
			}
		}
		// fix tooltip of axe
		var axeTooltip = getTooltip('boundEmptyAxe');
		if (axeTooltip)
		{
			axeTooltip.insertBefore(document.createElement('br'), axeTooltip.lastElementChild);
		}
		var texts = {
			'quests-book': 'Click to see a list of quests'
			, 'rake': 'Click to upgrade your rake'
			, 'boundBoat': 'Click to send boat'
			, 'boundCanoe': 'Click to send boat'
		};
		for (var item in texts)
		{
			setText(item, texts[item]);
		}
	}
	var cached = {
		scrollWidth: 0
		, scrollHeight: 0
	};

	function changeTooltipPosition(event)
	{
		var tooltipX = event.pageX - 8;
		var tooltipY = event.pageY + 8;
		var el = document.querySelector('body > div.tooltip');
		if (!el)
		{
			return;
		}
		if (!this)
		{
			// init
			cached.scrollWidth = document.body.scrollWidth;
			cached.scrollHeight = document.body.scrollHeight;
		}
		var rect = el.getBoundingClientRect();
		var css = {
			left: tooltipX
			, top: tooltipY
			, width: ''
			, height: ''
			, maxWidth: cached.scrollWidth
			, maxHeight: cached.scrollHeight
		};
		var diffX = cached.scrollWidth - 20 - tooltipX - rect.width;
		if (diffX < 0)
		{
			css.left += diffX;
			css.width = rect.width - 42;
		}
		var diffY = cached.scrollHeight - 20 - tooltipY - rect.height;
		if (diffY < 0)
		{
			css.top += diffY;
			css.height = rect.height - 22;
		}
		window.$(el).css(css);
	}

	function fixTooltipPositioning()
	{
		window.changeTooltipPosition = changeTooltipPosition;
		window.loadTooltips();
	}
	// fix tooltips of some food items
	function fixTooltips()
	{
		var foodTooltipTemplate = document.getElementById('tooltip-uncookedBread');
		if (!foodTooltipTemplate)
		{
			return;
		}

		function ensureTooltip(item, tooltipTemplate, onCreate)
		{
			var tooltipId = 'tooltip-' + item;
			if (document.getElementById(tooltipId) != null)
			{
				return;
			}
			var newTooltip = tooltipTemplate.cloneNode(true);
			newTooltip.id = tooltipId;
			onCreate(newTooltip);
			tooltipTemplate.parentElement.appendChild(newTooltip);
		}

		function ensureFoodTooltip(key, title, energyAmount)
		{
			ensureTooltip(key, foodTooltipTemplate, function (newTooltip)
			{
				var spans = newTooltip.getElementsByTagName('span');
				var titleSpan = spans[0];
				var energyNode = spans[1].firstChild;
				titleSpan.textContent = title;
				energyNode.textContent = '+'
					+ (typeof energyAmount === 'string' ? energyAmount : format.number(energyAmount))
					+ ' ';
			});
		}
		ensureFoodTooltip('uncookedCarrotCake', 'Uncooked Carrot Cake', window.getEnergyGained('carrotCake'));
		ensureFoodTooltip('carrotCake', 'Carrot Cake', window.getEnergyGained('carrotCake'));
	}

	function fixCombatNavigation()
	{
		var backBtns = document.querySelectorAll('span.medium-button[onclick*="openTab(\'combat\')"]');
		for (var i = 0; i < backBtns.length; i++)
		{
			var btn = backBtns.item(i);
			var img = btn.firstElementChild;
			var textNode = btn.lastChild;
			if (!img || img.tagName != 'IMG' || !textNode)
			{
				continue;
			}
			img.className = img.className.replace(/(-\d+)-b/, '$1');
			textNode.textContent = ' back';
		}
	}

	function init()
	{
		fixClientGameLoop();
		fixScroller();
		fixTooltipStyle();
		fixRefreshingMagicRecipes();
		moveStrangeLeafs();
		fixTreasureMap();
		fixWoodcutting();
		fixQuestBook();
		// apply fix for scroll images later to fix images in this code too
		fixHitText();
		fixScrollImages();
		fixQuest8BraveryRecipe();
		fixBoatTooltips();
		fixAlignments();
		addHeroStatTooltips();
		unifyTooltips();
		fixTooltipPositioning();
		fixTooltips();
		fixCombatNavigation();
	}
	temporaryFixes.init = init;
})(temporaryFixes || (temporaryFixes = {}));

/**
 * improve timer
 */
var timer;
(function (timer)
{
	timer.name = 'timer';

	function bindNewFormatter()
	{
		function doBind()
		{
			window.formatTime = window.formatTimeShort = window.formatTimeShort2 = function (seconds)
			{
				return format.timer(seconds);
			};
		}
		window.addEventListener('load', function ()
		{
			return setTimeout(function ()
			{
				return doBind();
			}, 100);
		});
		doBind();
		setTimeout(function ()
		{
			return doBind();
		}, 100);
	}

	function improveSmeltingTimer()
	{
		var TIMER_CLASS = 'timer';
		var REMAINING_CLASS = 'smelting-remaining';
		addStyle("\n#notif-smelting\n{\n\tposition: relative;\n}\n#notif-smelting > span:not(." + TIMER_CLASS + "):not(." + REMAINING_CLASS + ")\n{\n\tdisplay: none;\n}\n#notif-smelting > span." + REMAINING_CLASS + "\n{\n\tposition: absolute;\n\tleft: 10px;\n\tfont-size: 0.9rem;\n\tbottom: 0px;\n\twidth: 50px;\n\ttext-align: right\n}\n#notif-smelting > span." + REMAINING_CLASS + "::before\n{\n\tcontent: '\\0D7';\n\tmargin-right: .25rem;\n\tmargin-left: -.5rem;\n}\n\t\t");
		var smeltingNotifBox = document.getElementById('notif-smelting');
		var smeltingTimerEl = document.createElement('span');
		smeltingTimerEl.className = TIMER_CLASS;
		smeltingNotifBox.appendChild(smeltingTimerEl);
		var remainingBarsEl = document.createElement('span');
		remainingBarsEl.className = REMAINING_CLASS;
		smeltingNotifBox.appendChild(remainingBarsEl);
		var delta = 0;

		function updatePercValues(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			updateSmeltingTimer(delta = 0);
			if (init)
			{
				observer.add('smeltingPercD', function ()
				{
					return updatePercValues();
				});
				observer.add('smeltingPerc', function ()
				{
					return updatePercValues();
				});
			}
		}

		function updateSmeltingTimer(delta)
		{
			if (delta === void 0)
			{
				delta = 0;
			}
			var totalTime = window.smeltingPercD;
			// thanks at /u/marcus898 for your bug report
			var elapsedTime = Math.round(window.smeltingPerc * totalTime / 100) + delta;
			smeltingTimerEl.textContent = format.timer(Math.max(totalTime - elapsedTime, 0));
			remainingBarsEl.textContent = (window.smeltingTotalAmount - window.smeltingAmount).toString();
		}
		observer.addTick(function ()
		{
			return updateSmeltingTimer(delta++);
		});
		updatePercValues(true);
	}

	function improveTimer(cssRulePrefix, textColor, timerColor, infoIdPrefx, containerPrefix, updateFn)
	{
		addStyle("\n/* hide built in timer elements */\n" + cssRulePrefix + " > *:not(img):not(.info)\n{\n\tdisplay: none;\n}\n" + cssRulePrefix + " > div.info\n{\n\tcolor: " + textColor + ";\n\tmargin-top: 5px;\n\tpointer-events: none;\n\ttext-align: center;\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n}\n" + cssRulePrefix + " > div.info > div.name\n{\n\tfont-size: 1.2rem;\n}\n" + cssRulePrefix + " > div.info > div.timer\n{\n\tcolor: " + timerColor + ";\n}\n\t\t");
		for (var i = 0; i < 6; i++)
		{
			var num = i + 1;
			var infoId = infoIdPrefx + num;
			var container = document.getElementById(containerPrefix + num);
			container.style.position = 'relative';
			var infoEl = document.createElement('div');
			infoEl.className = 'info';
			infoEl.id = infoId;
			infoEl.innerHTML = "<div class=\"name\"></div><div class=\"timer\"></div>";
			container.appendChild(infoEl);
			updateFn(num, infoId, true);
		}
	}

	function updateTreeInfo(placeId, infoElId, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var infoEl = document.getElementById(infoElId);
		var nameEl = infoEl.firstElementChild;
		var timerEl = infoEl.lastElementChild;
		var idKey = 'treeId' + placeId;
		var growTimerKey = 'treeGrowTimer' + placeId;
		var lockedKey = 'treeUnlocked' + placeId;
		var treeId = getGameValue(idKey);
		if (treeId == 0)
		{
			var isLocked = placeId > 4 && getGameValue(lockedKey) == 0;
			nameEl.textContent = isLocked ? 'Locked' : 'Empty';
			timerEl.textContent = '';
		}
		else
		{
			nameEl.textContent = key2Name(window.getTreeName(treeId)) || 'Unknown Tree';
			var remainingTime = window.TREE_GROW_TIME[treeId - 1] - getGameValue(growTimerKey);
			timerEl.textContent = remainingTime > 0 ? '(' + format.timer(remainingTime) + ')' : 'Fully grown';
		}
		if (init)
		{
			observer.add([idKey, growTimerKey, lockedKey], function ()
			{
				return updateTreeInfo(placeId, infoElId, false);
			});
		}
	}
	// add tree grow timer
	function improveTreeGrowTimer()
	{
		improveTimer('.woodcutting-tree', 'white', 'yellow', 'wc-tree-info-', 'wc-div-tree-', updateTreeInfo);
	}

	function updatePatchInfo(patchId, infoElId, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var infoEl = document.getElementById(infoElId);
		var nameEl = infoEl.querySelector('.name');
		var timerEl = infoEl.querySelector('.timer');
		var idKey = 'farmingPatchSeed' + patchId;
		var growTimeKey = 'farmingPatchGrowTime' + patchId;
		var timerKey = 'farmingPatchTimer' + patchId;
		var stageKey = 'farmingPatchStage' + patchId;
		var stage = getGameValue(stageKey);
		var seedName = PLANT_NAME[getGameValue(idKey)] || 'Unkown Plant';
		if (stage == 0)
		{
			var isLocked = patchId > 4 && window.donorFarmingPatch == 0;
			nameEl.textContent = isLocked ? 'Locked' : 'Click to grow';
			timerEl.textContent = '';
		}
		else if (stage >= 4)
		{
			nameEl.textContent = stage > 4 ? 'Dead Plant' : seedName;
			timerEl.textContent = stage > 4 ? 'Click to remove' : 'Click to harvest';
		}
		else
		{
			nameEl.textContent = seedName;
			var remainingTime = getGameValue(growTimeKey) - getGameValue(timerKey);
			timerEl.textContent = '(' + format.timer(remainingTime) + ')';
		}
		if (init)
		{
			observer.add([idKey, timerKey, stageKey, 'donorFarmingPatch'], function ()
			{
				return updatePatchInfo(patchId, infoElId, false);
			});
		}
	}
	// add seed name and change color of timer
	function getSoonestTreeTimer()
	{
		if (window.treeStage1 == 4
			|| window.treeStage2 == 4
			|| window.treeStage3 == 4
			|| window.treeStage4 == 4
			|| window.treeStage5 == 4
			|| window.treeStage6 == 4)
		{
			return -1;
		}
		var minTimer = null;
		for (var i = 1; i <= 6; i++)
		{
			var treeId = getGameValue('treeId' + i);
			var unlocked = getGameValue('treeUnlocked' + i) == 1;
			var timerValue = getGameValue('treeGrowTimer' + i);
			if (unlocked && treeId !== 0 && timerValue > 0)
			{
				var remainingTime = window.TREE_GROW_TIME[treeId - 1] - timerValue;
				minTimer = minTimer === null ? remainingTime : Math.min(minTimer, remainingTime);
			}
		}
		return minTimer || 0;
	}

	function getSoonestFarmingTimer()
	{
		if (window.farmingPatchStage1 == 0 || window.farmingPatchStage1 == 4
			|| window.farmingPatchStage2 == 0 || window.farmingPatchStage2 == 4
			|| window.farmingPatchStage3 == 0 || window.farmingPatchStage3 == 4
			|| window.farmingPatchStage4 == 0 || window.farmingPatchStage4 == 4
			|| window.donorFarmingPatch != 0 && (window.farmingPatchStage5 == 0 || window.farmingPatchStage5 == 4
				|| window.farmingPatchStage6 == 0 || window.farmingPatchStage6 == 4))
		{
			return -1;
		}
		var minTimer = null;
		for (var i = 1; i <= (window.donorFarmingPatch ? 6 : 4); i++)
		{
			var remainingTimer = getGameValue('farmingPatchGrowTime' + i) - getGameValue('farmingPatchTimer' + i);
			minTimer = minTimer === null ? remainingTimer : Math.min(minTimer, remainingTimer);
		}
		return minTimer || 0;
	}

	function improveSeedGrowTimer()
	{
		improveTimer('div[id^="farming-patch-area-"]', 'black', 'blue', 'farming-patch-info-', 'farming-patch-area-', updatePatchInfo);
	}

	function addTabTimer()
	{
		var TAB_TIMER_KEY = 'tabTimer';
		addStyle("\ntable.tab-bar td\n{\n\tposition: relative;\n}\n." + TAB_TIMER_KEY + " table.tab-bar td.ready > img:first-child\n{\n\tborder: 5px solid #48ab32;\n\tmargin: -5px;\n}\ntable.tab-bar td .info\n{\n\tcolor: yellow;\n\tdisplay: none;\n\tfont-size: 0.8rem;\n\tpadding-left: 50px;\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\ttext-align: center;\n}\n." + TAB_TIMER_KEY + " table.tab-bar td .info\n{\n\tdisplay: block;\n}\ntable.tab-bar td .info.timer\n{\n\tbottom: 0;\n\tpadding-bottom: 5px;\n}\ntable.tab-bar td .info.timer:not(:empty)::before\n{\n\tcontent: '(';\n}\ntable.tab-bar td .info.timer:not(:empty)::after\n{\n\tcontent: ')';\n}\ntable.tab-bar td .info.extra\n{\n\tcolor: white;\n\tpadding-top: 5px;\n\ttop: 0;\n}\n." + TAB_TIMER_KEY + " #dhqol-notif-woodcutting,\n." + TAB_TIMER_KEY + " #dhqol-notif-farming,\n." + TAB_TIMER_KEY + " #dhqol-notif-combat,\n." + TAB_TIMER_KEY + " #dhqol-notif-vial\n{\n\tdisplay: none !important;\n}\n\t\t");

		function getTabEl(key)
		{
			return document.getElementById('tab-container-bar-' + key);
		}

		function addInfoDiv(key)
		{
			var infoDiv = document.createElement('div');
			infoDiv.className = 'info';
			var tab = getTabEl(key);
			if (tab)
			{
				tab.appendChild(infoDiv);
			}
			return infoDiv;
		}

		function createTabTimer(key, timerFn)
		{
			var tab = getTabEl(key);
			var timerDiv = addInfoDiv(key);
			if (!tab || !timerDiv)
			{
				return;
			}
			timerDiv.classList.add('timer');

			function updateTimer()
			{
				var minTimer = timerFn();
				if (tab)
				{
					tab.classList[minTimer == -1 ? 'add' : 'remove']('ready');
				}
				timerDiv.textContent = minTimer <= 0 ? '' : format.timer(minTimer);
			}
			updateTimer();
			observer.addTick(function ()
			{
				return updateTimer();
			});
		}
		createTabTimer('woodcutting', getSoonestTreeTimer);
		createTabTimer('farming', getSoonestFarmingTimer);
		createTabTimer('combat', function ()
		{
			return window.combatGlobalCooldown;
		});
		var energyDiv = addInfoDiv('combat');
		energyDiv.classList.add('extra');

		function updateEnergy()
		{
			energyDiv.innerHTML = '<img src="images/steak.png" class="image-icon-15"> ' + format.number(window.energy);
		}
		updateEnergy();
		observer.add('energy', function ()
		{
			return updateEnergy();
		});
		// add highlight for stardust potions
		var potionDiv = addInfoDiv('brewing');
		potionDiv.classList.add('extra');
		var potionList = ['stardustPotion', 'superStardustPotion'];
		var potionImageList = [];

		function updatePotion(key, img, init)
		{
			if (init === void 0)
			{
				init = false;
			}
			var timerKey = key + 'Timer';
			var show = getGameValue(key) > 0 && getGameValue(timerKey) === 0;
			img.style.display = show ? '' : 'none';
			if (init)
			{
				observer.add(key, function ()
				{
					return updatePotion(key, img);
				});
				observer.add(timerKey, function ()
				{
					return updatePotion(key, img);
				});
			}
		}
		for (var i = 0; i < potionList.length; i++)
		{
			var key = potionList[i];
			var img = document.createElement('img');
			img.src = 'images/' + key + '.png';
			img.className = 'image-icon-15';
			potionImageList[i] = img;
			potionDiv.appendChild(img);
			updatePotion(key, img, true);
		}

		function updateVisibility()
		{
			document.body.classList[settings.get(settings.KEY.showTabTimer) ? 'add' : 'remove'](TAB_TIMER_KEY);
		}
		updateVisibility();
		settings.observe(settings.KEY.showTabTimer, function ()
		{
			return updateVisibility();
		});
	}

	function addOilInfo()
	{
		var NULL_TYPE = 'null';
		var PLUS_TYPE = 'plus';
		var MINUS_TYPE = 'minus';
		addStyle("\ntable.top-bar span[id^=\"dh2qol-oil\"]\n{\n\tdisplay: none;\n}\n#oil-flow-net\n{\n\tcolor: hsla(195, 100%, 50%, 1);\n\tfont-weight: bold;\n}\n#oil-flow-net[data-type=\"" + NULL_TYPE + "\"]\n{\n\tcolor: hsla(195, 100%, 50%, 1);\n}\n#oil-flow-net[data-type=\"" + PLUS_TYPE + "\"]\n{\n\tcolor: green;\n}\n#oil-flow-net[data-type=\"" + MINUS_TYPE + "\"]\n{\n\tcolor: red;\n}\n#oil-flow-net-timer[data-type=\"" + NULL_TYPE + "\"]\n{\n\tdisplay: none;\n}\n#oil-flow-net-timer[data-type=\"" + PLUS_TYPE + "\"]\n{\n\tcolor: yellow;\n}\n#oil-flow-net-timer[data-type=\"" + MINUS_TYPE + "\"]\n{\n\tcolor: orange;\n}\n\t\t");
		var oilFlow = document.getElementById('oil-flow-values');
		var parent = oilFlow && oilFlow.parentElement;
		if (!oilFlow || !parent)
		{
			return;
		}
		var netFlow = document.createElement('span');
		netFlow.id = 'oil-flow-net';
		parent.insertBefore(netFlow, oilFlow);
		var next = oilFlow.nextElementSibling;
		var netTimer = document.createElement('span');
		netTimer.id = 'oil-flow-net-timer';
		if (next)
		{
			parent.insertBefore(netTimer, next);
		}
		else
		{
			parent.appendChild(netTimer);
		}
		var oilNet;
		var oilNetType;

		function updateNetFlow(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			oilNet = window.oilIn - window.oilOut;
			oilNetType = oilNet === 0 ? NULL_TYPE : (oilNet > 0 ? PLUS_TYPE : MINUS_TYPE);
			netFlow.dataset.type = oilNetType;
			var sign = oilNet === 0 ? PLUS_MINUS_SIGN : (oilNet > 0 ? '+' : '');
			netFlow.textContent = sign + oilNet;
			if (init)
			{
				observer.add('oilIn', function ()
				{
					return updateNetFlow();
				});
				observer.add('oilOut', function ()
				{
					return updateNetFlow();
				});
			}
			updateFullTimer(init);
		}

		function updateFullTimer(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			netTimer.dataset.type = oilNetType;
			var time = 0;
			if (oilNet > 0)
			{
				netTimer.title = 'full in...';
				var diff = window.maxOil - window.oil;
				time = diff / oilNet;
			}
			else if (oilNet < 0)
			{
				netTimer.title = 'empty in...';
				time = window.oil / Math.abs(oilNet);
			}
			netTimer.textContent = '(' + format.timer(Math.ceil(time)) + ')';
			if (init)
			{
				observer.add('maxOil', function ()
				{
					return updateFullTimer();
				});
				observer.add('oil', function ()
				{
					return updateFullTimer();
				});
				observer.addTick(function ()
				{
					return updateFullTimer();
				});
			}
		}
		updateNetFlow(true);
	}

	function init()
	{
		bindNewFormatter();
		improveSmeltingTimer();
		improveTreeGrowTimer();
		improveSeedGrowTimer();
		addTabTimer();
		addOilInfo();
	}
	timer.init = init;
})(timer || (timer = {}));

/**
 * improve smelting dialog
 */
var smelting;
(function (smelting)
{
	smelting.name = 'smelting';
	var TIME_NEEDED_ID = 'smelting-time-needed';
	var LAST_SMELTING_AMOUNT_KEY = 'lastSmeltingAmount';
	var LAST_SMELTING_BAR_KEY = 'lastSmeltingBar';
	var smeltingValue = null;
	var amountInput;

	function prepareAmountInput()
	{
		amountInput = document.getElementById('input-smelt-bars-amount');
		amountInput.type = 'number';
		amountInput.min = '0';
		amountInput.step = '5';

		function onValueChange()
		{
			smeltingValue = null;
			window.selectBar('', null, amountInput, document.getElementById('smelting-furnace-capacity').value);
		}
		amountInput.addEventListener('mouseup', onValueChange);
		amountInput.addEventListener('keyup', onValueChange);
		amountInput.setAttribute('onkeyup', '');
	}

	function setBarCap(bar, capacity)
	{
		if (bar == '')
		{
			bar = window.selectedBar;
		}
		var requirements = SMELTING_REQUIREMENTS[bar];
		var maxAmount = parseInt(capacity, 10);
		for (var key in requirements)
		{
			var req = requirements[key];
			maxAmount = Math.min(Math.floor(getGameValue(key) / req), maxAmount);
		}
		var value = parseInt(amountInput.value, 10);
		if (value > maxAmount)
		{
			smeltingValue = value;
			amountInput.value = maxAmount.toString();
		}
		else if (smeltingValue != null)
		{
			amountInput.value = Math.min(smeltingValue, maxAmount).toString();
			if (smeltingValue <= maxAmount)
			{
				smeltingValue = null;
			}
		}
	}

	function prepareTimeNeeded()
	{
		var neededMatsEl = document.getElementById('dialogue-furnace-mats-needed');
		var parent = neededMatsEl && neededMatsEl.parentElement;
		if (!neededMatsEl || !parent)
		{
			return;
		}
		var br = document.createElement('br');
		var timeBox = document.createElement('div');
		timeBox.className = 'basic-smallbox';
		timeBox.innerHTML = "<img src=\"images/icons/hourglass.png\" class=\"image-icon-30\">\n\t\tDuration: <span id=\"" + TIME_NEEDED_ID + "\"></span>";
		var next = neededMatsEl.nextElementSibling;
		parent.insertBefore(br, next);
		parent.insertBefore(timeBox, next);
	}

	function updateTimeNeeded(value)
	{
		var timeEl = document.getElementById(TIME_NEEDED_ID);
		if (!timeEl)
		{
			return;
		}
		var num = parseInt(value, 10);
		var timePerBar = window.getTimerPerBar(window.selectedBar);
		timeEl.textContent = format.timer(timePerBar * num);
	}

	function init()
	{
		prepareAmountInput();
		prepareTimeNeeded();
		var _selectBar = window.selectBar;
		var updateSmeltingRequirements = function (bar, inputElement, inputBarsAmountEl, capacity)
		{
			_selectBar(bar, inputElement, inputBarsAmountEl, capacity);
			updateTimeNeeded(inputBarsAmountEl.value);
		};
		window.selectBar = function (bar, inputElement, inputBarsAmountEl, capacity)
		{
			setBarCap(bar, capacity);
			// save selected bar
			if (bar != '')
			{
				store.set(LAST_SMELTING_BAR_KEY, bar);
			}
			// save amount
			store.set(LAST_SMELTING_AMOUNT_KEY, inputBarsAmountEl.value);
			updateSmeltingRequirements(bar, inputElement, inputBarsAmountEl, capacity);
		};
		var lastBar = store.get(LAST_SMELTING_BAR_KEY);
		var lastAmount = store.get(LAST_SMELTING_AMOUNT_KEY);
		var _openFurnaceDialogue = window.openFurnaceDialogue;
		window.openFurnaceDialogue = function (furnace)
		{
			var capacity = window.getFurnaceCapacity(furnace);
			if (window.smeltingBarType == 0)
			{
				amountInput.max = capacity.toString();
			}
			// restore amount
			var inputBarsAmountEl = document.getElementById('input-smelt-bars-amount');
			if (inputBarsAmountEl && inputBarsAmountEl.value == '-1' && lastAmount != null)
			{
				inputBarsAmountEl.value = lastAmount;
			}
			_openFurnaceDialogue(furnace);
			// restore selected bar
			if ((!window.selectedBar || window.selectedBar == 'none') && lastBar != null)
			{
				window.selectedBar = lastBar;
			}
			// update whether requirements are fulfilled
			var barInputId = 'input-furnace-' + split2Words(window.selectedBar, '-').toLowerCase();
			var inputElement = document.getElementById(barInputId);
			if (inputElement && inputBarsAmountEl)
			{
				updateSmeltingRequirements(window.selectedBar, inputElement, inputBarsAmountEl, capacity.toString());
			}
		};
	}
	smelting.init = init;
})(smelting || (smelting = {}));

/**
 * add chance to time calculator
 */
var fishingInfo;
(function (fishingInfo)
{
	fishingInfo.name = 'fishingInfo';
	/**
	 * 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
		var tooltipEl = ensureTooltip('chance-' + elId, targetEl);
		// set elements content
		var percValues = [1, 10, 20, 50, 80, 90, 99];
		var percRows = '';
		for (var _i = 0, percValues_1 = percValues; _i < percValues_1.length; _i++)
		{
			var p = percValues_1[_i];
			percRows += "\n\t\t\t\t<tr>\n\t\t\t\t\t<td>" + p + "%</td>\n\t\t\t\t\t<td>" + format.time2NearestUnit(calcSecondsTillP(chancePerSecond, p), true) + "</td>\n\t\t\t\t</tr>";
		}
		tooltipEl.innerHTML = "<h2>" + headline + "</h2>\n\t\t\t<table class=\"chance\">\n\t\t\t\t<tr>\n\t\t\t\t\t<th>Probability</th>\n\t\t\t\t\t<th>Time</th>\n\t\t\t\t</tr>\n\t\t\t\t" + percRows + "\n\t\t\t</table>\n\t\t";
	}

	function addChanceStyle()
	{
		addStyle("\ntable.chance\n{\n\tborder-spacing: 0;\n}\ntable.chance th\n{\n\tborder-bottom: 1px solid gray;\n}\ntable.chance td:first-child\n{\n\tborder-right: 1px solid gray;\n\ttext-align: center;\n}\ntable.chance th,\ntable.chance td\n{\n\tpadding: 4px 8px;\n}\ntable.chance tr:nth-child(2n) td\n{\n\tbackground-color: white;\n}\n\t\t");
	}

	function addXp()
	{
		var table = document.querySelector('#dialogue-id-fishingRod table');
		if (!table)
		{
			return;
		}
		var rows = table.rows;
		for (var i = 0; i < rows.length; i++)
		{
			var row = rows.item(i);
			if (row.classList.contains('xp-added'))
			{
				continue;
			}
			if (i == 0)
			{
				var xpCell = document.createElement('th');
				xpCell.textContent = 'XP';
				row.appendChild(xpCell);
			}
			else
			{
				var cell = row.insertCell(-1);
				var rawFish = row.id.replace('dialogue-fishing-rod-tr-', '');
				var xp = FISH_XP[rawFish];
				cell.textContent = xp == null ? '?' : format.number(xp);
			}
			row.classList.add('xp-added');
		}
	}

	function chance2TimeCalculator()
	{
		var table = document.querySelector('#dialogue-id-fishingRod table');
		if (!table)
		{
			return;
		}
		var rows = table.rows;
		for (var i = 1; i < rows.length; i++)
		{
			var row = rows.item(i);
			var rawFish = row.id.replace('dialogue-fishing-rod-tr-', '');
			var fish = rawFish.replace('raw', '').toLowerCase();
			if (!rawFish || !fish)
			{
				continue;
			}
			var chanceCell = row.cells.item(4);
			var chance = (chanceCell.textContent || '')
				.replace(/[^\d\/]/g, '')
				.split('/')
				.reduce(function (p, c)
				{
					return p / parseInt(c, 10);
				}, 1);
			addChanceTooltip("One raw " + fish + " at least every:", chance, rawFish, row);
		}
	}

	function init()
	{
		addChanceStyle();
		var _clicksShovel = window.clicksShovel;
		window.clicksShovel = function ()
		{
			_clicksShovel();
			var shovelChance = document.getElementById('dialogue-shovel-chance');
			var titleEl = shovelChance.parentElement;
			var chance = 1 / window.getChanceOfDiggingSand();
			addChanceTooltip('One sand at least every:', chance, 'shovel', titleEl);
		};
		// depends on fishingXp
		var _clicksFishingRod = window.clicksFishingRod;
		window.clicksFishingRod = function ()
		{
			_clicksFishingRod();
			addXp();
			chance2TimeCalculator();
		};
	}
	fishingInfo.init = init;
})(fishingInfo || (fishingInfo = {}));

/**
 * add tooltips for recipes
 */
var recipeTooltips;
(function (recipeTooltips)
{
	recipeTooltips.name = 'recipeTooltips';

	function updateRecipeTooltips(recipeKey, recipes)
	{
		var table = document.getElementById('table-' + recipeKey + '-recipe');
		var rows = table.rows;

		function recipe2Title(recipe)
		{
			return recipe.recipe
				.map(function (name, i)
				{
					return format.number(recipe.recipeCost[i]) + String.fromCharCode(160)
						+ split2Words(name).toLowerCase();
				})
				.join(' + ');
		};
		for (var i = 1; i < rows.length; i++)
		{
			var row = rows.item(i);
			var key = row.id.replace(recipeKey + '-', '');
			var recipe = recipes[key];
			var requirementCell = row.cells.item(3);
			requirementCell.title = recipe2Title(recipe);
			window.$(requirementCell).tooltip();
		}
	}

	function updateTooltipsOnReinitRecipes(key)
	{
		var capitalKey = capitalize(key);
		var processKey = 'process' + capitalKey + 'Tab';
		var _processTab = window[processKey];
		window[processKey] = function ()
		{
			var reinit = !!getGameValue('refreshLoad' + capitalKey + 'Table');
			_processTab();
			if (reinit)
			{
				updateRecipeTooltips(key, getGameValue(key + 'Recipes'));
			}
		};
	}

	function init()
	{
		updateTooltipsOnReinitRecipes('crafting');
		updateTooltipsOnReinitRecipes('brewing');
		updateTooltipsOnReinitRecipes('magic');
		updateTooltipsOnReinitRecipes('cooksBook');
	}
	recipeTooltips.init = init;
})(recipeTooltips || (recipeTooltips = {}));

/**
 * fix formatting of numbers
 */
var fixNumbers;
(function (fixNumbers)
{
	fixNumbers.name = 'fixNumbers';

	function prepareRecipeForTable(recipe)
	{
		// create a copy of the recipe to prevent requirement check from failing
		var newRecipe = JSON.parse(JSON.stringify(recipe));
		newRecipe.recipeCost = recipe.recipeCost.map(function (cost)
		{
			return format.number(cost);
		});
		newRecipe.description = format.numbersInText(newRecipe.description);
		newRecipe.xp = format.number(recipe.xp);
		return newRecipe;
	}

	function init()
	{
		var _addRecipeToBrewingTable = window.addRecipeToBrewingTable;
		window.addRecipeToBrewingTable = function (brewingRecipe)
		{
			_addRecipeToBrewingTable(prepareRecipeForTable(brewingRecipe));
		};
		var _addRecipeToMagicTable = window.addRecipeToMagicTable;
		window.addRecipeToMagicTable = function (magicRecipe)
		{
			_addRecipeToMagicTable(prepareRecipeForTable(magicRecipe));
		};
		var _addRecipeToCooksBookTable = window.addRecipeToCooksBookTable;
		window.addRecipeToCooksBookTable = function (cooksBookRecipe)
		{
			_addRecipeToCooksBookTable(prepareRecipeForTable(cooksBookRecipe));
		};
		var tooltipList = document.querySelectorAll('#tooltip-list div[id^="tooltip-"][id$="Seeds"]');
		for (var i = 0; i < tooltipList.length; i++)
		{
			var tooltip = tooltipList[i];
			tooltip.innerHTML = format.numbersInText(tooltip.innerHTML);
		}
		var fightEnergyCells = document.querySelectorAll('#dialogue-fight tr > td:nth-child(4)');
		for (var i = 0; i < fightEnergyCells.length; i++)
		{
			var cell = fightEnergyCells[i];
			cell.innerHTML = format.numbersInText(cell.innerHTML);
		}
	}
	fixNumbers.init = init;
})(fixNumbers || (fixNumbers = {}));

/**
 * add slider for machines
 */
var machineDialog;
(function (machineDialog)
{
	machineDialog.name = 'machineDialog';
	var $slider;

	function createSlider()
	{
		var br = document.querySelector('#dialogue-machinery-current-total ~ br');
		var parent = br && br.parentElement;
		if (!br || !parent)
		{
			return;
		}
		addStyle("\n#dialogue-id-boundMachinery .ui-slider\n{\n\tmargin: 10px 5px;\n}\n#dialogue-id-boundMachinery .ui-slider:not([data-owned=\"10\"])::after\n{\n\tbackground: hsla(0, 0%, 0%, 1);\n\tborder: 1px solid #c5c5c5;\n\tborder-radius: 3px;\n\tborder-top-left-radius: 0;\n\tborder-bottom-left-radius: 0;\n\tcontent: '';\n\tmargin-left: -3px;\n\tpadding-left: 3px;\n\theight: 100%;\n\twidth: 0%;\n\tposition: absolute;\n\tleft: 100%;\n\ttop: -1px;\n}\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"9\"] { width: calc((100% - 10px - 2px) / 10*9); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"9\"]::after { width: calc(100% / 9 * 1); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"8\"] { width: calc((100% - 10px - 2px) / 10*8); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"8\"]::after { width: calc(100% / 8 * 2); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"7\"] { width: calc((100% - 10px - 2px) / 10*7); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"7\"]::after { width: calc(100% / 7 * 3); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"6\"] { width: calc((100% - 10px - 2px) / 10*6); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"6\"]::after { width: calc(100% / 6 * 4); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"5\"] { width: calc((100% - 10px - 2px) / 10*5); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"5\"]::after { width: calc(100% / 5 * 5); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"4\"] { width: calc((100% - 10px - 2px) / 10*4); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"4\"]::after { width: calc(100% / 4 * 6); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"3\"] { width: calc((100% - 10px - 2px) / 10*3); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"3\"]::after { width: calc(100% / 3 * 7); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"2\"] { width: calc((100% - 10px - 2px) / 10*2); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"2\"]::after { width: calc(100% / 2 * 8); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"1\"] { width: calc((100% - 10px - 2px) / 10*1); }\n#dialogue-id-boundMachinery .ui-slider[data-owned=\"1\"]::after { width: calc(100% / 1 * 9); }\n\t\t");
		var slider = document.createElement('div');
		parent.insertBefore(slider, br);
		$slider = window.$(slider)
			.slider(
			{
				range: 'max'
				, min: 0
				, max: 10
				, value: 0
				, slide: function (event, ui)
				{
					return updateValue(ui.value);
				}
			});
		// hide br and up/down arrows
		br.style.display = 'none';
		var arrows = document.querySelectorAll('input[onclick^="turnOn("]');
		for (var i = 0; i < arrows.length; i++)
		{
			arrows[i].style.display = 'none';
		}
		var els = document.querySelectorAll('[onclick*="openMachineryDialogue("]');
		var boundMachineKeyList = [];
		for (var i = 0; i < els.length; i++)
		{
			var match = els[i].id.match(/openMachineryDialogue\('(.+?)'\)/);
			if (match)
			{
				boundMachineKeyList.push(getBoundKey(match[1]));
			}
		}
		observer.add(boundMachineKeyList, function ()
		{
			return updateMax();
		});
	}

	function updateMax()
	{
		var machineEl = document.getElementById('dialogue-machinery-chosen');
		if (machineEl && machineEl.value != '')
		{
			var boundMachineKey = getBoundKey(machineEl.value);
			var ownedNum = getGameValue(boundMachineKey);
			$slider.slider('option', 'max', ownedNum);
			$slider.get(0).dataset.owned = ownedNum.toString();
		}
	}

	function updateValue(value)
	{
		var typeEl = document.getElementById('dialogue-machinery-chosen');
		var numEl = document.getElementById('dialogue-machinery-current-on');
		if (numEl && typeEl)
		{
			var valueBefore = parseInt(numEl.textContent || '0', 10);
			var machine = typeEl.value;
			var increment = valueBefore < value;
			var diff = Math.abs(valueBefore - value);
			for (var i = 0; i < diff; i++)
			{
				window.turnOn(machine, increment);
			}
		}
	}

	function init()
	{
		createSlider();
		var _openMachineryDialogue = window.openMachineryDialogue;
		window.openMachineryDialogue = function (machineType)
		{
			_openMachineryDialogue(machineType);
			updateMax();
			$slider.slider('value', getGameValue(machineType + 'On'));
		};
	}
	machineDialog.init = init;
})(machineDialog || (machineDialog = {}));

/**
 * improve behaviour of amount inputs
 */
var amountInputs;
(function (amountInputs)
{
	amountInputs.name = 'amountInputs';

	function getMax(recipe)
	{
		var max = Number.MAX_SAFE_INTEGER;
		for (var i = 0; i < recipe.recipe.length; i++)
		{
			max = Math.min(max, Math.floor(getGameValue(recipe.recipe[i]) / recipe.recipeCost[i]));
		}
		return max;
	}

	function setAmount(id, recipeCollection, key)
	{
		var numInput = document.getElementById(id);
		if (!numInput)
		{
			return;
		}
		var max = getMax(recipeCollection[key]);
		numInput.value = max.toString();
		numInput.select();
	}

	function init()
	{
		var _multiCraft = window.multiCraft;
		window.multiCraft = function (item)
		{
			_multiCraft(item);
			setAmount('dialogue-multicraft-input', window.craftingRecipes, item);
		};
		var _brew = window.brew;
		window.brew = function (potion)
		{
			_brew(potion);
			setAmount('dialogue-brewing-input', window.brewingRecipes, potion);
		};
		var _cooksBookInputDialogue = window.cooksBookInputDialogue;
		window.cooksBookInputDialogue = function (food)
		{
			_cooksBookInputDialogue(food);
			setAmount('dialogue-cooksBook-input', window.cooksBookRecipes, food);
		};
	}
	amountInputs.init = init;
})(amountInputs || (amountInputs = {}));

/**
 * improves the top bar
 */
var newTopbar;
(function (newTopbar)
{
	newTopbar.name = 'newTopbar';
	var linkCell, tabCell, infoCell;
	var addQueues = {
		link: []
		, tab: []
		, info: []
	};

	function addLinkEntry(el)
	{
		if (!linkCell)
		{
			addQueues.link.push(el);
		}
		else
		{
			linkCell.appendChild(document.createTextNode('|'));
			linkCell.appendChild(el);
		}
	}
	newTopbar.addLinkEntry = addLinkEntry;

	function addTabEntry(el)
	{
		if (!tabCell)
		{
			addQueues.tab.push(el);
		}
		else
		{
			tabCell.appendChild(document.createTextNode('|'));
			tabCell.appendChild(el);
		}
	}
	newTopbar.addTabEntry = addTabEntry;

	function addInfoEntry(el)
	{
		if (!infoCell)
		{
			addQueues.info.push(el);
		}
		else
		{
			infoCell.appendChild(document.createTextNode('|'));
			infoCell.appendChild(el);
		}
	}
	newTopbar.addInfoEntry = addInfoEntry;

	function init()
	{
		addStyle("\ntable.top-links,\ntable.top-links *\n{\n\tpadding: 0;\n}\ntable.top-links td > *\n{\n\tdisplay: inline-block;\n\tpadding: 2px 6px;\n}\n\t\t");
		var table = document.querySelector('table.top-links');
		if (!table)
		{
			return;
		}
		var row = table.rows.item(0);
		var cells = row.cells;
		var tabIdx = [2, 4];
		var infoIdx = [5, 6];
		var newRow = table.insertRow(-1);
		linkCell = newRow.insertCell(-1);
		tabCell = newRow.insertCell(-1);
		tabCell.style.textAlign = 'center';
		infoCell = newRow.insertCell(-1);
		infoCell.style.textAlign = 'right';
		for (var i = 0; i < cells.length; i++)
		{
			var container = linkCell;
			if (tabIdx.indexOf(i) != -1)
			{
				container = tabCell;
			}
			else if (infoIdx.indexOf(i) != -1)
			{
				container = infoCell;
			}
			var cell = cells.item(i);
			var el = cell.firstElementChild;
			if (cell.childNodes.length > 1)
			{
				el = document.createElement('span');
				el.style.color = 'yellow';
				while (cell.childNodes.length > 0)
				{
					el.appendChild(cell.childNodes[0]);
				}
			}
			if (container.children.length > 0)
			{
				container.appendChild(document.createTextNode('|'));
			}
			if (el)
			{
				container.appendChild(el);
			}
		}
		var parent = row.parentElement;
		if (parent)
		{
			parent.removeChild(row);
		}
		for (var _i = 0, _a = addQueues.link; _i < _a.length; _i++)
		{
			var el = _a[_i];
			addLinkEntry(el);
		}
		for (var _b = 0, _c = addQueues.tab; _b < _c.length; _b++)
		{
			var el = _c[_b];
			addTabEntry(el);
		}
		for (var _d = 0, _e = addQueues.info; _d < _e.length; _d++)
		{
			var el = _e[_d];
			addInfoEntry(el);
		}
		var _openTab = window.openTab;
		window.openTab = function (newTab)
		{
			var oldTab = window.currentOpenTab;
			_openTab(newTab);
			var children = tabCell.children;
			for (var i = 0; i < children.length; i++)
			{
				var el = children[i];
				var match = (el.getAttribute('onclick') || '').match(/openTab\('([^']+)'\)/);
				if (!match)
				{
					continue;
				}
				var tab = match[1];
				if (oldTab == tab)
				{
					el.style.color = '';
				}
				if (newTab == tab)
				{
					el.style.color = 'white';
				}
			}
		};
	}
	newTopbar.init = init;
})(newTopbar || (newTopbar = {}));

/**
 * style tweaks
 */
var styleTweaks;
(function (styleTweaks)
{
	styleTweaks.name = 'styleTweaks';

	function addTweakStyle(setting, style)
	{
		if (setting != '')
		{
			var prefix = setting == '' ? '' : 'body.' + setting;
			style = style.replace(/(^\s*|,\s*|\}\s*)([^\{\},]+)(?=,|\s*\{)/g, '$1' + prefix + ' $2');
			document.body.classList.add(setting);
		}
		addStyle(style, setting != '' ? setting : null);
	}
	// tweak oil production/consumption
	function tweakOil()
	{
		addTweakStyle('tweak-oil', "\nspan#oil-flow-values\n{\n\tmargin-left: .5em;\n\tpadding-left: 2rem;\n\tposition: relative;\n}\n#oil-flow-values > span:nth-child(-n+2)\n{\n\tfont-size: 0px;\n\tposition: absolute;\n\tleft: 0;\n\ttop: -0.75rem;\n\tvisibility: hidden;\n}\n#oil-flow-values > span:nth-child(-n+2) > span\n{\n\tfont-size: 1rem;\n\tvisibility: visible;\n}\n#oil-flow-values > span:nth-child(2)\n{\n\ttop: 0.75rem;\n}\n#oil-flow-values span[data-item-display=\"oilIn\"]::before\n{\n\tcontent: '+';\n}\n#oil-flow-values span[data-item-display=\"oilOut\"]::before\n{\n\tcontent: '-';\n}\n\t\t");
		// make room for oil cell on small devices
		var oilFlowValues = document.getElementById('oil-flow-values');
		var oilFlowCell = oilFlowValues.parentElement;
		oilFlowCell.style.width = '30%';
	}

	function tweakSelection()
	{
		addTweakStyle('no-select', "\ntable.tab-bar,\nspan.item-box,\ndiv.farming-patch,\ndiv.farming-patch-locked,\ndiv#tab-sub-container-combat > span,\ntable.top-links a,\n#hero-area > div:last-child\n{\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n\t\t");
	}
	// tweak stardust monitor of DH2QoL to keep it in place
	function tweakStardust()
	{
		addTweakStyle('dh2qol', "\n#dh2qol-stardustMonitor\n{\n\tdisplay: inline-block;\n\tmargin-left: .25rem;\n\ttext-align: left;\n\twidth: 2.5rem;\n}\n\t\t");
	}

	function tweakSkillLevelText()
	{
		addTweakStyle('', "\ndiv.skill-xp-label\n{\n\ttext-shadow: white 0px 0px 0.5rem;\n}\n\t\t");
	}

	function tweakFightDialog()
	{
		addTweakStyle('smaller-fight-dialog', "\n#dialogue-fight img[width=\"150px\"]\n{\n\twidth: 120px;\n\theight: 50px;\n}\n#dialogue-fight img[src=\"images/icons/combat.png\"] ~ br\n{\n\tdisplay: none;\n}\n\t\t");
	}

	function addAdditionalSkillBars()
	{
		var _loadSkillTabs = window.loadSkillTabs;
		window.loadSkillTabs = function ()
		{
			_loadSkillTabs();
			for (var _i = 0, SKILL_LIST_1 = SKILL_LIST; _i < SKILL_LIST_1.length; _i++)
			{
				var skill = SKILL_LIST_1[_i];
				var unlocked = getGameValue(skill + 'Unlocked') == 1;
				if (!unlocked)
				{
					continue;
				}
				var xp = getGameValue(skill + 'Xp');
				var currentLevelXp = window.getXpNeeded(window.getLevel(xp));
				var nextLevelXp = window.getXpNeeded(window.getLevel(xp) + 1);
				var perc = (xp - currentLevelXp) / (nextLevelXp - currentLevelXp) * 100;
				var progress = document.getElementById('skill-progress-' + skill);
				if (progress)
				{
					progress.style.width = perc + '%';
				}
			}
		};
		// init additional skill bars
		addStyle("\ntd[id^=\"top-bar-level-td-\"]\n{\n\tposition: relative;\n}\n#top-bar-levels .skill-bar\n{\n\tbackground-color: grey;\n\theight: 5px;\n\tposition: absolute;\n\tbottom: 5px;\n\tleft: 60px;\n\tright: 10px;\n}\n#top-bar-levels .skill-bar > .skill-progress\n{\n\tbackground-color: rgb(51, 204, 51);\n\theight: 100%;\n\twidth: 0%;\n}\n\t\t");
		for (var _i = 0, SKILL_LIST_2 = SKILL_LIST; _i < SKILL_LIST_2.length; _i++)
		{
			var skill = SKILL_LIST_2[_i];
			var cell = document.getElementById('top-bar-level-td-' + skill);
			if (!cell)
			{
				continue;
			}
			var levelBar = document.createElement('div');
			levelBar.className = 'skill-bar';
			var progress = document.createElement('div');
			progress.id = 'skill-progress-' + skill;
			progress.className = 'skill-progress';
			levelBar.appendChild(progress);
			cell.appendChild(levelBar);
			// update skill level progress bars on click
			levelBar.addEventListener('click', function ()
			{
				return window.loadSkillTabs();
			});
		}
		window.loadSkillTabs();
	}
	// highlight cooking level requirement when not matched
	function highlightCookinglevel()
	{
		var _cookFoodDialogue = window.cookFoodDialogue;
		window.cookFoodDialogue = function (rawFood)
		{
			_cookFoodDialogue(rawFood);
			var dialog = document.getElementById('dialogue-id-cook-food');
			if (!dialog)
			{
				return;
			}
			var levelReq = document.getElementById('dialogue-cook-levelReq');
			var levelReqLabel = levelReq && levelReq.previousElementSibling;
			if (!levelReq || !levelReqLabel)
			{
				return;
			}
			var fulfilled = window.getCookingLevelReq(rawFood) > window.getLevel(window.cookingXp);
			levelReq.style.color = fulfilled ? 'rgb(204, 0, 0)' : '';
			levelReq.style.fontWeight = fulfilled ? 'bold' : '';
			levelReqLabel.style.color = fulfilled ? 'rgb(204, 0, 0)' : '';
		};
	}

	function amountStyle()
	{
		var tweakName = 'amount-symbol';
		addTweakStyle(tweakName, "\n.item-box:not(#item-box-special-case-questsUnlocked):not(#item-box-pirate):not(#item-box-miner):not(#item-box-boundPumpjacks):not([onclick^=\"openMachineryDialogue(\"]):not(#item-box-sandCollectorsQuest):not(#item-box-boundFilledBonemealBin) > span[data-item-display]::before\n{\n\tcontent: '\\0D7';\n\tmargin-right: .25rem;\n\tmargin-left: -.5rem;\n}\n\t\t");

		function setAmountSymbolVisibility(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			var show = settings.get(settings.KEY.amountSymbol);
			document.body.classList[show ? 'add' : 'remove'](tweakName);
			if (init)
			{
				settings.observe(settings.KEY.amountSymbol, function ()
				{
					return setAmountSymbolVisibility();
				});
			}
		}
		setAmountSymbolVisibility(true);
	}

	function efficiency()
	{
		var EFFICIENCY_CLASS = 'efficiency';
		addTweakStyle(EFFICIENCY_CLASS, "\ndiv.tab-container > h1.container-title:first-child\n{\n\tdisplay: none;\n}\n#game-div > table.top-bar\n{\n\tmargin-top: 0;\n}\n#game-div > table.top-bar,\n#game-div > table.tab-bar,\n#div-chat\n{\n\tmargin-top: 0;\n}\n#game-div > table.top-bar\n{\n\tborder-top-width: 0px;\n}\nspan.item-box\n{\n\tmargin: 2px;\n}\ndiv.item-box-area\n{\n\tmargin: 5px 0;\n}\n#tab-sub-container-farming\n{\n\tmargin: 5px 8px;\n}\n#game-div > div.tab-container\n{\n\tpadding: 0;\n}\ndiv.tab-container > center > table.table-default,\n#table-crafting-recipe,\n#table-brewing-recipe,\n#table-magic-recipe\n{\n\twidth: 100%;\n}\nul.settings-container,\n#tab-container-crafting .settings-container\n{\n\tmargin: 0;\n}\n#tab-sub-container-crafting + br,\n#tab-sub-container-woodcutting + br,\n#tab-sub-container-farming + br,\n#tab-sub-container-brewing + br,\n#tab-container-shop > br:last-child\n{\n\tdisplay: none;\n}\n\ndiv.side-by-side > div\n{\n\tmargin: 0 !important;\n\twidth: 50%;\n}\n.side-by-side h1.container-title\n{\n\tmargin: 2px;\n}\n.side-by-side h1.container-title + br,\n.side-by-side h1.container-title + br + br,\n.side-by-side input[type=\"image\"] + br,\n#hiscores-table-ingame + br\n{\n\tdisplay: none;\n}\n\ndiv.farming-patch,\ndiv.farming-patch-locked\n{\n\tmargin: 0;\n}\n\n#combat-table-area span.large-button,\n#combat-table-area span.medium-button\n{\n\tmargin: 2px;\n}\n#combat-loot-tables > table.hiscores-table\n{\n\tmargin: 2px;\n\twidth: calc(33.33% - 4px);\n}\n#combat-loot-tables > div[style*=\"both\"]\n{\n\theight: 0px;\n}\n\t\t");

		function checkSetting(init)
		{
			if (init === void 0)
			{
				init = false;
			}
			var show = settings.get(settings.KEY.useEfficiencyStyle);
			document.body.classList[show ? 'add' : 'remove'](EFFICIENCY_CLASS);
			if (init)
			{
				settings.observe(settings.KEY.useEfficiencyStyle, function ()
				{
					return checkSetting();
				});
			}
		}
		checkSetting(true);
	}

	function init()
	{
		tweakOil();
		tweakSelection();
		tweakStardust();
		tweakSkillLevelText();
		tweakFightDialog();
		addAdditionalSkillBars();
		highlightCookinglevel();
		amountStyle();
		efficiency();
	}
	styleTweaks.init = init;
})(styleTweaks || (styleTweaks = {}));

/**
 * add ingame notification boxes
 */
var notifBoxes;
(function (notifBoxes)
{
	notifBoxes.name = 'notifBoxes';

	function addNotifBox(imageKey, itemKey, showFront)
	{
		if (itemKey === void 0)
		{
			itemKey = null;
		}
		if (showFront === void 0)
		{
			showFront = false;
		}
		var notifBox = document.createElement('span');
		notifBox.className = 'notif-box';
		notifBox.id = 'notif-' + imageKey;
		notifBox.style.display = 'none';
		if (showFront)
		{
			notifBox.style.cssFloat = 'left';
		}
		notifBox.innerHTML = "<img src=\"images/" + imageKey + ".png\" class=\"image-icon-50\" id=\"notif-" + imageKey + "-img\">";
		if (itemKey != null)
		{
			notifBox.innerHTML += "<span data-item-display=\"" + itemKey + "\" style=\"margin-left: 10px;\"></span>";
		}
		var notifArea = document.getElementById('notifaction-area');
		if (notifArea)
		{
			notifArea.appendChild(notifBox);
		}
		return notifBox;
	}

	function addWorker()
	{
		var notifBox = addNotifBox('workers', null, true);

		function setVisibility()
		{
			var show = window.workersTimer === 1;
			notifBox.style.display = show ? '' : 'none';
		}
		setVisibility();
		observer.add('workersTimer', function ()
		{
			return setVisibility();
		});
	}

	function init()
	{
		addWorker();
	}
	notifBoxes.init = init;
})(notifBoxes || (notifBoxes = {}));

/**
 * extend market
 */
var market;
(function (market)
{
	market.name = 'market';
	var detectedTedsUIOnce = false;

	function detectTedsUI()
	{
		return detectedTedsUIOnce = detectedTedsUIOnce || typeof window.changeSetting === 'function';
	}

	function showOfferCancelCooldown()
	{
		if (detectTedsUI())
		{
			return;
		}
		addStyle("\n.market-slot-cancel:not([data-cooldown=\"0\"])\n{\n\tbackground: linear-gradient(hsla(12, 40%, 50%, 1), hsla(12, 40%, 40%, 1));\n\tcursor: not-allowed;\n\tposition: relative;\n}\n.market-slot-cancel:not([data-cooldown=\"0\"]):hover\n{\n\tbackground-color: hsla(0, 40%, 50%, 1);\n}\n.market-slot-cancel:not([data-cooldown=\"0\"])::after\n{\n\tcontent: attr(data-cooldown);\n\tposition: absolute;\n\tright: 10px;\n}\n\t\t");

		function slotCooldown(i, init)
		{
			if (init === void 0)
			{
				init = false;
			}
			var cooldownKey = 'marketCancelCooldownSlot' + i;
			var btn = document.getElementById('market-slot-' + i + '-cancel-btn');
			if (btn)
			{
				btn.dataset.cooldown = detectTedsUI() ? '0' : getGameValue(cooldownKey).toString();
			}
			if (init)
			{
				observer.add(cooldownKey, function ()
				{
					return slotCooldown(i);
				});
			}
		}
		for (var i = 1; i <= 3; i++)
		{
			slotCooldown(i, true);
		}
	}

	function init()
	{
		showOfferCancelCooldown();
	}
	market.init = init;
})(market || (market = {}));

var combat;
(function (combat)
{
	combat.name = 'combat';
	var LOOT_TABLE_URL = '/wiki/combat.php';
	var COMBAT_LOOT_TABLES_ID = 'combat-loot-tables';
	var CAT_2_NAME = {
		'always': 'Always'
		, 'common': 'Common'
		, 'uncommon': 'Uncommon'
		, 'rare': 'Rare'
		, 'veryrare': 'Very Rare'
	};
	var lootInfo = {};

	function readLootTable(table)
	{
		var monsterImg = table.getElementsByTagName('img').item(0);
		var src = monsterImg.getAttribute('src') || '';
		var monsterId = src.replace(/.+npc\/(\d+)\.png$/, '$1');
		var info = {
			always: []
			, common: []
			, uncommon: []
			, rare: []
			, veryrare: []
		};
		for (var i = 2; i < table.rows.length; i++)
		{
			var row = table.rows.item(i);
			var match = row.cells.item(0).innerHTML.match(/images\/(.+)\.png/);
			if (!match)
			{
				console.error('no item key found:', row.innerHTML);
				continue;
			}
			var itemKey = match[1];
			var amount = row.cells.item(1).textContent || '';
			var rarityCategory = row.cells.item(2).className;
			if (!info.hasOwnProperty(rarityCategory))
			{
				console.error('unknown rarity category:', rarityCategory);
				continue;
			}
			info[rarityCategory].push(
			{
				key: itemKey
				, amount: amount.split(' - ').map(function (s)
				{
					return Number(s.replace(/\D/g, ''));
				})
			});
		}
		lootInfo[monsterId] = info;
	}

	function updateLootTableInfo()
	{
		return doGet(LOOT_TABLE_URL)
			.then(function (response)
			{
				var parser = new DOMParser();
				var doc = parser.parseFromString(response, 'text/html');
				var tables = doc.getElementsByTagName('table');
				for (var i = 0; i < tables.length; i++)
				{
					readLootTable(tables.item(i));
				}
				return lootInfo;
			})
			.then(function (info)
			{
				setLootTableTabContent(info);
			});
	}

	function addLootTableTab()
	{
		var subTabContainer = document.getElementById('tab-sub-container-combat');
		var itemContainer = document.getElementById('tab-sub-container-combat-large-btns');
		var afterEl = itemContainer && itemContainer.previousElementSibling;
		if (!subTabContainer || !afterEl)
		{
			return;
		}
		addStyle("\n#item-box-combatDropTable\n{\n\tdisplay: none !important;\n}\nspan.medium-button.active\n{\n\tbackground: hsla(109, 55%, 43%, 1);\n\tcursor: not-allowed;\n}\n#combat-table-area:not([style$=\"auto;\"]) > tbody > tr > td:last-child\n{\n\twidth: 100%;\n}\n#" + COMBAT_LOOT_TABLES_ID + " td.always\n{\n\tbackground-color: #ccffff;\n}\n#" + COMBAT_LOOT_TABLES_ID + " td.common\n{\n\tbackground-color: #ccffcc;\n}\n#" + COMBAT_LOOT_TABLES_ID + " td.uncommon\n{\n\tbackground-color: #ffffcc;\n}\n#" + COMBAT_LOOT_TABLES_ID + " td.rare\n{\n\tbackground-color: #ffcc99;\n}\n#" + COMBAT_LOOT_TABLES_ID + " td.veryrare\n{\n\tbackground-color: #ff9999;\n}\n\n#" + COMBAT_LOOT_TABLES_ID + " table.hiscores-table\n{\n\tfloat: left;\n\tmargin-left: 30px;\n\twidth: 30%;\n}\n\t\t");
		var REFRESH_LOOT_TABLE_ID = 'refresh-loot-table';
		var subTab = document.createElement('span');
		subTab.setAttribute('onclick', "openSubTab('loot')");
		subTab.className = 'large-button';
		subTab.innerHTML = "<img class=\"image-icon-50\" src=\"images/combatDropTable.png\" style=\"filter: grayscale(100%);\">Loot";
		subTabContainer.insertBefore(subTab, afterEl);
		var combatSubTab = document.getElementById('tab-sub-container-combat');
		var equipSubTab = document.getElementById('tab-sub-container-equip');
		var spellsSubTab = document.getElementById('tab-sub-container-spells');
		var subPanelContainer = combatSubTab.parentElement;
		var lootSubTab = document.createElement('div');
		lootSubTab.id = 'tab-sub-container-loot';
		lootSubTab.style.display = 'none';
		lootSubTab.innerHTML = "<span onclick=\"openTab('combat')\" class=\"medium-button\"><img class=\"image-icon-30\" src=\"images/icons/back.png\"> back</span>\n\t\t<span id=\"" + REFRESH_LOOT_TABLE_ID + "\" class=\"medium-button\">refresh</span>\n\t\t<div id=\"" + COMBAT_LOOT_TABLES_ID + "\"></div>";
		subPanelContainer.appendChild(lootSubTab);
		var refreshBtn = document.getElementById(REFRESH_LOOT_TABLE_ID);
		if (refreshBtn)
		{
			refreshBtn.addEventListener('click', function ()
			{
				if (refreshBtn.classList.contains('active'))
				{
					return;
				}
				refreshBtn.classList.add('active');
				updateLootTableInfo()
					.then(function ()
					{
						return refreshBtn.classList.remove('active');
					})
					.catch(function ()
					{
						return refreshBtn.classList.remove('active');
					});
			});
		}
		var _openSubTab = window.openSubTab;
		window.openSubTab = function (tab)
		{
			combatSubTab.style.display = 'none';
			equipSubTab.style.display = 'none';
			spellsSubTab.style.display = 'none';
			lootSubTab.style.display = 'none';
			_openSubTab(tab);
			if (tab == 'loot')
			{
				lootSubTab.style.display = 'block';
			}
		};
		var _loadDefaultCombatTab = window.loadDefaultCombatTab;
		window.loadDefaultCombatTab = function ()
		{
			_loadDefaultCombatTab();
			lootSubTab.style.display = 'none';
		};
	}

	function setLootTableTabContent(lootInfo)
	{
		var combatTableWrapper = document.getElementById(COMBAT_LOOT_TABLES_ID);
		if (!combatTableWrapper)
		{
			return;
		}
		combatTableWrapper.innerHTML = "";
		for (var monsterId in lootInfo)
		{
			var info = lootInfo[monsterId];
			var monsterNum = Number(monsterId);
			if (monsterNum > 1 && monsterNum % 3 === 1)
			{
				var lineBreak = document.createElement('div');
				lineBreak.style.clear = 'both';
				lineBreak.innerHTML = "<br>";
				combatTableWrapper.appendChild(lineBreak);
			}
			var table = document.createElement('table');
			table.className = 'hiscores-table';
			var imgRow = table.insertRow(-1);
			imgRow.innerHTML = "<td colspan=\"3\">\n\t\t\t\t<img src=\"../images/hero/npc/" + monsterId + ".png\" class=\"image-icon-50\">\n\t\t\t</td>";
			var headerRow = table.insertRow(-1);
			headerRow.innerHTML = "<th>Item</th><th>Amount</th><th>Rarity</th>";
			for (var rarityCategory in info)
			{
				var itemList = info[rarityCategory];
				for (var i = 0; i < itemList.length; i++)
				{
					var item = itemList[i];
					var row = table.insertRow(-1);
					row.innerHTML = "<td><img src=\"../images/" + item.key + ".png\" class=\"image-icon-40\"></td><td>" + item.amount.map(function (n)
					{
						return format.number(n);
					}).join(' - ') + "</td><td class=\"" + rarityCategory + "\">" + CAT_2_NAME[rarityCategory] + "</td>";
				}
			}
			combatTableWrapper.appendChild(table);
		}
	}

	function init()
	{
		if (!settings.get(settings.KEY.showLootTab))
		{
			return;
		}
		addLootTableTab();
		updateLootTableInfo();
	}
	combat.init = init;
})(combat || (combat = {}));

/**
 * init
 */
var scriptInitialized = false;

function init()
{
	console.info('[%s] "DH2 Fixed %s" up and running.', (new Date).toLocaleTimeString(), version);
	scriptInitialized = true;
	var initModules = [
		settings
		, notifications
		, log
		, gameEvents
		, temporaryFixes
		, crafting
		, itemBoxes
		, chat
		, timer
		, smelting
		, fishingInfo
		, recipeTooltips
		, fixNumbers
		, machineDialog
		, amountInputs
		, newTopbar
		, styleTweaks
		, notifBoxes
		, market
		, combat
	];
	window.initModules = initModules;
	for (var _i = 0, initModules_1 = initModules; _i < initModules_1.length; _i++)
	{
		var module = initModules_1[_i];
		try
		{
			module.init();
		}
		catch (error)
		{
			console.error('Error during initialization in module "' + module.name + '":', error);
		}
	}
}
document.addEventListener('DOMContentLoaded', function ()
{
	var oldValues = new Map();
	var _doCommand = window.doCommand;
	window.doCommand = function (data)
	{
		if (data.startsWith('REFRESH_ITEMS='))
		{
			oldValues = new Map();
			for (var _i = 0, _a = window.jsItemArray; _i < _a.length; _i++)
			{
				var key = _a[_i];
				oldValues.set(key, getGameValue(key));
			}
			_doCommand(data);
			if (!scriptInitialized)
			{
				init();
			}
			return;
		}
		else if (!scriptInitialized)
		{
			if (data.startsWith('CHAT='))
			{
				var parts = data.substr(5).split('~');
				return chat.newAddToChatBox(parts[0], parts[1], parts[2], parts[3], 0);
			}
			else if (data.startsWith('PM='))
			{
				return chat.newAddToChatBox(window.username, '0', '0', data.substr(3), 1);
			}
		}
		var ret = commands.process(data);
		if (ret === void 0)
		{
			ret = _doCommand(commands.formatData(data));
		}
		return ret;
	};
	var _refreshItemValues = window.refreshItemValues;
	window.refreshItemValues = function (itemKeyList, firstLoad)
	{
		_refreshItemValues(itemKeyList, firstLoad);
		for (var _i = 0, itemKeyList_1 = itemKeyList; _i < itemKeyList_1.length; _i++)
		{
			var key = itemKeyList_1[_i];
			observer.notify(key, oldValues.get(key));
		}
		observer.notifyTick();
	};
});

/**
 * fix web socket errors
 */
var main;
(function (main)
{
	var WS_TIMEOUT_SEC = 30;
	var WS_TIMEOUT_CODE = 3000;
	var WS_OPEN_TIMEOUT_SEC = 2 * 60; // 2 minutes
	// reload the page after 5 consecutive reconnect attempts without successfully opening the websocket once
	var MAX_RECONNECTS = 5;

	function webSocketLoaded(event)
	{
		if (window.webSocket == null)
		{
			console.error('WebSocket instance not initialized!');
			return;
		}
		// cache old event listener
		var _onClose = window.webSocket.onclose;
		var _onError = window.webSocket.onerror;
		var _onMessage = window.webSocket.onmessage;
		var _onOpen = window.webSocket.onopen;
		var commandQueue = [];
		var _cBytes = window.cBytes;
		window.cBytes = function (command)
		{
			if (window.webSocket && window.webSocket.readyState === WebSocket.OPEN)
			{
				_cBytes(command);
			}
			else
			{
				commandQueue.push(command);
			}
		};
		var pageLoaded = false;
		var wsTimeout = null;
		var reconnectAttempts = 0;

		function onTimeout()
		{
			wsTimeout = null;
			// renew the websocket
			if (reconnectAttempts <= MAX_RECONNECTS)
			{
				window.webSocket = new WebSocket(window.SSL_ENABLED);
				window.ignoreBytesTracker = Date.now();
				initWSListener(window.webSocket);
				reconnectAttempts++;
			}
			if (window.webSocket)
			{
				window.webSocket.close(WS_TIMEOUT_CODE, 'Connection timed out after ' + WS_TIMEOUT_SEC + ' seconds');
			}
		}

		function updateWSTimeout()
		{
			if (wsTimeout)
			{
				window.clearTimeout(wsTimeout);
			}
			wsTimeout = window.setTimeout(onTimeout, WS_TIMEOUT_SEC * 1e3);
		}
		var messageQueue = [];

		function onMessage(event)
		{
			if (pageLoaded)
			{
				updateWSTimeout();
				return _onMessage.call(this, event);
			}
			else
			{
				messageQueue.push(event);
			}
		};
		var wsOpenTimeout = null;

		function onOpenTimeout()
		{
			wsOpenTimeout = null;
			location.reload();
		}

		function onOpen(event)
		{
			reconnectAttempts = 0;
			if (wsOpenTimeout)
			{
				window.clearTimeout(wsOpenTimeout);
				wsOpenTimeout = null;
			}
			// do the handshake first
			_onOpen.call(this, event);
			commandQueue.forEach(function (command)
			{
				return window.cBytes(command);
			});
		}

		function onError(event)
		{
			console.error('error in websocket:', event);
			return _onError.call(this, event);
		}

		function onClose(event)
		{
			console.info('websocket closed:', event);
			if (event.code !== WS_TIMEOUT_CODE || reconnectAttempts > MAX_RECONNECTS)
			{
				location.reload();
			}
			return _onClose.call(this, event);
		}

		function initWSListener(ws)
		{
			if (ws.readyState === WebSocket.CONNECTING)
			{
				wsOpenTimeout = window.setTimeout(onOpenTimeout, WS_OPEN_TIMEOUT_SEC * 1e3);
			}
			ws.onclose = onClose;
			ws.onerror = onError;
			ws.onmessage = onMessage;
			ws.onopen = onOpen;
		}
		initWSListener(window.webSocket);
		document.addEventListener('DOMContentLoaded', function ()
		{
			pageLoaded = true;
			messageQueue.forEach(function (event)
			{
				return window.webSocket.onmessage(event);
			});
		});
	}

	function isScriptElement(el)
	{
		return el.nodeName === 'SCRIPT';
	}

	function isWebSocketScript(script)
	{
		return script.src.includes('socket.js');
	}
	var found = false;
	var scripts = document.head ? document.head.querySelectorAll('script') : [];
	for (var i = 0; i < scripts.length; i++)
	{
		if (isWebSocketScript(scripts[i]))
		{
			// does this work?
			scripts[i].onload = webSocketLoaded;
			found = true;
		}
	}
	if (!found)
	{
		// create an observer instance
		var mutationObserver_1 = new MutationObserver(function (mutationList)
		{
			mutationList.forEach(function (mutation)
			{
				if (mutation.addedNodes.length === 0)
				{
					return;
				}
				for (var i = 0; i < mutation.addedNodes.length; i++)
				{
					var node = mutation.addedNodes[i];
					if (isScriptElement(node) && isWebSocketScript(node))
					{
						mutationObserver_1.disconnect();
						node.onload = webSocketLoaded;
						return;
					}
				}
			});
		});
		mutationObserver_1.observe(document.head
		, {
			childList: true
		});
	}
	// fix scrollText (e.g. when joining the game and receiving xp at that moment)
	window.mouseX = window.innerWidth / 2;
	window.mouseY = window.innerHeight / 2;
})(main || (main = {}));

})();