DH2 Fixed

Improve Diamond Hunt 2

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         DH2 Fixed
// @namespace    FileFace
// @description  Improve Diamond Hunt 2
// @version      0.246.2
// @author       Zorbing
// @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.246.2';
var buildTime = new Date('2017-08-12T16:37:11.942Z');
var win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
"use strict";

/**
 * 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;

	function removeTick(fn)
	{
		return remove(observer.GAME_TICK_KEY, fn);
	}
	observer.removeTick = removeTick;
})(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 RECIPE_MAX = {
	'brewing':
	{
		'braveryPotion':
		{
			max: 1
		}
		, 'stardustCrystalPotion':
		{
			max: 1
		}
	}
	, 'cooksBook':
	{}
	, 'crafting':
	{
		'drills':
		{
			max: 10
		}
		, 'crushers':
		{
			max: 10
		}
		, 'giantDrills':
		{
			max: 10
		}
		, 'excavators':
		{
			max: 10
		}
		, 'oilPipe':
		{
			max: 1
		}
		, 'pumpjacks':
		{
			max: 10
		}
		, 'rowBoat':
		{
			max: 1
		}
		, 'canoe':
		{
			max: 1
		}
		, 'sailBoat':
		{
			max: 1
		}
		, 'steamBoat':
		{
			max: 1
		}
		// thanks aguyd
		, 'bonemealBin':
		{
			extraKeys: ['boundFilledBonemealBin']
			, max: 1
		}
		, 'oilFactory':
		{
			max: 1
		}
		, 'brewingKit':
		{
			max: 1
		}
		, 'rocket':
		{
			max: 1
		}
	}
	, 'magic':
	{}
};
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'
	, '22': 'Essence Tree'
};
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'];

function getAreaName(areaId)
{
	if (areaId === 33)
	{
		return 'Quest';
	}
	else
	{
		return AREA_NAMES[areaId];
	}
}
var MONSTER_NAMES = ['Chicken', 'Rat', 'Bee', 'Snake', 'Field Tree', 'Thief', 'Bear', 'Bat', 'Skeleton', 'Golem', 'Fire Bird', 'Healer', 'Lizard', 'Northern Tree', 'Ice Bird', 'Phantom', 'Ghost', 'Grim Reaper', 'Troll', 'Five Eyed', 'Stone Golem'];

function getMonsterName(monsterId)
{
	if (monsterId === 101)
	{
		return 'Ghostly Old Mage';
	}
	else
	{
		return MONSTER_NAMES[monsterId];
	}
}
var FISH_XP = {
	'rawShrimp': 50
	, 'rawSardine': 500
	, 'rawSalmon': 700
	, 'rawTuna': 3e3
	, 'rawLobster': 5e3
	, 'rawSwordfish': 5e3
	, 'rawEel': 6e3
	, 'rawShark': 12e3
	, 'rawWhale': 20e3
	, 'rawRainbowFish': 30e3
};
var BOAT_LIST = ['rowBoat', 'canoe', 'sailBoat', 'steamBoat'];
var TRIP_DURATION = {
	'rowBoat': 3
	, 'canoe': 5
	, 'sailBoat': 7
	, 'steamBoat': 10
};
var MAX_ROCKET_KM = 384400;

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 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 zeroPadLeft(num)
{
	return (num < 10 ? '0' : '') + num;
}

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 pluralize(name)
{
	return name.replace(/([^aeiou])y$/, '$1ie').replace(/s?$/, '') + 's';
}

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;
		win.$(target).bind(
		{
			mousemove: win.changeTooltipPosition
			, mouseenter: win.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)
				{
					win.showTooltip.call(parent, event);
				}
				else
				{
					win.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 win[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 = win.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();
	});
}

function removeWhitespaceChildNodes(el)
{
	for (var i = 0; i < el.childNodes.length; i++)
	{
		var child = el.childNodes.item(i);
		if (child.nodeType === Node.TEXT_NODE && /^\s*$/.test(child.textContent || ''))
		{
			el.removeChild(child);
			i--;
		}
	}
}

function debounce(func, wait, immediate)
{
	var timeout;
	return function ()
	{
		var _this = this;
		var args = [];
		for (var _i = 0; _i < arguments.length; _i++)
		{
			args[_i] = arguments[_i];
		}
		var callNow = immediate && !timeout;
		timeout && clearTimeout(timeout);
		timeout = setTimeout(function ()
		{
			timeout = null;
			if (!immediate)
			{
				func.apply(_this, args);
			}
		}, wait);
		if (callNow)
		{
			func.apply(this, args);
		}
	};
}

function passThis(fn)
{
	return function ()
	{
		var args = [];
		for (var _i = 0; _i < arguments.length; _i++)
		{
			args[_i] = arguments[_i];
		}
		return fn.apply(void 0, [this].concat(args));
	};
}
/**
 * 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[KEY["makeNumberInputs"] = 17] = "makeNumberInputs";
		KEY[KEY["addKeepInput"] = 18] = "addKeepInput";
		KEY[KEY["addMaxBtn"] = 19] = "addMaxBtn";
		KEY[KEY["highlightUnplantableSeed"] = 20] = "highlightUnplantableSeed";
		KEY[KEY["showSdChange"] = 21] = "showSdChange";
		KEY[KEY["usePotionWarning"] = 22] = "usePotionWarning";
		KEY[KEY["showCaptions"] = 23] = "showCaptions";
		KEY[KEY["syncPriceHistory"] = 24] = "syncPriceHistory";
		KEY[KEY["useNewToolbar"] = 25] = "useNewToolbar";
		KEY[KEY["changeMachineDialog"] = 26] = "changeMachineDialog";
	})(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: 'A boat 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'
				}
				, 'rocket':
				{
					defaultValue: true
					, label: 'The rocket has landed on the moon or earth'
				}
				, 'wind':
				{
					defaultValue: true
					, label: 'The wind for the sail boat has changed'
				}
				, 'perk':
				{
					defaultValue: true
					, label: 'A new perk is unlocked (achievement set completed)'
				}
				, '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
		}
		, _a[KEY.useEfficiencyStyle] = {
			name: 'Use space efficient style'
			, description: "Use a space efficient style with less blank space"
			, defaultValue: false
		}
		, _a[KEY.makeNumberInputs] = {
			name: 'Turn text inputs into number inputs'
			, description: "Number inputs allow you to change the amount via arrow buttons"
			, defaultValue: true
		}
		, _a[KEY.addKeepInput] = {
			name: 'Add keep input for selling to npc shop'
			, description: "A keep input allows you to set the amount of items you want to keep when selling"
			, defaultValue: true
		}
		, _a[KEY.addMaxBtn] = {
			name: 'Add max button for some crafting inputs'
			, description: "Add max button for crafting (e.g. vials), brewing potions and cooking food"
			, defaultValue: true
		}
		, _a[KEY.highlightUnplantableSeed] = {
			name: 'Show whether a seed can be planted'
			, description: "Fades the item box of a seed when it's not plantable"
			, defaultValue: true
		}
		, _a[KEY.showSdChange] = {
			name: 'Show stardust change'
			, description: "Shows the amount of stardust earned or spent in the last tick"
			, defaultValue: true
		}
		, _a[KEY.usePotionWarning] = {
			name: 'Use drink warning for active potions'
			, description: "Disable drink button for 3 seconds if the potion is already active"
			, defaultValue: true
		}
		, _a[KEY.showCaptions] = {
			name: 'Show item captions'
			, description: "Show item captions for some items instead of the number of owned items"
			, defaultValue: true
		}
		, _a[KEY.syncPriceHistory] = {
			name: 'Sync price history'
			, description: "Synchronize the local price history"
			, defaultValue: false
			, sub:
			{
				'url':
				{
					defaultValue: ''
					, label: 'paste url here'
				}
			}
		}
		, _a[KEY.useNewToolbar] = {
			name: 'Use new toolbar'
			, description: "Use new reordered toolbar"
			, defaultValue: true
			, requiresReload: true
		}
		, _a[KEY.changeMachineDialog] = {
			name: 'Use slider for machine dialog'
			, description: "Change buttons in machine dialog into slider"
			, defaultValue: true
			, requiresReload: true
		}
		, _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.settings-container > li.setting > button.ui-button\n{\n\twidth: 100%;\n}\n.ui-textfield\n{\n\tbackground: none;\n\tcolor: inherit;\n\tcursor: text;\n\tfont: inherit;\n\toutline: none;\n\ttext-align: inherit;\n}\n.ui-textfield.ui-state-active,\n.ui-widget-content .ui-textfield.ui-state-active,\n.ui-widget-header .ui-textfield.ui-state-active,\n.ui-button.ui-textfield:active,\n.ui-button.ui-textfield.ui-state-active:hover\n{\n\tbackground: transparent;\n\tborder: 1px solid #c5c5c5;\n\tcolor: #333333;\n\tfont-weight: normal;\n}\n.settings-container.list > li\n{\n\tborder: 1px solid #c5c5c5;\n\tborder-radius: 3px;\n\tdisplay: flex;\n\tmargin: 5px 0;\n}\n.settings-container.list > li > span.content\n{\n\tflex: 1 0 auto;\n\tline-height: 2rem;\n\tmargin: 0 5px 0 1rem;\n}\n.settings-container.list > li > button.ui-button\n{\n\tmargin: -1px;\n}\n.instruction\n{\n\tcursor: default;\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n.instruction code,\n.instruction a\n{\n\tcursor: initial;\n\t-webkit-user-select: text;\n\t-moz-user-select: text;\n\t-ms-user-select: text;\n\tuser-select: text;\n}\n.instruction code\n{\n\tbackground-color: lightgray;\n\tdisplay: inline-block;\n\tpadding: .25rem;\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;

		function syncPriceHistory(dialog)
		{
			var setting = CFG[KEY.syncPriceHistory];
			var subSettings = setting.sub;
			var instructionEl = document.createElement('div');
			instructionEl.className = 'instruction';
			instructionEl.innerHTML = "Go to <a href=\"http://myjson.com/\" target=\"_blank\">http://myjson.com/</a>, insert <code>{}</code> and press \"<em>Save</em>\". Then copy the URL of the created store (e.g. <code>http://myjson.com/ltk51</code>) and insert it into the following input:";
			dialog.appendChild(instructionEl);
			var settingContainer = createSubSettingsContainer(KEY.syncPriceHistory, subSettings);
			dialog.appendChild(settingContainer);
		}
		subDialog.syncPriceHistory = syncPriceHistory;
	})(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);
		}

		function addInput(listEl, subKey, id, setting)
		{
			var input = document.createElement('input');
			input.type = 'text';
			input.placeholder = setting.label || '';
			input.value = getSub(parentKey, subKey);
			var onChange = function ()
			{
				return setSub(parentKey, subKey, input.value);
			};
			input.addEventListener('click', onChange);
			input.addEventListener('change', onChange);
			input.addEventListener('keyup', onChange);
			listEl.appendChild(input);
		}
		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 if (typeof setting.defaultValue === 'boolean')
			{
				addCheckbox(listEl, subKey, settingId, setting);
			}
			else if (typeof setting.defaultValue === 'string')
			{
				addInput(listEl, subKey, settingId, setting);
			}
			settingsContainer.appendChild(listEl);
		}
		return settingsContainer;
	}

	function initJQueryDialog(dialog)
	{
		var $dialog = win.$(dialog);
		$dialog.dialog(
		{
			width: DIALOG_WIDTH + 'px'
		});
		$dialog.find('input[type="checkbox"]').checkboxradio()
			.next().children(':first-child').removeClass('ui-state-hover');
		$dialog.find('button:not(.sub)').button();
		$dialog.find('input:text').button()
			.addClass('ui-textfield')
			.off('mouseenter').off('mousedown').off('keydown');
		$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.className = 'sub';
				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 initMuteDialog(settingsContainer)
	{
		// muted people dialog
		var dialog = document.createElement('div');
		dialog.id = 'dialog-chat-muted-people';
		dialog.style.display = 'none';
		dialog.innerHTML = "<h2>Muted people</h2>";
		var input = document.createElement('input');
		input.type = 'text';
		input.placeholder = 'username';
		dialog.appendChild(input);
		var addBtn = document.createElement('button');
		addBtn.textContent = '+';
		dialog.appendChild(addBtn);
		var listEl = document.createElement('ul');
		listEl.className = 'settings-container list';
		var username2Item = {};
		var username2Btn = {};

		function removeListener(event)
		{
			var target = event.target;
			var username = target.dataset.username || '';
			var index = win.mutedPeople.indexOf(username);
			if (index !== -1)
			{
				win.mutedPeople.splice(index, 1);
			}
		}

		function add2List(username)
		{
			var item = document.createElement('li');
			item.innerHTML = "<span class=\"content\">" + username + "</span>";
			var removeBtn = document.createElement('button');
			removeBtn.dataset.username = username;
			removeBtn.textContent = '-';
			win.$(removeBtn).button();
			removeBtn.addEventListener('click', removeListener);
			username2Btn[username] = removeBtn;
			item.appendChild(removeBtn);
			username2Item[username] = item;
			listEl.appendChild(item);
		}
		var _push = win.mutedPeople.push;
		win.mutedPeople.push = function ()
		{
			var items = [];
			for (var _i = 0; _i < arguments.length; _i++)
			{
				items[_i] = arguments[_i];
			}
			items.forEach(function (username)
			{
				return add2List(username);
			});
			return _push.call.apply(_push, [win.mutedPeople].concat(items));
		};
		var _splice = win.mutedPeople.splice;
		win.mutedPeople.splice = function (start, deleteCount)
		{
			var items = [];
			for (var _i = 2; _i < arguments.length; _i++)
			{
				items[_i - 2] = arguments[_i];
			}
			for (var i = 0; i < deleteCount; i++)
			{
				var username = win.mutedPeople[start + i];
				var item = username2Item[username];
				delete username2Item[username];
				listEl.removeChild(item);
				var btn = username2Btn[username];
				delete username2Btn[username];
				btn.removeEventListener('click', removeListener);
			}
			items.forEach(function (username)
			{
				return add2List(username);
			});
			return _splice.call.apply(_splice, [win.mutedPeople, start, deleteCount].concat(items));
		};
		dialog.appendChild(listEl);
		addBtn.addEventListener('click', function ()
		{
			win.mutedPeople.push(input.value);
			input.value = '';
		});
		document.body.appendChild(dialog);
		var listItem = document.createElement('li');
		listItem.classList.add('setting');
		var dialogBtn = document.createElement('button');
		dialogBtn.innerHTML = "List of muted people";
		dialogBtn.addEventListener('click', function ()
		{
			initJQueryDialog(dialog);
		});
		listItem.appendChild(dialogBtn);
		settingsContainer.appendChild(listItem);
	}

	function initKeywordDialog(settingsContainer)
	{
		// keyword dialog
		var dialog = document.createElement('div');
		dialog.id = 'dialog-chat-keyword-list';
		dialog.style.display = 'none';
		dialog.innerHTML = "<h2>Keywords</h2>";
		var input = document.createElement('input');
		input.type = 'text';
		input.placeholder = 'keyword';
		dialog.appendChild(input);
		var addBtn = document.createElement('button');
		addBtn.textContent = '+';
		dialog.appendChild(addBtn);
		var listEl = document.createElement('ul');
		listEl.className = 'settings-container list';

		function add2List(keyword)
		{
			var item = document.createElement('li');
			item.innerHTML = "<span class=\"content\">" + keyword + "</span>";
			var removeBtn = document.createElement('button');
			removeBtn.textContent = '-';
			win.$(removeBtn).button();
			var remove = function ()
			{
				if (chat.removeKeyword(keyword))
				{
					listEl.removeChild(item);
					removeBtn.removeEventListener('click', remove);
				}
			};
			removeBtn.addEventListener('click', remove);
			item.appendChild(removeBtn);
			listEl.appendChild(item);
		}
		// add all keywords
		chat.keywordList.forEach(function (keyword)
		{
			return add2List(keyword);
		});
		dialog.appendChild(listEl);
		addBtn.addEventListener('click', function ()
		{
			var keyword = input.value;
			if (chat.addKeyword(keyword))
			{
				add2List(keyword);
				input.value = '';
			}
		});
		document.body.appendChild(dialog);
		var listItem = document.createElement('li');
		listItem.classList.add('setting');
		var dialogBtn = document.createElement('button');
		dialogBtn.innerHTML = "Manage list of keywords";
		dialogBtn.addEventListener('click', function ()
		{
			initJQueryDialog(dialog);
		});
		listItem.appendChild(dialogBtn);
		settingsContainer.appendChild(listItem);
	}

	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);
		initMuteDialog(settingsContainer);
		initKeywordDialog(settingsContainer);
		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);
	}
	var HSVColor = (function ()
	{
		function HSVColor(H, S, V)
		{
			this.H = H;
			this.S = S;
			this.V = V;
		}
		HSVColor.fromHSVArray = function (hsv)
		{
			return new HSVColor(hsv[0], hsv[1], hsv[2]);
		};
		HSVColor.prototype.toHex = function ()
		{
			var rgb = this.toRGB();
			return '#' + this.componentToHex(rgb[0]) + this.componentToHex(rgb[1]) + this.componentToHex(rgb[2]);
		};
		HSVColor.prototype.toHSL = function ()
		{
			var h = this.H;
			var s = this.S / 100;
			var v = this.V / 100;
			var k = (2 - s) * v;
			return [
				h
				, Math.round(s * v / (k < 1 ? k : 2 - k) * 10e3) / 100
				, k / 2 * 100
			];
		};
		HSVColor.prototype.toHSLString = function (alpha)
		{
			var hsl = this.toHSL();
			if (alpha !== undefined)
			{
				return "hsla(" + hsl[0] + ", " + hsl[1] + "%, " + hsl[2] + "%, " + alpha + ")";
			}
			else
			{
				return "hsl(" + hsl[0] + ", " + hsl[1] + "%, " + hsl[2] + "%)";
			}
		};
		HSVColor.prototype.toRGB = function ()
		{
			// this doesn't work for the values of 0 and 360 here's the hacky fix
			var h = Math.min(Math.max(this.H, 1), 359);
			// Rebase the h,s,v values
			h = h / 360;
			var s = this.S / 100;
			var v = this.V / 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)];
		};
		HSVColor.prototype.toRGBString = function (alpha)
		{
			var rgb = this.toRGB();
			if (alpha !== undefined)
			{
				return "rgba(" + rgb.join(', ') + ", " + alpha + ")";
			}
			else
			{
				return "rgb(" + rgb.join(', ') + ")";
			}
		};
		HSVColor.prototype.componentToHex = function (c)
		{
			var hex = c.toString(16);
			return hex.length == 1 ? '0' + hex : hex;
		};
		return HSVColor;
	}());
	colorGenerator.HSVColor = HSVColor;

	function setFormat(hsv, options)
	{
		var color = HSVColor.fromHSVArray(hsv);
		switch (options.format)
		{
		case 'object':
			return color;
		case 'hsvArray':
			return hsv;
		case 'hslArray':
			return color.toHSL();
		case 'hsl':
			return color.toHSLString();
		case 'hsla':
			return color.toHSLString(options.alpha || Math.random());
		case 'rgbArray':
			return color.toRGB();
		case 'rgb':
			return color.toRGBString();
		case 'rgba':
			return color.toRGBString(options.alpha || Math.random());
		case 'hex':
		default:
			return color.toHex();
		}
	}

	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 colors = [];
			while (options.count > 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));
			}
			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;
	}());
	colorGenerator.ColorInterval = ColorInterval;
	var defaultRootInterval = new ColorInterval(0, 360);

	function getEquallyDistributed(rootInterval)
	{
		if (rootInterval === void 0)
		{
			rootInterval = defaultRootInterval;
		}
		return 'hsl(' + rootInterval.getNextValue() + ', 100%, 80%)';
	}
	colorGenerator.getEquallyDistributed = getEquallyDistributed;
	var Color = (function ()
	{
		function Color(r, g, b)
		{
			this.r = r;
			this.g = g;
			this.b = b;
		}
		Color.fromHex = function (hex)
		{
			return new Color(parseInt(hex.substr(1, 2), 16), parseInt(hex.substr(3, 2), 16), parseInt(hex.substr(5, 2), 16));
		};
		Color.fromRgb = function (rgb)
		{
			var match = rgb.match(this.rgbRegex);
			return new Color(parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10));
		};
		Color.fromString = function (str)
		{
			if (this.hexRegex.test(str))
			{
				return this.fromHex(str);
			}
			else if (this.rgbRegex.test(str))
			{
				return this.fromRgb(str);
			}
			else
			{
				throw new Error('Unexpected color format: ' + str);
			}
		};
		Color.prototype.toString = function (hex)
		{
			if (hex === void 0)
			{
				hex = true;
			}
			return '#' + this.toHex(this.r) + this.toHex(this.g) + this.toHex(this.b);
		};
		Color.prototype.toHex = function (x)
		{
			var xStr = x.toString(16);
			return (xStr.length == 1 ? '0' : '') + xStr;
		};
		Color.hexRegex = /^#(?:[0-9a-f]{3}){1,2}$/i;
		Color.rgbRegex = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
		return Color;
	}());

	function ratioColor(color1, color2, ratio)
	{
		var color = new Color(Math.ceil(color1.r * (1 - ratio) + color2.r * ratio), Math.ceil(color1.g * (1 - ratio) + color2.g * ratio), Math.ceil(color1.b * (1 - ratio) + color2.b * ratio));
		return color.toString();
	}

	function getColorTransition(value, colorStrings)
	{
		var smallerValue = -1;
		var biggerValue = Number.MAX_SAFE_INTEGER;
		var colors = {};
		for (var v in colorStrings)
		{
			var vNum = Number(v);
			if (vNum === value)
			{
				return colorStrings[v];
			}
			else if (vNum < value)
			{
				smallerValue = Math.max(smallerValue, vNum);
			}
			else
			{
				biggerValue = Math.min(biggerValue, vNum);
			}
			colors[v] = Color.fromString(colorStrings[v]);
		}
		if (smallerValue === -1)
		{
			return colorStrings[biggerValue];
		}
		if (biggerValue === Number.MAX_SAFE_INTEGER)
		{
			return colorStrings[smallerValue];
		}
		var ratio = (value - smallerValue) / (biggerValue - smallerValue);
		return ratioColor(colors[smallerValue], colors[biggerValue], ratio);
	}
	colorGenerator.getColorTransition = getColorTransition;
	// populate the color dictionary
	loadColorBounds();
})(colorGenerator || (colorGenerator = {}));

/**
 * provides icons
 */
var icons;
(function (icons)
{
	icons.CHART_LINE = 'M16,11.78L20.24,4.45L21.97,5.45L16.74,14.5L10.23,10.75L5.46,19H22V21H2V3H4V17.54L9.5,8L16,11.78Z';
	icons.WIKIA = '<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"/>';

	function getSvgAsUrl(svg)
	{
		return "url('data:image/svg+xml;base64," + btoa(svg) + "')";
	}
	icons.getSvgAsUrl = getSvgAsUrl;

	function wrapCodeWithSvg(code, viewBox, width, height)
	{
		return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + width + "\" height=\"" + height + "\" viewBox=\"" + viewBox + "\">" + code + "</svg>";
	}
	icons.wrapCodeWithSvg = wrapCodeWithSvg;

	function getMd(pathDots, color, width, height)
	{
		if (color === void 0)
		{
			color = 'black';
		}
		if (width === void 0)
		{
			width = '30';
		}
		if (height === void 0)
		{
			height = '30';
		}
		return getSvgAsUrl(wrapCodeWithSvg("<path fill=\"" + color + "\" d=\"" + pathDots + "\" />", '0 0 24 24', width, height));
	}
	icons.getMd = getMd;
})(icons || (icons = {}));

/**
 * 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 win))
		{
			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)
						{
							win.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 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, extraXp)
	{
		if (!settings.get(settings.KEY.newXpAnimation))
		{
			return;
		}
		animationQueue[skill] = animationQueue[skill] || [];
		animationQueue[skill].push(
		{
			cell: cell
			, color: color
			, xpAmount: xpAmount
			, extraXp: extraXp
		});
		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
			, extraXp = entry.extraXp;
		var rect = cell.getBoundingClientRect();
		var extraXpStr = extraXp > 0 ? " (+" + extraXp + ")" : '';
		var $el = win.$("<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) + extraXpStr + "</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')
		{
			log.processFishingXpChange(xpAmount);
		}
		var extraXp = 0;
		if (extra && settings.get(settings.KEY.newXpAnimation))
		{
			var extraMatch = extra.match(/^\s*\(<img[^>]+src=(['"])images\/([^']+)\.png\1[^>]+>\s*(.+)\)$/);
			var extraXpMatch = extra.match(/^\s*\(\+(\d+)\s*xp\)\s*$/);
			if (extraMatch)
			{
				var icon_1 = extraMatch[2];
				var text = extraMatch[3];
				if (icon_1 == 'brewingKit')
				{
					text = '+' + text;
				}
				win.scrollText(icon_1, color, text);
			}
			else if (extraXpMatch)
			{
				extraXp = Number(extraXpMatch[1]);
			}
			else
			{
				win.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, extraXp);
		}
		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 = win.getLevel(xp);
		log.add(
		{
			type: 'lvlup'
			, skill: skill
			, newLevel: oldLvl + 1
		});
		return true;
	}

	function processCombat(data)
	{
		var match = data.match(/^STHS=([^~]+)~([^~]+)~([^~]+)~img-(.+)~(melee|heal)$/);
		if (!match)
		{
			return false;
		}
		// keep track of different battles and add the data to the current battle
		var number = match[3];
		if (!/\D/.test(number))
		{
			number = Number(number);
		}
		log.add(
		{
			type: 'combat'
			, what: match[5]
			, who: match[4]
			, text: number
		});
		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='))
		{
			log.add(
			{
				data: minutes2String(data.replace(/^[^=]+=/, ''))
			});
		}
		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 currentCombat = null;
	var currentCombatEl = null;
	var LOG_FILTER = {
		'combat':
		{
			title: 'Combat'
			, img: 'images/icons/combat.png'
		}
		, 'loot':
		{
			title: 'Loot'
			, img: 'http://www.clker.com/cliparts/U/a/v/n/h/w/bag-hi.png'
		}
		, 'fish':
		{
			title: 'Caught fish'
			, img: 'images/tuna.png'
		}
		, 'skill':
		{
			title: 'Skill advance'
			, img: 'images/icons/skills.png'
		}
		, 'other':
		{
			title: 'All other'
			, label: 'Other'
		}
	};
	var logEl;

	function isFightStarted()
	{
		return win.fightMonsterId !== 0;
	}

	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)
	{
		var filterEl = logEl.firstElementChild;
		var next = filterEl && filterEl.nextElementSibling;
		if (next)
		{
			logEl.insertBefore(entryEl, next);
		}
		else
		{
			logEl.appendChild(entryEl);
		}
		logEl.classList.remove('empty');
	}

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

	function setLootEntry(entry, init)
	{
		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);
		var valueContainer = document.createElement('div');
		valueContainer.className = 'total-value';
		valueContainer.appendChild(document.createTextNode('Total value: '));
		var totalValue = document.createElement('span');
		totalValue.style.cursor = 'pointer';
		totalValue.textContent = 'Click to calculate';
		valueContainer.appendChild(totalValue);
		totalValue.addEventListener('click', function ()
		{
			var items = {};
			for (var _i = 0, _a = entry.itemList; _i < _a.length; _i++)
			{
				var item = _a[_i];
				if (item.text.indexOf('xp') === -1)
				{
					var key = item.icon.replace(/^.+\/([^\/]+)\.png$/, '$1');
					var num = Number(item.text.replace(/\D/g, ''));
					items[key] = (items[key] || 0) + num;
				}
			}
			market.calcMarketValue(items)
				.then(function (sum)
				{
					totalValue.innerHTML = "<img class=\"image-icon-20\" src=\"images/coins.png\"> " + format.number(sum[0]) + " - <img class=\"image-icon-20\" src=\"images/coins.png\"> " + format.number(sum[1]);
				});
		});
		el.appendChild(valueContainer);
		appendLi(el);
	}

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

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

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

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

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

	function getHTMLFromCombatInfo(info, name)
	{
		return "<div class=\"combat-log-graph\">\n\t\t\t<span>" + name + " (" + info.startHp + " Hp to " + info.endHp + " Hp):</span><br>\n\t\t\t<svg style=\"height: " + info.startHp + "px;\"><polygon points=\"" + info.points.join(',') + "\"></polygon></svg>\n\t\t</div>";
	}

	function setCombatEntry(entry, init)
	{
		var created = init || currentCombatEl == null;
		if (init || 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 = logEl.scrollWidth - 4 * 12.8 - 2;
			var scaleX = currentTick === 0 ? 0 : width / currentTick;
			var hero = getCombatInfo(entry.hero, win.heroHp, scaleX, width);
			var monster = getCombatInfo(entry.monster, win.fightMonsterHp, scaleX, width);
			// TODO: who won?
			HTML = "The fight took " + format.sec2Str(currentTick) + ".\n\t\t\t" + getHTMLFromCombatInfo(hero, 'Hero') + "\n\t\t\t" + getHTMLFromCombatInfo(monster, 'Monster') + "\n\t\t\t";
		}
		// map monster name and area name from monster id (the ids are starting at 1)
		var isShiny = entry.monsterId > 1e3;
		var mId = entry.monsterId - (isShiny ? 1001 : 1);
		var monsterName = (isShiny ? 'Shiny ' : '') + (getMonsterName(mId) || '(' + (mId % 3 + 1) + ')');
		var areaId = Math.floor(mId / 3);
		var areaName = getAreaName(areaId) || '(' + (areaId + 1) + ')';
		currentCombatEl.innerHTML = "<h2>Combat against " + monsterName + " in " + areaName + "</h2>\n\t\t" + HTML;
		if (created)
		{
			appendLi(currentCombatEl);
		}
		if (!isFightStarted())
		{
			currentCombatEl = null;
		}
	}

	function setMarketEntry(entry, init)
	{
		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, init)
	{
		var el = createLi(entry);
		el.innerHTML = "You added " + format.number(entry.bonemeal) + " bonemeal to your bonemeal bin.";
		appendLi(el);
	}

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

	function setStardustEntry(entry, init)
	{
		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, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		if (!logEl)
		{
			return;
		}
		if (entry.type && entryType2Fn.hasOwnProperty(entry.type))
		{
			entryType2Fn[entry.type](entry, init);
		}
		else
		{
			setGenericEntry(entry, init);
		}
	}

	function add2Log(entry)
	{
		logList.push(entry);
		logList = logList.slice(-MAX_LOG_SIZE);
		saveLog();
	}
	// 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
	function findCurrentCombat()
	{
		for (var i = logList.length - 1; i >= 0; i--)
		{
			if (logList[i].type == 'combat')
			{
				var entry = logList[i];
				if (entry.monsterId == win.fightMonsterId
					&& entry.hero[entry.ticks] !== 0
					&& entry.hero[entry.ticks] !== 0)
				{
					return entry;
				}
				break;
			}
		}
		return null;
	}

	function add(entry)
	{
		if (!entry.time)
		{
			entry.time = now();
		}
		if (entry.type == 'combat')
		{
			currentCombat = currentCombat || findCurrentCombat();
			if (!currentCombat)
			{
				return;
			}
			// skip entries without further information
			if (typeof entry.text !== 'number' || entry.text === 0)
			{
				return;
			}
			var hp = entry.who == 'hero' ? win.heroHp : win.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.text;
			currentCombat[entry.who][currentCombat.ticks] = hp;
			saveLog();
			updateLog(currentCombat);
		}
		else
		{
			add2Log(entry);
			updateLog(entry);
		}
	}
	log.add = add;

	function addLogEl()
	{
		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:not(.filter)\n{\n\tborder: 1px solid gray;\n\tborder-radius: .2rem;\n\tdisplay: none;\n\tmargin: .2rem 0;\n\tpadding: .4rem .8rem;\n}\n#activity-log li:not(.filter)::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#activity-log.combat > li[data-type=\"combat\"]\n{\n\tdisplay: block;\n}\n#activity-log.loot > li[data-type=\"loot\"]\n{\n\tdisplay: block;\n}\n#activity-log.fish > li[data-type=\"fish\"]\n{\n\tdisplay: block;\n}\n#activity-log.lvlup > li[data-type=\"lvlup\"]\n{\n\tdisplay: block;\n}\n#activity-log.other > li[data-type=\"energy\"],\n#activity-log.other > li[data-type=\"heat\"],\n#activity-log.other > li[data-type=\"market\"],\n#activity-log.other > li[data-type=\"bonemeal\"],\n#activity-log.other > li[data-type=\"crafting\"],\n#activity-log.other > li[data-type=\"stardust\"],\n#activity-log.other > li[data-type=\"undefined\"]\n{\n\tdisplay: block;\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';
		var classList = [];
		var html = '';
		for (var key in LOG_FILTER)
		{
			// TODO: load saved filter
			var checked = true;
			classList.push(key);
			html += "<label for=\"log-filter-" + key + "\" title=\"" + LOG_FILTER[key].title + "\">\n\t\t\t\t" + (LOG_FILTER[key].img ? "<img class=\"image-icon-20\" src=\"" + LOG_FILTER[key].img + "\">" : LOG_FILTER[key].label) + "\n\t\t\t</label>\n\t\t\t<input type=\"checkbox\" id=\"log-filter-" + key + "\" " + (checked ? 'checked' : '') + ">";
		}
		logEl.className = 'empty ' + classList.join(' ');
		logEl.innerHTML = "<li class=\"filter\">\n\t\t\t" + html + "\n\t\t</li>";
		document.body.appendChild(logEl);
		var $checkboxes = win.$('li.filter > input[id^="log-filter-"]');
		$checkboxes.checkboxradio(
		{
			icon: false
		});
		$checkboxes.change(function (event)
		{
			var id = event.target.id;
			var key = id.replace('log-filter-', '');
			var checked = document.getElementById(id).checked;
			logEl.classList[checked ? 'add' : 'remove'](key);
			// TODO: save current state
		});
		// add all stored elements
		logList.forEach(function (e)
		{
			return updateLog(e, true);
		});
	}

	function observeCombat()
	{
		observer.add('fightMonsterId', function (key, oldValue, newValue)
		{
			if (isFightStarted())
			{
				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] = win.heroHp;
					currentCombat.monster[0] = win.fightMonsterHp;
				}
				updateLog(currentCombat);
			}
		});
	}
	var possiblyCaughtFish;
	var lastFishingXpChange = 0;

	function fishObserver(key, oldValue, newValue)
	{
		if (oldValue < newValue && lastFishingXpChange >= now() - 5e3)
		{
			var idx = possiblyCaughtFish.indexOf(key);
			if (idx !== -1)
			{
				add(
				{
					type: 'fish'
					, fish: key
				});
				possiblyCaughtFish = [];
				lastFishingXpChange = 0;
			}
		}
	}

	function processFishingXpChange(xp)
	{
		lastFishingXpChange = now();
		possiblyCaughtFish = [];
		for (var fish in FISH_XP)
		{
			if (FISH_XP[fish] == xp)
			{
				possiblyCaughtFish.push(fish);
			}
		}
	}
	log.processFishingXpChange = processFishingXpChange;

	function observeFishing()
	{
		for (var fish in FISH_XP)
		{
			observer.add(fish, fishObserver);
		}
	}

	function init()
	{
		addLogEl();
		observeCombat();
		observeFishing();
	}
	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
		, map: true
		, essence: true
		, rocket: true
		, wind: true
		, perk: 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('.');
				win.openTab(tabNames[0]);
				if (tabNames.length > 1)
				{
					win.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()
	{
		var timerKeys = BOAT_LIST.map(function (boatKey)
		{
			return boatKey + 'Timer';
		});
		observeTimer(timerKeys, function (key, oldValue, newValue)
		{
			if (!gameEvents.enabled.boat || !settings.getSub(settings.KEY.showNotifications, 'boatReturned'))
			{
				return;
			}
			var boatKey = key.replace(/Timer$/, '');
			notifyTabClickable('Fishy business', 'Your ' + split2Words(boatKey).toLowerCase() + ' returned from its trip.', boatKey + '.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 = win.refreshMarketSlot;
		var lastCollectText = 0;
		win.refreshMarketSlot = function (offerId, itemKey, amount, price, collectText, slotId, timeLeft)
		{
			var diff = collectText - lastCollectText;
			lastCollectText = collectText;
			if (gameEvents.enabled.market && settings.getSub(settings.KEY.showNotifications, 'itemsSold') && collectText > 0)
			{
				var soldAmount = diff / price;
				var amountText = ['one (1)', 'two (2)', 'three (3)'][soldAmount - 1] || format.number(soldAmount);
				var itemName = split2Words(itemKey).toLowerCase();
				if (soldAmount > 1)
				{
					itemName = pluralize(itemName);
				}
				var textTemplate = function (itemText)
				{
					return "You've sold " + itemText + " to the market.";
				};
				if (amount > 0)
				{
					notifyTabClickable('Ka-ching', textTemplate(amountText + ' ' + itemName), 'icons/shop.png', 'playermarket');
				}
				else
				{
					notifyTabClickable('Sold out', textTemplate((soldAmount === 1 ? 'your' : 'all') + ' ' + amountText + ' ' + itemName), 'icons/shop.png', 'playermarket');
				}
			}
			_refreshMarketSlot(offerId, itemKey, amount, price, collectText, slotId, timeLeft);
		};
	}

	function gameValues()
	{
		observer.add('treasureMap', function (key, oldValue, newValue)
		{
			if (gameEvents.enabled.map && 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 (gameEvents.enabled.essence && settings.get(settings.KEY.showEssencePopup))
				{
					win.confirmDialogue(400, text, 'Close', '', '');
				}
				if (gameEvents.enabled.essence && settings.getSub(settings.KEY.showNotifications, 'essence'))
				{
					notifyTabClickable('Essence of Life', text, 'essence.png', 'combat.spells');
				}
			}
		});
		observer.add('rocketMoonId', function (key, oldValue, newValue)
		{
			if (gameEvents.enabled.rocket && settings.getSub(settings.KEY.showNotifications, 'rocket'))
			{
				if (newValue > 0)
				{
					notifyTabClickable('One small step for a man...', 'Your rocket landed on the moon.', 'rocket.png', 'mining');
				}
				else if (oldValue < 0 && newValue === 0)
				{
					notifyTabClickable('Back home', 'Your rocket returned to earth.', 'rocket.png', 'mining');
				}
			}
		});
		var WIND_DESCRIPTION = [
			'The sea is sleeping like a baby'
			, 'There is a slight breeze'
			, 'A normal day on the sea'
			, 'There is a storm coming'
			, 'The sea is raging'
		];
		var WIND_CATEGORY = ['none', 'low', 'medium', 'high', 'very high'];
		var _setSailBoatWind = win.setSailBoatWind;
		var oldValue = -1;
		win.setSailBoatWind = function (windLevel)
		{
			_setSailBoatWind(windLevel);
			var newValue = win.sailBoatWindGlobal;
			if (oldValue !== -1
				&& oldValue !== newValue
				&& win.boundSailBoat > 0
				&& gameEvents.enabled.wind
				&& settings.getSub(settings.KEY.showNotifications, 'wind'))
			{
				var windText = (WIND_DESCRIPTION[win.sailBoatWindGlobal] || 'The wind is turning')
					+ ' (' + (WIND_CATEGORY[win.sailBoatWindGlobal] || 'level ' + win.sailBoatWindGlobal) + ' wind).';
				notifyTabClickable('Wind of change', windText, 'sailBoat.png', 'combat');
			}
			oldValue = newValue;
		};
		// trigger getting the wind level once at page load
		// so the script can distinguish between getting the wind initially and an actual wind change
		win.processTab('combat');
		// achievements (e.g. achBrewingEasyCompleted)
		var achRegex = /^ach([A-Z][a-z]+)([A-Z][a-z]+)Completed$/;

		function checkAchievement(key, oldValue, newValue)
		{
			if (gameEvents.enabled.perk && settings.getSub(settings.KEY.showNotifications, 'perk') && oldValue < newValue)
			{
				var match = key.match(/^ach([A-Z][a-z]+)([A-Z][a-z]+)Completed$/);
				var skillName = match[1].toLowerCase();
				var difficulty = match[2].toLowerCase();
				notifyTabClickable('New perk unlocked', 'You completed the ' + difficulty + ' ' + skillName + ' achievement set.', 'achievementBook.png', 'achievements');
			}
		}
		for (var _i = 0, _a = win.jsItemArray; _i < _a.length; _i++)
		{
			var key = _a[_i];
			if (achRegex.test(key))
			{
				observer.add(key, checkAchievement);
			}
		}
		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.className = 'hide';
			changeEl_1.id = 'stardust-change';
			parent.appendChild(changeEl_1);
			var HIDE_AFTER_TICKS_1 = 5;
			var ticksSinceSdChange_1 = HIDE_AFTER_TICKS_1;
			var sdDiff_1 = 0;
			observer.add('stardust', function (key, oldValue, newValue)
			{
				sdDiff_1 = Math.max(newValue - oldValue, 0);
				if (sdDiff_1 > 0)
				{
					ticksSinceSdChange_1 = 0;
				}
			});
			observer.addTick(function ()
			{
				var show = settings.get(settings.KEY.showSdChange) && ticksSinceSdChange_1 < HIDE_AFTER_TICKS_1;
				changeEl_1.classList[show ? 'remove' : 'add']('hide');
				ticksSinceSdChange_1++;
				var diff = ticksSinceSdChange_1 > 1 ? 0 : sdDiff_1;
				var sign = diff > 0 ? '+' : PLUS_MINUS_SIGN;
				changeEl_1.textContent = '(' + sign + format.number(diff) + ')';
			});
		}
	}

	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, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var info = RECIPE_MAX.crafting[key];
		var maxValue = typeof info.max === 'function' ? info.max() : info.max;
		var boundKey = getBoundKey(key);
		var unbound = getGameValue(key);
		var bound = getGameValue(boundKey);
		var extra = (info.extraKeys || []).map(function (k)
		{
			return getGameValue(k);
		}).reduce(function (p, c)
		{
			return p + c;
		}, 0);
		setRecipeVisibility(key, maxValue - (bound + unbound + extra) > 0);
		if (init)
		{
			observer.add([key, boundKey], function ()
			{
				return hideRecipe(key, 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 RECIPE_MAX.crafting)
			{
				hideRecipe(key, init);
			}
			if (init)
			{
				settings.observe(settings.KEY.hideCraftingRecipes, function ()
				{
					return processRecipes(false);
				});
			}
		}
		processRecipes(true);
		var _processCraftingTab = win.processCraftingTab;
		win.processCraftingTab = function ()
		{
			var reinit = !!win.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 RECIPE_MAX.crafting)
			{
				hideItem(key, RECIPE_MAX.crafting[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.querySelector('span[data-item-display]');
		if (!numberElement)
		{
			return;
		}
		numberElement.classList.add('number-caption');
		if (setVisibility)
		{
			numberElement.classList.remove('hide');
			numberElement.classList.add('hidden');
		}
		else
		{
			numberElement.classList.remove('hidden');
			numberElement.classList.add('hide');
		}
	}

	function addSpan2ItemBox(key, replace, setVisibility)
	{
		if (replace === void 0)
		{
			replace = true;
		}
		if (setVisibility === void 0)
		{
			setVisibility = false;
		}
		if (replace)
		{
			hideNumberInItemBox(key, setVisibility);
		}
		var itemBox = document.getElementById('item-box-' + key);
		if (!itemBox)
		{
			return;
		}
		var span = document.createElement('span');
		span.className = 'caption';
		itemBox.appendChild(span);
		return span;
	}

	function addCaptionStyle()
	{
		var CLASS_NAME = 'show-captions';
		addStyle("\nbody:not(." + CLASS_NAME + ") span.caption\n{\n\tdisplay: none;\n}\nbody." + CLASS_NAME + " span.number-caption.hidden\n{\n\tvisibility: hidden;\n}\nbody." + CLASS_NAME + " span.number-caption.hide\n{\n\tdisplay: none;\n}\n\t\t");

		function updateBodyClass()
		{
			var show = settings.get(settings.KEY.showCaptions);
			document.body.classList[show ? 'add' : 'remove'](CLASS_NAME);
		}
		updateBodyClass();
		settings.observe(settings.KEY.showCaptions, function ()
		{
			return updateBodyClass();
		});
	}

	function setOilPerSecond(span, oil)
	{
		span.innerHTML = "+ " + format.number(oil) + " L/s <img src=\"images/oil.png\" class=\"image-icon-20\">";
	}
	// 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.classList.add('capacity');
				capacitySpan.textContent = 'Capacity: ' + format.number(win.getFurnaceCapacity(boundKey), true);
			}
		}
		// charcoal foundry
		var foundryCapacitySpan = addSpan2ItemBox('charcoalFoundry');
		if (foundryCapacitySpan)
		{
			foundryCapacitySpan.classList.add('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.classList.add('oil-cap');
				capSpan.textContent = 'Oil cap: ' + format.number(OIL_STORAGE_SIZES[i], true);
			}
		}
	}
	var oilPipeOrbKey = 'boundBlueOilPipeOrb';

	function setOilPipeCaption(span)
	{
		setOilPerSecond(span, 50 + win.achMiningEasyCompleted * 50 + getGameValue(oilPipeOrbKey) * 100);
	}
	// show oil per second
	function addOilCaption()
	{
		addStyle("\n#item-box-handheldOilPump,\n#item-box-boundOilPipe,\n#item-box-boundPumpjacks,\n#item-box-boundOilFactory\n{\n\tposition: relative;\n}\nspan.caption.oil\n{\n\tfont-size: .9rem;\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n}\nspan.caption.oil img[src=\"images/oil.png\"]\n{\n\t-webkit-filter: drop-shadow(0px 0px 5px rgb(255,255,255));\n\tfilter: url(#drop-shadow);\n\t-ms-filter: \"progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=0, Color='#FFF')\";\n\tfilter: \"progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=0, Color='#FFF')\";\n}\n\t\t");
		var tpl = document.createElement('templateWrapper');
		tpl.innerHTML = "\n<svg height=\"0\" xmlns=\"http://www.w3.org/2000/svg\">\n    <filter id=\"drop-shadow\">\n        <feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"1\"></feGaussianBlur>\n        <feOffset dx=\"0\" dy=\"0\" result=\"offsetblur\"></feOffset>\n        <feFlood flood-color=\"rgba(255,255,255,1)\"></feFlood>\n        <feComposite in2=\"offsetblur\" operator=\"in\"></feComposite>\n        <feMerge>\n\n            <feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode>\n        </feMerge>\n    </filter>\n</svg>\n\t\t";
		var shadowDrop = tpl.firstElementChild;
		document.body.appendChild(shadowDrop);
		var handheldOilSpan = addSpan2ItemBox('handheldOilPump', true, true);
		if (handheldOilSpan)
		{
			handheldOilSpan.classList.add('oil');
			setOilPerSecond(handheldOilSpan, 1 * win.miner);
			observer.add('miner', function ()
			{
				return setOilPerSecond(handheldOilSpan, 1 * win.miner);
			});
		}
		var oilPipeSpan = addSpan2ItemBox('boundOilPipe', true, true);
		if (oilPipeSpan)
		{
			oilPipeSpan.classList.add('oil');
			setOilPipeCaption(oilPipeSpan);
			observer.add(oilPipeOrbKey, function ()
			{
				return setOilPipeCaption(oilPipeSpan);
			});
		}
		// add pump jack oil display
		var pumpjackSpan = addSpan2ItemBox('boundPumpjacks', false);
		if (pumpjackSpan)
		{
			pumpjackSpan.classList.add('oil');
			var setCaption_1 = function ()
			{
				return setOilPerSecond(pumpjackSpan, win.boundPumpjacks * 10);
			};
			setCaption_1();
			observer.add('boundPumpjacks', function ()
			{
				return setCaption_1();
			});
		}
		// add number of workers as caption to oil factory
		var workerSpan = addSpan2ItemBox('boundOilFactory');
		if (workerSpan)
		{
			var setCaption_2 = function ()
			{
				return workerSpan.textContent = 'Workers: ' + format.number(win.oilFactoryCheapWorkers, true);
			};
			setCaption_2();
			observer.add('oilFactoryCheapWorkers', function ()
			{
				return setCaption_2();
			});
		}
		var factoryOilSpan = addSpan2ItemBox('boundOilFactory');
		if (factoryOilSpan)
		{
			factoryOilSpan.classList.add('oil');
			var setCaption_3 = function ()
			{
				return setOilPerSecond(factoryOilSpan, win.oilFactoryCheapWorkers);
			};
			setCaption_3();
			observer.add('oilFactoryCheapWorkers', function ()
			{
				return setCaption_3();
			});
		}
	}

	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 = {
			'achievementBook': 'Achievements'
			, '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.classList.add('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 boatTimerKeys = BOAT_LIST.map(function (boatKey)
	{
		return boatKey + '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' : '';
		var parent = span.parentElement;
		parent.style.opacity = otherInTransit ? '.5' : '';
		if (init)
		{
			observer.add(boatTimerKeys, function ()
			{
				return checkBoat(span, timerKey, false);
			});
		}
	}
	// show boat progress
	function addBoatCaption()
	{
		addStyle("\n#item-box-boundSailBoat.item-box > span[data-item-display] + span + span\n{\n\tdisplay: none;\n}\n\t\t");
		for (var i = 0; i < BOAT_LIST.length; i++)
		{
			var span = addSpan2ItemBox(getBoundKey(BOAT_LIST[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(win.bonemeal);
		var captionSpan = document.createElement('span');
		captionSpan.className = 'caption';
		captionSpan.textContent = 'Bonemeal: ';
		bonemealSpan.parentElement.insertBefore(captionSpan, bonemealSpan);
	}

	function warningBeforeSellingGems()
	{
		var _sellNPCItemDialogue = win.sellNPCItemDialogue;
		win.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
					|| !win.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
					|| !win.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-color: black;\n\tbackground-image: " + icons.getSvgAsUrl(icons.wrapCodeWithSvg(icons.WIKIA, '-2 -2 26 27', 30, 30)) + ";\n\tbackground-repeat: 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()
	{
		addCaptionStyle();
		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';
	chat.keywordList = store.has(KEYWORD_LIST_KEY) ? store.get(KEYWORD_LIST_KEY) : [];
	var CHAT_HISTORY_KEY = 'chatHistory';
	var MAX_CHAT_HISTORY_LENGTH = 100;
	var PM_HISTORY_KEY = 'pmHistory';
	var MAX_PM_HISTORY_LENGTH = 50;
	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 = [
	{
		key: ''
		, title: ''
	}
	, {
		key: 'halloween2015'
		, title: 'Halloween Gamer (2015)'
	}
	, {
		key: 'christmas2015'
		, title: 'Chirstmas Gamer (2015)'
	}
	, {
		key: 'easter2016'
		, title: 'Easter Gamer (2016)'
	}
	, {
		key: 'halloween2016'
		, title: 'Halloween Gamer (2016)'
	}
	, {
		key: 'christmas2016'
		, title: 'Chirstmas Gamer (2016)'
	}
	, {
		key: 'dh1Max'
		, title: 'DH1 Pro'
	}
	, {
		key: 'hardcore'
		, title: 'Hardcore Player'
	}
	, {
		key: 'quest'
		, title: 'Questmaster'
	}
	, {
		key: 'maxMining'
		, title: 'Mastery in mining'
	}
	, {
		key: 'maxCrafting'
		, title: 'Mastery in crafting'
	}
	, {
		key: 'maxWC'
		, title: 'Mastery in woodcutting'
	}
	, {
		key: 'maxFarming'
		, title: 'Mastery in farming'
	}
	, {
		key: 'maxBrewing'
		, title: 'Mastery in brewing'
	}
	, {
		key: 'maxCombat'
		, title: 'Mastery in combat'
	}
	, {
		key: 'maxMagic'
		, title: 'Mastery in magic'
	}
	, {
		key: 'maxFishing'
		, title: 'Mastery in fishing'
	}
	, {
		key: 'maxCooking'
		, title: 'Mastery in cooking'
	}
	, {
		key: 'maxLevel'
		, title: 'Mastery of all skills'
	}
	, {
		key: 'birdcage'
		, title: 'Stole a birdcage'
	}
	, {
		key: 'achievement'
		, title: 'Achievement Hunter'
	}];
	var getUnknownChatIcon = function (icon)
	{
		return {
			key: 'unknown'
			, title: ''
			, img: '<img src="images/chat-icons/' + icon + '.png" class="image-icon-20" />'
		};
	};
	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';
	// load chat history
	var chatHistory = store.get(CHAT_HISTORY_KEY) || [];
	var pmHistory = store.get(PM_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 !== win.username
			&& win.mutedPeople.some(function (name)
			{
				return user.indexOf(name) > -1;
			});
	}

	function isSpam(data)
	{
		// allow all own messages, messages from contributors, mods, devs and all server messages
		if (data.username === win.username || data.tag != Tag.none)
		{
			return false;
		}
		/**
		 * get last message of current user
		 */
		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 (dataBefore.username === data.username)
			{
				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 saveKeywordList()
	{
		store.set(KEYWORD_LIST_KEY, chat.keywordList);
	}

	function addKeyword(keyword)
	{
		if (keyword !== '' && chat.keywordList.indexOf(keyword) === -1)
		{
			chat.keywordList.push(keyword);
			saveKeywordList();
			return true;
		}
		return false;
	}
	chat.addKeyword = addKeyword;

	function removeKeyword(keyword)
	{
		var index = chat.keywordList.indexOf(keyword);
		if (index !== -1)
		{
			chat.keywordList.splice(index, 1);
			saveKeywordList();
			return true;
		}
		return false;
	}
	chat.removeKeyword = removeKeyword;

	function handleScrolling(chatbox)
	{
		if (win.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];
			if (username !== 'sexy_squid')
			{
				username = username.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 = win.getGlobalLevel();
			}
		}
		// unlinkify when using DH2QoL to store the plain message
		if (win.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 savePmHistory()
	{
		store.set(PM_HISTORY_KEY, pmHistory);
	}

	function add2ChatHistory(data)
	{
		if (data.type === Type.pmReceived
			|| data.type === Type.pmSent)
		{
			pmHistory.push(data);
			pmHistory = pmHistory.slice(-MAX_PM_HISTORY_LENGTH);
			savePmHistory();
		}
		else
		{
			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')
			|| !win.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 == '' ? win.username + ':' : 'PM to ' + toUsername + ':';
		document.getElementById(CHAT_INPUT_ID).placeholder = newTextPlaceholder;
		handleScrolling(newChatPanel);
	}

	function clearChat(username)
	{
		if (username === '')
		{
			// clean server chat
			chatHistory = [];
			saveChatHistory();
		}
		else
		{
			// delete pms stored for that user
			for (var i = 0; i < pmHistory.length; i++)
			{
				var data = pmHistory[i];
				if (data.username == username)
				{
					pmHistory.splice(i, 1);
					i--;
				}
			}
			savePmHistory();
		}
		// 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 ? win.username : data.username;
		var history = isThisPm ? pmHistory : chatHistory;
		var historyIndex = history.indexOf(data);
		var isSameUser = null;
		var isSameTime = null;
		for (var i = historyIndex - 1; i >= 0 && (isSameUser === null || isSameTime === null); i--)
		{
			var dataBefore = history[i];
			if (isThisPm === isDataPM(dataBefore))
			{
				if (isSameUser === null)
				{
					var beforeUsername = dataBefore.type == Type.pmSent ? win.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] || getUnknownChatIcon(data.icon);
		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)
		{
			win.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("\ndiv.div-chat-area\n{\n\tpadding-left: 0;\n}\nspan.chat-msg\n{\n\tdisplay: flex;\n\tmin-height: 21px;\n\tpadding: 1px 0;\n\tpadding-left: 5px;\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.unknown > img,\n.chat-msg .user .icon:not(.unknown)::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.chat-msg .user .icon.maxMining::before\t\t{ background-image: url('images/chat-icons/9.png'); }\n.chat-msg .user .icon.maxCrafting::before\t{ background-image: url('images/chat-icons/10.png'); }\n.chat-msg .user .icon.maxWC::before\t\t\t{ background-image: url('images/chat-icons/11.png'); }\n.chat-msg .user .icon.maxFarming::before\t{ background-image: url('images/chat-icons/12.png'); }\n.chat-msg .user .icon.maxBrewing::before\t{ background-image: url('images/chat-icons/13.png'); }\n.chat-msg .user .icon.maxCombat::before\t\t{ background-image: url('images/chat-icons/14.png'); }\n.chat-msg .user .icon.maxMagic::before\t\t{ background-image: url('images/chat-icons/15.png'); }\n.chat-msg .user .icon.maxFishing::before\t{ background-image: url('images/chat-icons/16.png'); }\n.chat-msg .user .icon.maxCooking::before\t{ background-image: url('images/chat-icons/17.png'); }\n.chat-msg .user .icon.maxLevel::before\t\t{ background-image: url('images/chat-icons/18.png'); }\n.chat-msg .user .icon.birdcage::before\t\t{ background-image: url('images/chat-icons/19.png'); }\n.chat-msg .user .icon.achievement::before\t{ background-image: url('images/chat-icons/20.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: calc(100% - 5px);\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 -5px -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 (win.isAutoScrolling != value)
			{
				toggleCheckbox.checked = value;
				win.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)
					{
						win.clearTimeout(showScrollTextTimeout);
					}
					showScrollTextTimeout = win.setTimeout(function ()
					{
						return win.scrollText(icon_2, color_1, text_1);
					}, 300);
				}
				else
				{
					win.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 - win.innerHeight;
			var referenceBottom = visibleBottom + win.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 - 1;
				setAutoScrolling(scrolled2Bottom, true);
			}
			var n = now();
			if (delayedScrollStart == null)
			{
				delayedScrollStart = n;
			}
			if (delayedScrollStart + 300 > n)
			{
				if (delayedScrollTimeout)
				{
					win.clearTimeout(delayedScrollTimeout);
				}
				delayedScrollTimeout = win.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 = win.sendChat;
		win.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' + win.username + '\\b', 'i');
		if (settings.getSub(settings.KEY.showNotifications, 'mention') && usernameRegex.test(lowerMsg))
		// if (lowerMsg.indexOf(win.username) > -1)
		{
			notifications.event('You\'ve been mentioned'
			, {
				body: data.msg
			});
		}
		var match = [];
		for (var _i = 0, keywordList_1 = chat.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 != win.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 ? win.addToChatBox : addToChatBox_;
		fn(username, icon, tag, msg, isPM);
	}
	chat.newAddToChatBox = newAddToChatBox;

	function openPmTab(username)
	{
		if (username == win.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_ = win.addToChatBox;
		win.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);
		win.$(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 = win.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'))
			{
				win.lookup(username);
			}
			else if (target.classList.contains('mute'))
			{
				if (username == '')
				{
					return;
				}
				win.mutedPeople.push(username);
				win.scrollText('none', 'lime', '<em>' + username + '</em> muted');
			}
			else if (target.classList.contains('unmute'))
			{
				if (username == '')
				{
					return;
				}
				var index = win.mutedPeople.indexOf(username);
				if (index !== -1)
				{
					win.mutedPeople.splice(index, 1);
				}
				win.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';
			}
		});
		win.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);

		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;
				}
			}
			return msgPrefix + msg;
		}
		var _sendChat = win.sendChat;
		win.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 = win.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;
			}
		}
		win.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
		// TEMP >>>
		// move pm entries to pm history
		var changed = false;
		for (var i = 0; i < chatHistory.length; i++)
		{
			var data = chatHistory[i];
			if (isDataPM(data))
			{
				chatHistory.splice(i, 1);
				i--;
				pmHistory.push(data);
				changed = true;
			}
		}
		if (changed)
		{
			saveChatHistory();
			savePmHistory();
		}
		// TEMP <<<
		chatHistory.forEach(function (d)
		{
			return add2Chat(d);
		});
		pmHistory.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)
		{
			for (var i = 0; i < spellbox.children.length; i++)
			{
				var child = spellbox.children.item(i);
				if (!win.isInCombat() && child.hasAttribute('onclick'))
				{
					child.dataset.onclick = child.getAttribute('onclick') || '';
					child.removeAttribute('onclick');
				}
				else if (win.isInCombat() && !!child.dataset.onclick)
				{
					child.setAttribute('onclick', child.dataset.onclick || '');
					child.dataset.onclick = '';
				}
			}
		}
	}
	// warn before unloading/reloading the tab if combat is in progress
	function combatWarnOnUnload()
	{
		if (!win.isInCombat())
		{
			win.onbeforeunload = null;
		}
		else
		{
			if (win.onbeforeunload == null)
			{
				win.onbeforeunload = function ()
				{
					return 'You are in a fight!';
				};
			}
		}
	}

	function fixCombatCountdown()
	{
		var el = document.getElementById('combat-countdown');
		if (!el)
		{
			return;
		}
		if (win.isInCombat())
		{
			el.style.display = '';
			var visible = win.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')
		{
			win.combatNotFightingTick();
		}
	}

	function fixClientGameLoop()
	{
		var _clientGameLoop = win.clientGameLoop;
		win.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()
	{
		// define missing properties for checking the needed materials
		win.enchantStargemPotionMagic = 0;
		win.changeWeatherMagic = 0;
		win.refreshLoadMagicTable = true;
		var _processMagicTab = win.processMagicTab;
		win.processMagicTab = function ()
		{
			var _refreshLoadCraftingTable = win.refreshLoadCraftingTable;
			win.refreshLoadCraftingTable = win.refreshLoadMagicTable;
			_processMagicTab();
			win.refreshLoadCraftingTable = _refreshLoadCraftingTable;
			if (win.magicPage3 == 1)
			{
				win.showMateriesNeededAndLevelLabelsMagic('enchantStargemPotion');
				win.showMateriesNeededAndLevelLabelsMagic('beam');
				win.showMateriesNeededAndLevelLabelsMagic('changeWeather');
			}
		};
	}

	function moveItemBox(itemKey, targetElId, color1, color2)
	{
		var itemBox = document.getElementById('item-box-' + itemKey);
		var targetContainer = document.getElementById(targetElId);
		targetContainer.appendChild(itemBox);
		// remove event listeners before binding the tooltip to it
		var $itemBox = win.$(itemBox);
		$itemBox.off('mouseover').off('mouseleave');
		itemBox.title = '';
		// bind tooltip to item box
		ensureTooltip('ingredient-secondary', itemBox);
		// change color
		itemBox.style.background = 'linear-gradient(' + color1 + ', ' + color2 + ')';
		$itemBox
			.mouseover(function ()
			{
				itemBox.style.background = 'none';
				itemBox.style.backgroundColor = color2;
			})
			.mouseleave(function ()
			{
				itemBox.style.background = 'linear-gradient(' + color1 + ', ' + color2 + ')';
			});
	}
	// move the strange leaf to brewing tab (thanks lasse_brus for this idea)
	function moveStrangeLeafs()
	{
		moveItemBox('strangeLeaf', 'tab-sub-container-brewing', '#800080', '#990099');
		moveItemBox('strangerLeaf', 'tab-sub-container-brewing', '#800080', '#990099');
	}
	// 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 = win.scrollTextHitSplat;
		win.scrollTextHitSplat = function (icon, color, text, elId, cbType)
		{
			_scrollTextHitSplat(fixIcon(icon), color, text, elId, cbType);
		};
		var _scrollText = win.scrollText;
		win.scrollText = function (icon, color, text)
		{
			_scrollText(fixIcon(icon), color, text);
		};
	}

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

	function fixHitText()
	{
		win.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 = win.$("<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 tooltipParent = boatTooltip && boatTooltip.parentElement;
		if (!boatBox || !boatTooltip || !tooltipParent)
		{
			return;
		}

		function setTripDuration(durationEl, boatKey)
		{
			var durationStr = TRIP_DURATION.hasOwnProperty(boatKey) ? TRIP_DURATION[boatKey].toString(10) : '?';
			durationEl.innerHTML = "<strong>Trip duration:</strong> " + durationStr + " hours";
		}
		boatTooltip.id = boatBox.dataset.tooltipId = 'tooltip-boundRowBoat';
		boatTooltip.appendChild(document.createElement('br'));
		var boatDuration = document.createElement('span');
		boatDuration.className = 'trip-duration';
		setTripDuration(boatDuration, 'rowBoat');
		boatTooltip.appendChild(boatDuration);
		for (var _i = 0, BOAT_LIST_1 = BOAT_LIST; _i < BOAT_LIST_1.length; _i++)
		{
			var boatKey = BOAT_LIST_1[_i];
			var boundKey = getBoundKey(boatKey);
			var itemBox = document.getElementById('item-box-' + boundKey);
			if (!itemBox)
			{
				continue;
			}
			var tooltip = document.getElementById('tooltip-' + boundKey);
			if (!tooltip)
			{
				tooltip = boatTooltip.cloneNode(true);
				tooltip.id = 'tooltip-' + boundKey;
				var header = tooltip.firstElementChild;
				header.textContent = capitalize(split2Words(boatKey));
				tooltipParent.appendChild(tooltip);
				itemBox.dataset.tooltipId = 'tooltip-' + boundKey;
			}
			var durationEl = tooltip.getElementsByClassName('trip-duration').item(0);
			if (durationEl)
			{
				setTripDuration(durationEl, boatKey);
			}
		}
	}

	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}\nimg.farming-patch-img\n{\n\twidth: 349px;\n\theight: 400px;\n}\n/* fix position of some plant images */\nimg.farming-patch-img[src$=\"/3_1.png\"]\n{\n\theight: 398px;\n\tmargin-top: -2px;\n\tmargin-bottom: 4px;\n}\nimg.farming-patch-img[src$=\"/3_2.png\"]\n{\n\theight: 399px;\n\tmargin-top: -1px;\n\tmargin-bottom: 2px;\n\tmargin-left: 2px;\n\tmargin-right: -2px;\n}\nimg.farming-patch-img[src$=\"/3_4.png\"]\n{\n\tmargin-top: 1px;\n\tmargin-bottom: -1px;\n\tmargin-left: -2px;\n\tmargin-right: 2px;\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 div.fight-spellbook\n{\n\tmargin: 0 -3px;\n}\n#hero-area span.fight-spell\n{\n\tmargin-bottom: 0;\n\tmargin-top: 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';
		win.$(attackCell).tooltip();
		var accuracyCell = statRow.cells.item(1);
		accuracyCell.title = 'Attack Accuracy';
		win.$(accuracyCell).tooltip();
		var speedCell = statRow.cells.item(2);
		speedCell.title = 'Attack Speed';
		win.$(speedCell).tooltip();
		var defenseCell = statRow.cells.item(3);
		defenseCell.title = 'Defense';
		win.$(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'
			, 'boundRocket'
			, '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() === ''))
			{
				if (textNode.nodeName === 'SPAN')
				{
					textNode = textNode.lastChild;
				}
				else
				{
					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 the list of quests'
			, 'achievementBook': 'Click to see the list of achievements'
			, 'boundEmptyChisel': 'Click to use'
			, 'rake': 'Click to upgrade your rake'
			, 'boundBoat': 'Click to send boat'
		};
		for (var item in texts)
		{
			setText(item, texts[item]);
		}
		for (var _b = 0, BOAT_LIST_2 = BOAT_LIST; _b < BOAT_LIST_2.length; _b++)
		{
			var boatKey = BOAT_LIST_2[_b];
			setText(getBoundKey(boatKey), 'Click to send boat');
		}
	}
	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;
		}
		win.$(el).css(css);
	}

	function fixTooltipPositioning()
	{
		win.changeTooltipPosition = changeTooltipPosition;
		win.loadTooltips();
	}

	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 fixPromethiumSmeltingTime()
	{
		var _getTimerPerBar = win.getTimerPerBar;
		win.getTimerPerBar = function (bar)
		{
			if (bar == 'promethiumBar')
			{
				return 80;
			}
			return _getTimerPerBar(bar);
		};
	}

	function fixImage()
	{
		var oxygenEl = document.querySelector('img[src="images/oxygenPotion"]');
		if (oxygenEl)
		{
			oxygenEl.src += '.png';
		}
	}

	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();
		fixCombatNavigation();
		fixPromethiumSmeltingTime();
		fixImage();
	}
	temporaryFixes.init = init;
})(temporaryFixes || (temporaryFixes = {}));

/**
 * improve timer
 */
var timer;
(function (timer)
{
	timer.name = 'timer';
	var IMPROVED_CLASS = 'improved';
	var NOTIFICATION_AREA_ID = 'notifaction-area';
	var PERCENT_CLASS = 'percent';
	var REMAINING_CLASS = 'remaining';
	var TIMER_CLASS = 'timer';

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

	function applyStyle()
	{
		addStyle("\nspan.notif-box." + IMPROVED_CLASS + "\n{\n\tposition: relative;\n}\nspan.notif-box." + IMPROVED_CLASS + " > span:not(." + TIMER_CLASS + "):not(." + REMAINING_CLASS + "):not(." + PERCENT_CLASS + ")\n{\n\tdisplay: none;\n}\nspan.notif-box." + IMPROVED_CLASS + " > span." + REMAINING_CLASS + ",\nspan.notif-box." + IMPROVED_CLASS + " > span." + PERCENT_CLASS + "\n{\n\tposition: absolute;\n\tleft: 10px;\n\tfont-size: 0.9rem;\n\tbottom: 0px;\n\twidth: 50px;\n\ttext-align: right;\n\ttext-shadow: 1px 1px 4px black;\n}\nspan.notif-box." + IMPROVED_CLASS + " > span." + REMAINING_CLASS + "::before\n{\n\tcontent: '\\0D7';\n\tmargin-right: .25rem;\n\tmargin-left: -.5rem;\n}\nspan.notif-box." + IMPROVED_CLASS + " > span." + PERCENT_CLASS + "::after\n{\n\tcontent: '%';\n}\n\t\t");
	}

	function improveSmeltingTimer()
	{
		var el = document.getElementById('notif-smelting');
		if (!el)
		{
			return;
		}
		var smeltingNotifBox = el;
		smeltingNotifBox.classList.add(IMPROVED_CLASS);
		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 = win.smeltingPercD;
			// thanks at /u/marcus898 for your bug report
			var elapsedTime = Math.round(win.smeltingPerc * totalTime / 100) + delta;
			smeltingTimerEl.textContent = format.timer(Math.max(totalTime - elapsedTime, 0));
			remainingBarsEl.textContent = (win.smeltingTotalAmount - win.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(win.getTreeName(treeId)) || 'Unknown Tree';
			var remainingTime = win.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 && win.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 (win.treeStage1 == 4
			|| win.treeStage2 == 4
			|| win.treeStage3 == 4
			|| win.treeStage4 == 4
			|| win.treeStage5 == 4
			|| win.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 = win.TREE_GROW_TIME[treeId - 1] - timerValue;
				minTimer = minTimer === null ? remainingTime : Math.min(minTimer, remainingTime);
			}
		}
		return minTimer || 0;
	}

	function getSoonestFarmingTimer()
	{
		if (win.farmingPatchStage1 == 0 || win.farmingPatchStage1 == 4
			|| win.farmingPatchStage2 == 0 || win.farmingPatchStage2 == 4
			|| win.farmingPatchStage3 == 0 || win.farmingPatchStage3 == 4
			|| win.farmingPatchStage4 == 0 || win.farmingPatchStage4 == 4
			|| win.donorFarmingPatch != 0 && (win.farmingPatchStage5 == 0 || win.farmingPatchStage5 == 4
				|| win.farmingPatchStage6 == 0 || win.farmingPatchStage6 == 4))
		{
			return -1;
		}
		var minTimer = null;
		for (var i = 1; i <= (win.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\tbackground-image: linear-gradient(#161618, #48ab32);\n\tmargin: -2px -5px -3px;\n\tpadding: 6px 5px 7px;\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}\nbody.short-tabs table.tab-bar td .info.timer\n{\n\tdisplay: none;\n}\ntable.tab-bar td .info.extra\n{\n\tcolor: white;\n\tpadding-top: 5px;\n\ttop: 0;\n}\nbody.short-tabs table.tab-bar td .info.extra\n{\n\tdisplay: none;\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 win.combatGlobalCooldown;
		});
		var energyDiv = addInfoDiv('combat');
		energyDiv.classList.add('extra');

		function updateEnergy()
		{
			energyDiv.innerHTML = '<img src="images/steak.png" class="image-icon-15"> ' + format.number(win.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();
		});
		observer.add('profileShortTabs', function ()
		{
			var short = !!win.profileShortTabs;
			document.body.classList[short ? 'add' : 'remove']('short-tabs');
		});
	}

	function addOilInfo()
	{
		var NULL_TYPE = 'null';
		var PLUS_TYPE = 'plus';
		var MINUS_TYPE = 'minus';
		addStyle("\n#oil-filling-level\n{\n\tbackground-color: black;\n\tborder: 1px solid white;\n\tdisplay: inline-block;\n\tposition: absolute;\n\tbottom: 0;\n\ttop: 0;\n\ttransform: translateX(-10px);\n\twidth: 8px;\n}\n#oil-filling-level > div\n{\n\tbackground-color: white;\n\twidth: 100%;\n}\n\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 container = document.createElement('div');
		container.id = 'oil-filling-level';
		var fillingLevel = document.createElement('div');
		container.appendChild(fillingLevel);
		var first = parent.firstElementChild;
		if (first)
		{
			parent.insertBefore(container, first);
		}
		else
		{
			parent.appendChild(container);
		}
		parent.style.position = 'relative';
		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 = win.oilIn - win.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);
		}
		var hour2Color = (_a = {}
			, // 30min
			_a[.5 * 60 * 60] = 'rgb(255, 0, 0)'
			, _a[5 * 60 * 60] = 'rgb(255, 255, 0)'
			, _a[8 * 60 * 60] = 'rgb(255, 255, 255)'
			, _a);

		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 = win.maxOil - win.oil;
				time = diff / oilNet;
			}
			else if (oilNet < 0)
			{
				netTimer.title = 'empty in...';
				time = win.oil / Math.abs(oilNet);
			}
			netTimer.textContent = '(' + format.timer(Math.ceil(time)) + ')';
			var filledPercent = win.oil / win.maxOil * 100;
			fillingLevel.style.height = (100 - filledPercent) + '%';
			/**
			 * colorize filling level according to the time it needs to be full/empty:
			 *	- red		iff oil storage full/empty in 30min
			 *	- yellow	iff oil storage full/empty in 5h
			 *	- white		iff oil storage full/empty in 8h or more
			 */
			var color = oilNet === 0 ? '#ffffff' : colorGenerator.getColorTransition(time, hour2Color);
			container.style.borderColor = color;
			if (init)
			{
				observer.add('maxOil', function ()
				{
					return updateFullTimer();
				});
				observer.add('oil', function ()
				{
					return updateFullTimer();
				});
				observer.addTick(function ()
				{
					return updateFullTimer();
				});
			}
		}
		updateNetFlow(true);
		var _a;
	}

	function addRocketTimer()
	{
		var notifArea = document.getElementById(NOTIFICATION_AREA_ID);
		if (!notifArea)
		{
			return;
		}
		var notifBox = document.createElement('span');
		notifBox.className = 'notif-box ' + IMPROVED_CLASS;
		notifBox.id = 'notif-rocket';
		notifBox.style.display = 'none';
		notifBox.innerHTML = "<img src=\"images/rocket.png\" class=\"image-icon-50\" id=\"notif-rocket-img\" style=\"margin-right: 10px;\"><span class=\"timer\" data-item-display=\"rocketTimer\"></span><span class=\"" + PERCENT_CLASS + "\" data-item-display=\"rocketPercent\"></span>";
		notifBox.title = 'This value is only an estimation based on an average speed of 15km per second.';
		notifArea.appendChild(notifBox);
		var img = notifBox.getElementsByTagName('img').item(0);
		var timerEl = notifBox.getElementsByClassName(TIMER_CLASS).item(0);
		var percentEl = notifBox.getElementsByClassName(PERCENT_CLASS).item(0);
		var AVG_KM_PER_SEC = 15;
		var smoothedTime = 0;

		function updateRocketKm()
		{
			var hideStatic = win.rocketKm < MAX_ROCKET_KM;
			var hideTimer = win.rocketKm <= 0 || !hideStatic;
			notifBox.style.display = hideTimer ? 'none' : '';
			var percent = win.rocketKm / MAX_ROCKET_KM;
			var diff = MAX_ROCKET_KM - win.rocketKm;
			if (win.rocketMoonId < 0)
			{
				percent = 1 - percent;
				diff = win.rocketKm;
			}
			var avgRemainingTime = Math.round(diff / AVG_KM_PER_SEC);
			// be more accurate in the last few seconds (may be the last 2 up to 16 seconds)
			var threshold = smoothedTime < 10 ? 1 : 8;
			if (Math.abs(smoothedTime - avgRemainingTime) >= threshold)
			{
				smoothedTime = avgRemainingTime + 1;
			}
			percentEl.textContent = Math.floor(percent * 100).toString();
		}

		function tickRocketTimer()
		{
			if (smoothedTime > 0)
			{
				smoothedTime = Math.max(smoothedTime - 1, 0);
				timerEl.textContent = format.timer(smoothedTime);
			}
		}
		updateRocketKm();
		observer.add('rocketKm', function (key, oldValue, newValue)
		{
			return updateRocketKm();
		});
		observer.addTick(function ()
		{
			return tickRocketTimer();
		});

		function updateRocketDirection()
		{
			// alternatively: `transform: rotateZ(180deg) rotateY(180deg)`
			var transform = win.rocketMoonId >= 0 ? '' : 'rotate(90deg)';
			img.style.transform = transform;
			var itemBox = document.getElementById('default-item-img-tag-boundRocket');
			if (itemBox)
			{
				itemBox.style.transform = transform;
			}
		}
		updateRocketDirection();
		observer.add('rocketMoonId', function ()
		{
			return updateRocketDirection();
		});
	}

	function getLogTypeList()
	{
		var list = [];
		var els = document.querySelectorAll('input[id^="input-charcoalFoundry-"]');
		for (var i = 0; i < els.length; i++)
		{
			list.push(els[i].id.replace(/^input-charcoalFoundry-/i, ''));
		}
		return list;
	}

	function improveFoundryTimer()
	{
		var el = document.getElementById('notif-charcoalFoundry');
		if (!el)
		{
			return;
		}
		var notifBox = el;
		notifBox.classList.add(IMPROVED_CLASS);
		var timerEl = document.createElement('span');
		timerEl.className = TIMER_CLASS;
		notifBox.appendChild(timerEl);
		var remainingEl = document.createElement('span');
		remainingEl.className = REMAINING_CLASS;
		notifBox.appendChild(remainingEl);
		var logTypeList = null;
		observer.add('charcoalFoundryN', function (key, oldValue, newValue)
		{
			timerEl.textContent = format.timer(win.charcoalFoundryD - win.charcoalFoundryN);
			// init log type list when needed
			if (!logTypeList)
			{
				logTypeList = getLogTypeList();
			}
			var woodAmount = win.charcoalFoundryTotal - win.charcoalFoundryCurrent;
			var coalPerLog = win.getCharcoalPerLog(logTypeList[win.charcoalFoundryLogId - 1]);
			var remainingCoal = woodAmount * (isNaN(coalPerLog) ? 1 : coalPerLog);
			remainingEl.textContent = remainingCoal.toString();
		});
	}

	function init()
	{
		bindNewFormatter();
		applyStyle();
		improveSmeltingTimer();
		improveTreeGrowTimer();
		improveSeedGrowTimer();
		addTabTimer();
		addOilInfo();
		addRocketTimer();
		improveFoundryTimer();
	}
	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;
			win.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 = win.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 = win.getTimerPerBar(win.selectedBar);
		timeEl.textContent = format.timer(timePerBar * num);
	}

	function init()
	{
		prepareAmountInput();
		prepareTimeNeeded();
		var _selectBar = win.selectBar;
		var updateSmeltingRequirements = function (bar, inputElement, inputBarsAmountEl, capacity)
		{
			_selectBar(bar, inputElement, inputBarsAmountEl, capacity);
			var matsArea = document.getElementById('dialogue-furnace-mats-needed');
			if (matsArea)
			{
				matsArea.innerHTML = format.numbersInText(matsArea.innerHTML);
			}
			updateTimeNeeded(inputBarsAmountEl.value);
		};
		win.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 = win.openFurnaceDialogue;
		win.openFurnaceDialogue = function (furnace)
		{
			var capacity = win.getFurnaceCapacity(furnace);
			if (win.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 ((!win.selectedBar || win.selectedBar == 'none') && lastBar != null)
			{
				win.selectedBar = lastBar;
			}
			// update whether requirements are fulfilled
			var barInputId = 'input-furnace-' + split2Words(win.selectedBar, '-').toLowerCase();
			var inputElement = document.getElementById(barInputId);
			if (inputElement && inputBarsAmountEl)
			{
				updateSmeltingRequirements(win.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(row.cells.length - 2);
			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 = win.clicksShovel;
		win.clicksShovel = function ()
		{
			_clicksShovel();
			var shovelChance = document.getElementById('dialogue-shovel-chance');
			var titleEl = shovelChance.parentElement;
			var chance = 1 / win.getChanceOfDiggingSand();
			addChanceTooltip('One sand at least every:', chance, 'shovel', titleEl);
		};
		// depends on fishingXp
		var _clicksFishingRod = win.clicksFishingRod;
		win.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);
			win.$(requirementCell).tooltip();
		}
	}

	function updateTooltipsOnReinitRecipes(key)
	{
		var capitalKey = capitalize(key);
		var processKey = 'process' + capitalKey + 'Tab';
		var _processTab = win[processKey];
		win[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 = win.addRecipeToBrewingTable;
		win.addRecipeToBrewingTable = function (brewingRecipe)
		{
			_addRecipeToBrewingTable(prepareRecipeForTable(brewingRecipe));
		};
		var _addRecipeToMagicTable = win.addRecipeToMagicTable;
		win.addRecipeToMagicTable = function (magicRecipe)
		{
			_addRecipeToMagicTable(prepareRecipeForTable(magicRecipe));
		};
		var _addRecipeToCooksBookTable = win.addRecipeToCooksBookTable;
		win.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);
		}
		var _rocketTick = win.rocketTick;
		win.rocketTick = function ()
		{
			_rocketTick();
			var rocketBox = document.getElementById('itembox-rocket');
			if (rocketBox && /^\d+\s*Km$/i.test(rocketBox.textContent || ''))
			{
				rocketBox.innerHTML = format.numbersInText(rocketBox.innerHTML).replace('Km', 'km');
			}
		};
	}
	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 = win.$(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++)
			{
				win.turnOn(machine, increment);
			}
		}
	}

	function init()
	{
		if (!settings.get(settings.KEY.changeMachineDialog))
		{
			return;
		}
		createSlider();
		var _openMachineryDialogue = win.openMachineryDialogue;
		win.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 getVialType(recipe)
	{
		return recipe.levelReq < 35 ? 'vialOfWater' : (recipe.levelReq < 65 ? 'largeVialOfWater' : 'hugeVialOfWater');
	}

	function getSimpleMax(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 getMax(recipe)
	{
		var max = getSimpleMax(recipe);
		if (/Potion$/.test(recipe.itemName))
		{
			var vialType = getVialType(recipe);
			max = Math.min(max, getGameValue(vialType));
		}
		return max;
	}

	function ensureNumberInput(idOrEl)
	{
		var numInput = typeof idOrEl === 'string' ? document.getElementById(idOrEl) : idOrEl;
		if (numInput)
		{
			if (numInput.type != 'number' && settings.get(settings.KEY.makeNumberInputs))
			{
				var width = numInput.clientWidth;
				if (width !== 0)
				{
					numInput.style.width = width + 'px';
				}
				numInput.type = 'number';
				numInput.min = '0';
				var onkeyup_1 = numInput.getAttribute('onkeyup');
				if (onkeyup_1)
				{
					numInput.setAttribute('onmouseup', onkeyup_1);
				}
			}
			else if (numInput.type == 'number' && !settings.get(settings.KEY.makeNumberInputs))
			{
				numInput.style.width = '';
				numInput.type = '';
				numInput.removeAttribute('onmouseup');
			}
		}
		return numInput;
	}

	function getCurrentMax(keyId, recipeCollection)
	{
		var keyEl = document.getElementById(keyId);
		if (!keyEl)
		{
			return 0;
		}
		var key = keyEl.value;
		return getMax(recipeCollection[key]);
	};

	function ensureMaxBtn(keyId, inputId, recipeCollection, key)
	{
		var recipe = recipeCollection[key];
		var numInput = ensureNumberInput(inputId);
		var next = numInput && numInput.nextElementSibling;
		var parent = numInput && numInput.parentElement;
		if (numInput && parent)
		{
			if ((!next || next.nodeName !== 'BUTTON') && settings.get(settings.KEY.addMaxBtn))
			{
				var btn = document.createElement('button');
				btn.textContent = 'Max';
				btn.addEventListener('click', function ()
				{
					numInput.value = getCurrentMax(keyId, recipeCollection).toString();
				});
				parent.appendChild(btn);
			}
			else if (next && next.nodeName === 'BUTTON' && !settings.get(settings.KEY.addMaxBtn))
			{
				parent.removeChild(next);
			}
			numInput.value = Math.min(1, getMax(recipe)).toString();
			numInput.select();
		}
	}

	function watchKeepInput(event)
	{
		var itemInput = document.getElementById('npc-sell-item-chosen');
		var numInput = ensureNumberInput('dialogue-input-cmd');
		if (!itemInput || !numInput)
		{
			return;
		}
		var item = itemInput.value;
		var newValue = Math.max(getGameValue(item) - Number(this.value), 0);
		numInput.value = newValue.toString();
	}

	function updateKeepMaxValue(keepInput, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var itemInput = document.getElementById('npc-sell-item-chosen');
		if (!itemInput)
		{
			return;
		}
		var item = itemInput.value;
		var max = getGameValue(item);
		keepInput.max = max.toString();
		if (init)
		{
			observer.addTick(function ()
			{
				return updateKeepMaxValue(keepInput);
			});
		}
	}

	function ensureKeepInput(item)
	{
		var numInput = ensureNumberInput('dialogue-input-cmd');
		var parent = numInput && numInput.parentElement;
		var next = numInput && numInput.nextElementSibling;
		var nextNext = next && next.nextElementSibling;
		if (next && nextNext && parent)
		{
			if (nextNext.nodeName === 'BR' && settings.get(settings.KEY.addKeepInput))
			{
				var div = document.createElement('div');
				var text = document.createTextNode('Keep: ');
				div.appendChild(text);
				var keepInput = document.createElement('input');
				keepInput.type = 'number';
				keepInput.value = keepInput.min = '0';
				keepInput.max = getGameValue(item).toString();
				keepInput.addEventListener('keyup', watchKeepInput);
				keepInput.addEventListener('mouseup', watchKeepInput);
				updateKeepMaxValue(keepInput, true);
				div.appendChild(keepInput);
				parent.insertBefore(div, nextNext);
			}
			else if (nextNext.nodeName !== 'BR' && !settings.get(settings.KEY.addKeepInput))
			{
				var br = document.createElement('br');
				parent.insertBefore(br, nextNext);
				parent.removeChild(nextNext);
			}
		}
	}

	function init()
	{
		var _multiCraft = win.multiCraft;
		win.multiCraft = function (item)
		{
			_multiCraft(item);
			ensureMaxBtn('dialogue-multicraft-chosen', 'dialogue-multicraft-input', win.craftingRecipes, item);
		};
		var _brew = win.brew;
		win.brew = function (potion)
		{
			_brew(potion);
			ensureMaxBtn('dialogue-potion-chosen', 'dialogue-brewing-input', win.brewingRecipes, potion);
		};
		var _cooksBookInputDialogue = win.cooksBookInputDialogue;
		win.cooksBookInputDialogue = function (food)
		{
			_cooksBookInputDialogue(food);
			ensureMaxBtn('dialogue-cooksBook-chosen', 'dialogue-cooksBook-input', win.cooksBookRecipes, food);
		};
		var _openSellNPCDialogue = win.openSellNPCDialogue;
		win.openSellNPCDialogue = function (item)
		{
			_openSellNPCDialogue(item);
			ensureKeepInput(item);
		};
		var allowedInputs = [
			'dialogue-ashes'
			, 'dialogue-bindDonorCoins'
			, 'dialogue-bonemeal'
			, 'dialogue-bones'
			, 'dialogue-brewing'
			, 'dialogue-buy-item-2'
			, 'dialogue-buyFromMarket'
			, 'dialogue-charcoalFoundry'
			, 'dialogue-consume'
			, 'dialogue-cooksBook'
			, 'dialogue-createArrows'
			, 'dialogue-createFireArrows'
			, 'dialogue-createIceArrows'
			, 'dialogue-furnace'
			, 'dialogue-iceBones'
			, 'dialogue-id-boundHammer'
			, 'dialogue-id-boundPickaxe'
			, 'dialogue-id-cook-food'
			, 'dialogue-id-oven-addheat'
			, 'dialogue-market-chosenpostitem'
			, 'dialogue-multicraft'
			, 'dialogue-oilBarrels'
			, 'dialogue-oilFactory'
			, 'dialogue-sell-item'
			, 'dialogue-stardustCrystals'
			, 'dialogue-wand'
		];
		var _openDialogue = win.openDialogue;
		win.openDialogue = function (id, width, position)
		{
			_openDialogue(id, width, position);
			if (allowedInputs.indexOf(id) === -1
				|| id === 'dialogue-buyFromMarket' && market.detectTedsUI()
				|| id === 'dialogue-market-chosenpostitem' && market.detectTedsUI())
			{
				return;
			}
			var dialog = document.getElementById(id);
			var input = dialog && dialog.querySelector('input[type="text"],input[type="number"]');
			if (!input)
			{
				return;
			}
			ensureNumberInput(input);
		};
	}
	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 createPipeNode()
	{
		return document.createTextNode('|');
	}

	function addLinkEntry(el)
	{
		if (!linkCell)
		{
			addQueues.link.push(el);
		}
		else
		{
			linkCell.appendChild(createPipeNode());
			linkCell.appendChild(el);
		}
	}
	newTopbar.addLinkEntry = addLinkEntry;

	function addTabEntry(el)
	{
		if (!tabCell)
		{
			addQueues.tab.push(el);
		}
		else
		{
			tabCell.appendChild(createPipeNode());
			tabCell.appendChild(el);
		}
	}
	newTopbar.addTabEntry = addTabEntry;

	function addInfoEntry(el)
	{
		if (!infoCell)
		{
			addQueues.info.push(el);
		}
		else
		{
			if (infoCell.firstChild)
			{
				infoCell.insertBefore(createPipeNode(), infoCell.firstChild);
				infoCell.insertBefore(el, infoCell.firstChild);
			}
			else
			{
				infoCell.appendChild(createPipeNode());
				infoCell.appendChild(el);
			}
		}
	}
	newTopbar.addInfoEntry = addInfoEntry;

	function init()
	{
		if (!settings.get(settings.KEY.useNewToolbar))
		{
			return;
		}
		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, 5];
		var infoIdx = [6, 7];
		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(createPipeNode());
			}
			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 = win.openTab;
		win.openTab = function (newTab)
		{
			var oldTab = win.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';
	var bodyRegex = /(\bbody)(\s|$)/i;

	function addTweakStyle(setting, style)
	{
		if (setting != '')
		{
			var prefix_1 = setting === '' ? '' : 'body.' + setting + ' ';
			style = style
				.replace(/(^\s*|\}\s*)([^\{\}]+)(?=\s*\{)/g, function (wholeMatch, before, rules)
				{
					return before + rules.split(',').map(function (rule)
					{
						if (bodyRegex.test(rule) && setting !== '')
						{
							return rule.replace(bodyRegex, '$1.' + setting + '$2');
						}
						return rule.replace(/^(\s*\n\s*)?/, '$1' + prefix_1);
					}).join(',');
				});
			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 = win.loadSkillTabs;
		win.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 = win.getXpNeeded(win.getLevel(xp));
				var nextLevelXp = win.getXpNeeded(win.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 win.loadSkillTabs();
			});
		}
		win.loadSkillTabs();
	}
	// highlight cooking level requirement when not matched
	function highlightCookinglevel()
	{
		var _cookFoodDialogue = win.cookFoodDialogue;
		win.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 = win.getCookingLevelReq(rawFood) > win.getLevel(win.cookingXp);
			levelReq.style.color = fulfilled ? 'rgb(204, 0, 0)' : '';
			levelReq.style.fontWeight = fulfilled ? 'bold' : '';
			levelReqLabel.style.color = fulfilled ? 'rgb(204, 0, 0)' : '';
			var ratioEl = document.getElementById('dialogue-cook-ratio');
			if (!ratioEl)
			{
				var cookReqBox = levelReq.parentElement;
				var br = document.createElement('br');
				cookReqBox.appendChild(br);
				var b = document.createElement('b');
				b.innerHTML = "<img src=\"images/steak.png\" class=\"image-icon-20\" title=\"Energy\"> per <img src=\"images/icons/fire.png\" class=\"image-icon-20\" title=\"Heat\">: ";
				cookReqBox.appendChild(b);
				ratioEl = document.createElement('span');
				ratioEl.id = 'dialogue-cook-ratio';
				cookReqBox.appendChild(ratioEl);
			}
			var heat = win.getHeatNeeded(rawFood);
			var energy = win.getEnergyGained(rawFood);
			ratioEl.textContent = format.number(Math.round(energy / heat * 100) / 100);
		};
	}

	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, "\nbody\n{\n\tmargin: 0;\n}\nbody > br\n{\n\tdisplay: none;\n}\ntable.top-links\n{\n\tborder-left-width: 0px;\n\tborder-right-width: 0px;\n}\n#game-div\n{\n\tmargin-top: 29px;\n}\n#game-div > table.top-bar,\n#game-div > table.tab-bar,\n#div-chat\n{\n\tborder-width: 0;\n\tmargin-top: 0;\n}\n#game-div > table.top-bar#top-bar-levels\n{\n\tborder-width: 1px 0;\n}\n#notifaction-area\n{\n\tpadding: 0;\n}\nspan.notif-box\n{\n\tmargin: -1px;\n\tmargin-left: 0;\n\tpadding: 5px;\n}\n#game-div > div.tab-container\n{\n\tborder-width: 1px 0 0;\n\tpadding: 0;\n}\ndiv.tab-container > h1.container-title:first-child\n{\n\tdisplay: none;\n}\ndiv.item-box-area,\n#tab-sub-container-farming,\n#tab-sub-container-magic-items\n{\n\tmargin: 0;\n\tpadding: 1px 1px 0 0;\n}\nspan.item-box\n{\n\tmargin: -1px -1px 0 0;\n}\ndiv.tab-container > center > table.table-default,\n#table-crafting-recipe,\n#table-brewing-recipe,\n#table-magic-recipe\n{\n\twidth: calc(100% - 1px);\n}\nul.settings-container,\n#tab-container-crafting .settings-container\n{\n\tmargin: 0;\n}\ndiv.tab-container > br:last-child,\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-sub-container-spells > 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\tborder-width: 0;\n\tmargin: 0;\n}\n\n#combat-table-area\n{\n\tborder-width: 0;\n}\n#combat-table-area > tbody > tr > td\n{\n\tborder-width: 0;\n\tborder-right-width: 1px;\n}\n#combat-table-area > tbody > tr > td:last-child\n{\n\tborder-right-width: 0;\n}\n#combat-table-area span.large-button,\n#combat-table-area span.medium-button\n{\n\tmargin: 2px 2px 4px;\n}\n#combat-loot-tables\n{\n\tmargin-top: -3px;\n}\n#combat-loot-tables > table.hiscores-table\n{\n\tmargin: 2px -1px 0 0;\n\twidth: calc(33.33% - 4px);\n}\n#combat-loot-tables > div[style*=\"both\"]\n{\n\theight: 0px;\n}\n\t\t");
		var farmingTab = document.getElementById('tab-container-farming');
		if (farmingTab)
		{
			removeWhitespaceChildNodes(farmingTab);
		}
		var combatSubTab = document.getElementById('tab-sub-container-combat');
		if (combatSubTab)
		{
			removeWhitespaceChildNodes(combatSubTab);
		}

		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 hardcore()
	{
		if (win.isHardcore != 1)
		{
			return;
		}
		addStyle("\nspan#shop-giant-button-playermarket\n{\n\tbackground-color: gray;\n\tbackground-image: none;\n\tcursor: not-allowed;\n}\n\t\t");
		var marketBtn = document.getElementById('shop-giant-button-playermarket');
		if (marketBtn)
		{
			marketBtn.removeAttribute('onclick');
			marketBtn.setAttribute('title', 'The player market is disabled for hardcore accounts');
		}
	}

	function smallScreen()
	{
		addStyle("\ntable.top-links\n{\n\tz-index: 10;\n}\n\t\t");
	}

	function init()
	{
		tweakOil();
		tweakSelection();
		tweakStardust();
		tweakSkillLevelText();
		tweakFightDialog();
		addAdditionalSkillBars();
		highlightCookinglevel();
		amountStyle();
		efficiency();
		hardcore();
		smallScreen();
	}
	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 = win.workersTimer === 1;
			notifBox.style.display = show ? '' : 'none';
		}
		setVisibility();
		observer.add('workersTimer', function ()
		{
			return setVisibility();
		});
	}

	function init()
	{
		addStyle("\n#notifaction-area\n{\n\tpadding: 5px 0;\n}\nspan.notif-box\n{\n\tfont-size: 1rem;\n\tmargin: 0;\n\tmargin-right: 5px;\n}\ntable.tab-bar\n{\n\tmargin-top: 0;\n}\nspan.notif-box,\ntable.tab-bar td\n{\n\tborder-color: gray;\n}\nspan.notif-box[id^=\"notification-static-\"]\n{\n\tbackground: linear-gradient(rgb(22, 22, 24), rgb(72, 171, 50));\n}\n\t\t");
		// remove pure text nodes
		var notifArea = document.getElementById('notifaction-area');
		if (notifArea)
		{
			removeWhitespaceChildNodes(notifArea);
		}
		addWorker();
	}
	notifBoxes.init = init;
})(notifBoxes || (notifBoxes = {}));


/**
 * extend market
 */
var market;
(function (market)
{
	market.name = 'market';
	// max limit age: 5min
	var MAX_LIMIT_AGE = 5 * 60 * 1e3;
	var PRICE_HISTORY_KEY = 'priceHistory';
	// restrict the size of the history of each item to 2000 entries (for a number comparison: 1 entry per minute, would result in 1440 entries per day)
	var MAX_ENTRIES_PER_ITEM = 2e3;
	var SYNC_URL_REGEX = /^(?:https?:\/\/)?(?:(?:www\.)?myjson\.com\/|api\.myjson\.com\/bins\/)([^\/]+)$/i;
	var detectedTedsUIOnce = false;

	function detectTedsUI()
	{
		return detectedTedsUIOnce = detectedTedsUIOnce || typeof win.changeSetting === 'function';
	}
	market.detectTedsUI = detectTedsUI;
	var priceHistory = store.has(PRICE_HISTORY_KEY) ? store.get(PRICE_HISTORY_KEY) :
	{};
	var getItemColor = function (H, S, L)
	{
		return [
			"hsl(" + H + ", " + S + "%, " + L + "%)"
			, "hsl(" + H + ", " + S + "%, " + (L < 35 ? L + 35 : L - 35) + "%)"
		];
	};
	var itemColor = {
		'blewitMushroom': getItemColor(255, 100, 78)
		, 'bronzeBar': getItemColor(39, 100, 46)
		, 'crystalLeaf': getItemColor(226, 100, 50)
		, 'diamond': getItemColor(186, 76, 82)
		, 'dottedGreenLeaf': getItemColor(92, 63, 19)
		, 'emerald': getItemColor(110, 100, 48)
		, 'goldLeaf': getItemColor(50, 100, 50)
		, 'goldBar': getItemColor(54, 100, 46)
		, 'greenLeaf': getItemColor(92, 63, 28)
		, 'ironBar': getItemColor(44, 11, 46)
		, 'limeLeaf': getItemColor(110, 72, 40)
		, 'promethiumBar': getItemColor(354, 81, 46)
		, 'redMushroom': getItemColor(0, 83, 48)
		, 'ruby': getItemColor(5, 87, 45)
		, 'sapphire': getItemColor(197, 100, 32)
		, 'shrimp': getItemColor(17, 88, 50)
		, 'silverBar': getItemColor(0, 0, 74)
		, 'snapegrass': getItemColor(120, 99, 42)
		, 'stardust': getItemColor(37, 100, 50)
		, 'strangeLeaf': getItemColor(195, 100, 40)
	};
	// use ambassadors to name the categories
	var categoryAmbassador2CategoryName = {
		'stone': 'Ores' // 0
		, 'emptyChisel': 'Crystals' // 1
		, 'bronzeBar': 'Bars' // 2
		, 'dottedGreenLeafSeeds': 'Seeds' // 3
		, 'logs': 'Logs' // 4
		, 'dottedGreenLeaf': 'Ingredients' // 5
		, 'rawShrimp': 'Fish' // 6
		, 'shrimp': 'Food' // 7
		, 'stinger': 'Equipment' // 8
		, 'promethiumHelmetMould': 'Mould' // 9
		, 'essence': 'Magic' // 10
		, 'blueFishingRodOrb': 'Orbs' // 11
		, 'stardust': 'Other' // 12
	};
	var item2Category = new Map();
	var category2Name = new Map();
	var item2Resolver = new Map();
	var itemLimits = new Map();
	var offerPerItem = new Map();
	var offerList = new Array();
	var lastSyncValue = '{}';

	function getSyncUrl()
	{
		if (!settings.get(settings.KEY.syncPriceHistory))
		{
			return null;
		}
		var url = settings.getSub(settings.KEY.syncPriceHistory, 'url');
		var match = url.match(SYNC_URL_REGEX);
		if (!match)
		{
			console.error('URL "' + url + '" does not match the expected pattern: ' + SYNC_URL_REGEX.source);
			return null;
		}
		return 'https://api.myjson.com/bins/' + match[1];
	}

	function integratePriceData(data, responseText)
	{
		var changed = recIntegrate(data, priceHistory);
		lastSyncValue = responseText;
		return changed;

		function recIntegrate(source, target)
		{
			var changed = false;
			if (typeof source !== typeof target)
			{
				console.error('Different data types. Could not integrate data into local price history.\nsource: ' + JSON.stringify(source) + '\ntarget: ' + JSON.stringify(target));
			}
			else if (typeof source === 'object')
			{
				for (var key in source)
				{
					if (source.hasOwnProperty(key))
					{
						if (!target.hasOwnProperty(key))
						{
							target[key] = source[key];
							changed = true;
						}
						else if (recIntegrate(source[key], target[key]))
						{
							changed = true;
						}
					}
				}
			}
			else
			{
				// do nothing and prefer the local value
			}
			return changed;
		}
	}

	function loadPriceHistory()
	{
		var url = getSyncUrl();
		if (url)
		{
			win.$.get(url, function (data, textStatus, jqXHR)
			{
				if (integratePriceData(data, jqXHR.responseText))
				{
					savePriceHistory(true);
				}
			});
		}
	}

	function savePriceHistory(forceWrite)
	{
		if (forceWrite === void 0)
		{
			forceWrite = false;
		}
		for (var itemKey in priceHistory)
		{
			var history_1 = priceHistory[itemKey];
			var timestampList = Object.keys(history_1).sort();
			var i = 0;
			for (var _i = 0, timestampList_1 = timestampList; _i < timestampList_1.length; _i++)
			{
				var timestamp = timestampList_1[_i];
				i++;
				if (i > MAX_ENTRIES_PER_ITEM)
				{
					delete history_1[timestamp];
				}
			}
		}
		store.set(PRICE_HISTORY_KEY, priceHistory);
		var url = getSyncUrl();
		if (url)
		{
			var doPut_1 = function ()
			{
				$.ajax(
				{
					url: url
					, type: 'PUT'
					, data: JSON.stringify(priceHistory)
					, contentType: 'application/json; charset=utf-8'
					, dataType: 'json'
					, success: function (data, textStatus, jqXHR)
					{
						lastSyncValue = jqXHR.responseText;
					}
				});
			};
			if (forceWrite === true)
			{
				doPut_1();
			}
			else
			{
				win.$.get(url, function (data, textStatus, jqXHR)
				{
					if (lastSyncValue !== jqXHR.responseText)
					{
						integratePriceData(data, jqXHR.responseText);
					}
					doPut_1();
				});
			}
		}
	}

	function processMarketData(data)
	{
		var nowKey = now();
		offerPerItem = new Map();
		offerList = new Array();
		if (data != 'NONE')
		{
			offerList = data.split(';').map(function (offerData)
			{
				var values = offerData.split('~');
				var itemId = Number(values[1]);
				var itemKey = win.jsItemArray[itemId];
				var itemName = key2Name(itemKey);
				var categoryId = item2Category.has(itemKey) ? item2Category.get(itemKey) : -1;
				var offer = {
					offerId: Number(values[0])
					, itemId: itemId
					, itemKey: itemKey
					, itemName: itemName
					, categoryId: categoryId
					, amount: Number(values[2])
					, price: Number(values[3])
					, timeLeft: values[4]
					, playerId: Number(values[5])
				};
				if (!offerPerItem.has(itemKey))
				{
					offerPerItem.set(itemKey, []);
				}
				offerPerItem.get(itemKey).push(offer);
				var history = priceHistory[itemKey];
				if (!history)
				{
					history = {};
					priceHistory[itemKey] = history;
				}
				if (!history.hasOwnProperty(nowKey)
					|| history[nowKey] > offer.price)
				{
					history[nowKey] = offer.price;
				}
				return offer;
			});
		}
		savePriceHistory();
	}

	function processItemLimits(itemKey, lowerLimit, upperLimit)
	{
		var limit = {
			timestamp: now()
			, min: lowerLimit
			, max: upperLimit
		};
		itemLimits.set(itemKey, limit);
		if (item2Resolver.has(itemKey))
		{
			var limitArr_1 = [lowerLimit, upperLimit];
			item2Resolver.get(itemKey).forEach(function (resolve)
			{
				return resolve(limitArr_1);
			});
			item2Resolver.delete(itemKey);
			return false;
		}
		return true;
	}

	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 addExtraBtns()
	{
		var browseBtn = document.querySelector('.market-browse-button');
		if (!browseBtn)
		{
			return;
		}
		var HISTORY_CLASS = 'local-history';
		var paddingLeft = 30 + 42;
		var paddingRight = 30;
		addStyle("\ncenter > span.market-browse-button,\n#ted-market-ui > span.market-browse-button\n{\n\tposition: relative;\n}\ncenter > span.market-browse-button\n{\n\tpadding-left: " + paddingLeft + "px;\n\tpadding-right: " + paddingRight + "px;\n}\nspan.market-browse-button > span.market-browse-button\n{\n\tpadding: 10px 20px;\n\tposition: absolute;\n\ttop: -1px;\n\tbottom: -1px;\n}\n/*\n*/\nspan.market-browse-button > span.market-browse-button." + HISTORY_CLASS + "\n{\n\tleft: -1px;\n}\nspan.market-browse-button > span.market-browse-button::before\n{\n\tbackground-color: transparent;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\tbackground-size: 30px;\n\tcontent: '';\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\tbottom: 0;\n\tright: 0;\n}\n/*\n*/\nspan.market-browse-button." + HISTORY_CLASS + "::before\n{\n\tbackground-image: " + icons.getMd(icons.CHART_LINE) + ";\n}\n/*\n*/\n#ted-market-ui > span.market-browse-button > span.market-browse-button." + HISTORY_CLASS + "\n{\n\tborder-left: 0;\n\tleft: 0;\n}\n\t\t");
		var historyBtn = document.createElement('span');
		historyBtn.className = 'market-browse-button ' + HISTORY_CLASS;
		browseBtn.appendChild(historyBtn);
		var historyItemKey = null;
		var _postItemDialogue = win.postItemDialogue;
		win.postItemDialogue = function (offerTypeEl, itemName, inputEl)
		{
			historyItemKey = itemName;
			_postItemDialogue(offerTypeEl, itemName, inputEl);
		};
		var PRICE_HISTORY_DIALOG_ID = 'dialog-price-history';
		var PRICE_HISTORY_ID = 'price-history';
		var PRICE_HISTORY_ITEM_SELECT_ID = 'price-history-item-select';
		addStyle("\n#" + PRICE_HISTORY_DIALOG_ID + "\n{\n\tdisplay: flex;\n\tflex-direction: column;\n}\n#" + PRICE_HISTORY_ID + "\n{\n\tflex-grow: 1;\n\tposition: relative;\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n#" + PRICE_HISTORY_ID + " > div\n{\n\tposition: absolute !important;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n}\n#" + PRICE_HISTORY_ID + " .anychart-credits\n{\n\tdisplay: none;\n}\n\t\t");
		var dialog = document.createElement('dialog');
		dialog.id = PRICE_HISTORY_DIALOG_ID;
		dialog.style.display = 'none';
		dialog.style.overflowX = 'hidden';
		dialog.innerHTML = "\n\t\t<select id=\"" + PRICE_HISTORY_ITEM_SELECT_ID + "\" multiple=\"multiple\" data-placeholder=\"Add items\" style=\"width: 100%\"></select>\n\t\t<div id=\"" + PRICE_HISTORY_ID + "\"></div>\n\t\t";
		document.body.appendChild(dialog);
		var itemSelect = document.getElementById(PRICE_HISTORY_ITEM_SELECT_ID);
		var $itemSelect = win.$(itemSelect);

		function loadScripts(urlList, callback)
		{
			var url = urlList[0];
			if (!url)
			{
				callback && callback();
				return;
			}
			var script = document.createElement('script');
			script.src = url;
			script.onload = function ()
			{
				return loadScripts(urlList.slice(1), callback);
			};
			document.head.appendChild(script);
		}
		var monthArray = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
		var lastHistoryItemKey;
		var itemKey2SeriesId = {};
		var chart;
		var stage;
		// add style for select2
		var style = document.createElement('link');
		style.rel = 'stylesheet';
		style.href = 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css';
		document.head.appendChild(style);
		loadScripts([
			'https://cdn.anychart.com/js/7.14.3/anychart.min.js'
			, 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
		], function ()
		{
			chart = win.anychart.area();
			// pass the container id, chart will be displayed there
			stage = anychart.graphics.create(PRICE_HISTORY_ID);
			chart.container(stage);
			var tooltip = chart.tooltip();
			tooltip.displayMode('union');
			tooltip.format(passThis(function (context)
			{
				var name = context.seriesName || 'Price';
				return name + ': ' + (isNaN(context.value) ? '-' : format.number(context.value));
			}));
			tooltip.titleFormat(passThis(function (context)
			{
				var d = new Date(context.x);
				return monthArray[d.getMonth()] + ' ' + d.getDate() + ' @ ' + zeroPadLeft(d.getHours()) + ':' + zeroPadLeft(d.getMinutes()) + ':' + zeroPadLeft(d.getSeconds());
			}));
			var valueAxis = chart.yAxis();
			valueAxis.title().text('Price').enabled(true);
			valueAxis.labels().format(passThis(function (context)
			{
				return format.number(context.value);
			}));
			var timeAxis = chart.xAxis();
			timeAxis.labels().format(passThis(function (context)
			{
				var d = new Date(context.tickValue);
				return d.getDate() + '. ' + monthArray[d.getMonth()];
			}));
			var timeScale = win.anychart.scales.dateTime();
			var ticks = timeScale.ticks();
			ticks.interval(0, 0, 1);
			chart.xScale(timeScale);
			var timeScroller = chart.xScroller();
			timeScroller.enabled(true);
			chart.animation(true, 300);
			chart.legend(true);
		});
		historyBtn.addEventListener('click', function (event)
		{
			event.preventDefault();
			event.stopPropagation();
			var height = Math.floor(.66 * window.innerHeight);
			var width = Math.min(Math.floor(.66 * window.innerWidth), window.innerWidth - 30);
			win.$(dialog).dialog(
			{
				title: 'Price history from local data'
				, height: height
				, width: width
			});
			dialog.style.height = (height) + 'px';
			var itemKeyList = Object.keys(priceHistory).sort();
			itemSelect.innerHTML = "";
			var category2OptGroup = {};

			function ensureOptGroup(categoryId)
			{
				var optGroup = category2OptGroup[categoryId];
				if (!optGroup)
				{
					optGroup = document.createElement('optgroup');
					optGroup.label = category2Name.get(categoryId) || 'Stuff';
					itemSelect.appendChild(optGroup);
					category2OptGroup[categoryId] = optGroup;
				}
				return optGroup;
			}
			var categoryList = Array.from(category2Name.keys()).map(function (id)
			{
				return Number(id);
			}).sort();
			for (var _i = 0, categoryList_1 = categoryList; _i < categoryList_1.length; _i++)
			{
				var categoryId = categoryList_1[_i];
				ensureOptGroup(categoryId);
			}
			var itemKey2EnabledFn = {};

			function replaceEnabled(itemKey, series)
			{
				var _enabled = series.enabled.bind(series);
				itemKey2EnabledFn[itemKey] = _enabled;
				series.enabled = function (value)
				{
					if (value !== undefined)
					{
						var itemList = $itemSelect.val();
						var index = itemList.indexOf(itemKey);
						if (index !== -1)
						{
							itemList.splice(index, 1);
						}
						else
						{
							itemList.push(itemKey);
						}
						$itemSelect.val(itemList).trigger('change');
					}
					return _enabled(value);
				};
			}
			var min = Number.MAX_SAFE_INTEGER;
			var max = 0;
			var enabledSeriesList = [];
			var _loop_1 = function (itemKey)
			{
				if (!itemColor[itemKey])
				{
					var baseColor = colorGenerator.getRandom(
					{
						format: 'hslArray'
					});
					var borderColor = baseColor.slice(0);
					if (borderColor[2] < 35)
					{
						borderColor[2] += 35;
					}
					else
					{
						borderColor[2] -= 35;
					}
					itemColor[itemKey] = [
						"hsl(" + baseColor[0] + ", " + baseColor[1] + "%, " + baseColor[2] + "%)"
						, "hsl(" + borderColor[0] + ", " + borderColor[1] + "%, " + borderColor[2] + "%)"
					];
				}
				var history_2 = priceHistory[itemKey];
				var keyList = Object.keys(history_2).sort();
				var data = keyList
					.map(function (n)
					{
						return ([
							Number(n)
							, history_2[n]
						]);
					});
				min = Math.min(Number(keyList[0]), min);
				max = Math.max(Number(keyList[keyList.length - 1]), max);
				var id = itemKey2SeriesId[itemKey];
				var series = void 0;
				if (id != null)
				{
					series = chart.getSeries(id);
					series.data(data);
				}
				else
				{
					var hoverifyColor = function (hslColor)
					{
						return (
						{
							color: hslColor
							, opacity: .8
						});
					};
					series = chart.area(data);
					itemKey2SeriesId[itemKey] = series.id();
					series.name(key2Name(itemKey));
					var bgColor = itemColor[itemKey][0];
					var strokeColor = itemColor[itemKey][1];
					series.fill(bgColor);
					var bgColorHover = hoverifyColor(bgColor);
					series.selectFill(bgColorHover);
					series.hoverFill(bgColorHover);
					series.stroke(strokeColor, 2);
					var strokeColorHover = hoverifyColor(strokeColor);
					series.hoverStroke(strokeColorHover, 2);
					series.selectStroke(strokeColorHover, 2);
					var markerOptions = {
						fill: strokeColor
						, size: 5
						, type: 'circle'
					};
					series.hoverMarkers(markerOptions);
					series.selectMarkers(markerOptions);
					replaceEnabled(itemKey, series);
				}
				if (lastHistoryItemKey !== historyItemKey)
				{
					if (itemKey === historyItemKey)
					{
						enabledSeriesList.push(series);
					}
					series.enabled(false);
				}
				var categoryId = item2Category.has(itemKey) ? item2Category.get(itemKey) : -1;
				var optGroup = ensureOptGroup(categoryId);
				var option = document.createElement('option');
				option.value = itemKey;
				option.textContent = key2Name(itemKey);
				optGroup.appendChild(option);
			};
			for (var _a = 0, itemKeyList_1 = itemKeyList; _a < itemKeyList_1.length; _a++)
			{
				var itemKey = itemKeyList_1[_a];
				_loop_1(itemKey);
			}
			stage.listenOnce('renderfinish', function ()
			{
				enabledSeriesList.forEach(function (series)
				{
					return series.enabled(true);
				});
			});
			var timeScale = chart.xScale();
			timeScale.minimum(min);
			timeScale.maximum(max);
			var timeZoom = chart.xZoom();
			var threeDaysLong = 3 * 24 * 60 * 60 * 1e3;
			timeZoom.setToValues(Math.max(max - threeDaysLong, min), max);
			// call the chart draw() method to initiate chart display
			chart.draw(true);
			// init item select
			if ($itemSelect.data('select2'))
			{
				$itemSelect.select2('destroy');
			}
			$itemSelect.select2();

			function getEnabledFn(event)
			{
				var data = event.params.data;
				var itemKey = data.id;
				var enabledFn = itemKey2EnabledFn[itemKey];
				if (enabledFn)
				{
					return enabledFn;
				}
				else
				{
					var id = itemKey2SeriesId[itemKey];
					var series = chart.getSeries(id);
					return series.enabled.bind(series);
				}
			}
			$itemSelect.on('select2:select', function (event)
			{
				getEnabledFn(event)(true);
			});
			$itemSelect.on('select2:unselect', function (event)
			{
				getEnabledFn(event)(false);
				// close select menu when it was closed before an element has been removed
				var openBefore = $itemSelect.data('select2').$container.hasClass('select2-container--open');
				setTimeout(function ()
				{
					if (!openBefore && $itemSelect.data('select2').$container.hasClass('select2-container--open'))
					{
						$itemSelect.select2('close');
					}
				});
			});
			lastHistoryItemKey = historyItemKey;
		});
	}
	var categoryList = [-1];
	var itemListPerCategory = new Map();

	function improveOfferList()
	{
		var itemArea = document.getElementById('dialogue-market-items-area');
		if (itemArea)
		{
			var children = itemArea.children;
			for (var i = 1; i < children.length; i++)
			{
				var categoryId = i - 1;
				categoryList.push(categoryId);
				var box = children.item(i);
				var inputs = box.children;
				for (var j = 0; j < inputs.length; j++)
				{
					var match = inputs.item(j).src.match(/images\/([^\/]+)\.(?:png|jpe?g|gif)/);
					if (!match)
					{
						continue;
					}
					var itemKey = match[1];
					item2Category.set(itemKey, categoryId);
					if (categoryAmbassador2CategoryName[itemKey])
					{
						category2Name.set(categoryId, categoryAmbassador2CategoryName[itemKey]);
					}
					if (!itemListPerCategory.has(categoryId))
					{
						itemListPerCategory.set(categoryId, []);
					}
					itemListPerCategory.get(categoryId).push(itemKey);
				}
			}
		}
	}

	function getItemLimit(itemKey)
	{
		// TODO: combine list of offers with min/max-boundries
		var limit = itemLimits.get(itemKey);
		if (limit && limit.timestamp > now() - MAX_LIMIT_AGE)
		{
			return Promise.resolve([limit.min, limit.max]);
		}
		else if (!win.jsTradalbeItems.hasOwnProperty(itemKey))
		{
			return Promise.resolve([0, 0]);
		}
		return new Promise(function (resolve, reject)
		{
			win.postItemDialogue(
			{
				value: 'sell'
			}, itemKey, null);
			if (!item2Resolver.has(itemKey))
			{
				item2Resolver.set(itemKey, []);
			}
			item2Resolver.get(itemKey).push(resolve);
			setTimeout(function ()
			{
				return reject(new Error('Request timed out'));
			}, 30e3);
		});
	}

	function calcMarketValue(items)
	{
		var itemKeyList = Object.keys(items);
		return Promise.all(itemKeyList.map(function (key)
			{
				return getItemLimit(key);
			}))
			.then(function (limitList)
			{
				var sum = [0, 0];
				for (var i = 0; i < itemKeyList.length; i++)
				{
					var amount = items[itemKeyList[i]];
					var limit = limitList[i];
					sum[0] += amount * limit[0];
					sum[1] += amount * limit[1];
				}
				return sum;
			});
	}
	market.calcMarketValue = calcMarketValue;

	function init()
	{
		showOfferCancelCooldown();
		addExtraBtns();
		improveOfferList();
		var _chosenPostItemDialogue = win.chosenPostItemDialogue;
		win.chosenPostItemDialogue = function (itemName, lowerLimit, upperLimit)
		{
			if (processItemLimits(itemName, Number(lowerLimit), Number(upperLimit)))
			{
				_chosenPostItemDialogue(itemName, lowerLimit, upperLimit);
			}
		};
		var _addToPlayerMarket = win.addToPlayerMarket;
		win.addToPlayerMarket = function (data)
		{
			processMarketData(data);
			_addToPlayerMarket(data);
		};
		loadPriceHistory();
		// delay (debounce) sending the request for 3s
		var startDebouncedRequest = debounce(function ()
		{
			return loadPriceHistory();
		}, 3e3);
		settings.observe(settings.KEY.syncPriceHistory, function ()
		{
			return startDebouncedRequest();
		});
		settings.observeSub(settings.KEY.syncPriceHistory, 'url', function ()
		{
			return startDebouncedRequest();
		});
	}
	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 lootInfoInitialized = false;
	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;
		lootInfoInitialized = true;
	}

	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("\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: 0 10px;\n\twidth: calc(33.3% - 20px);\n}\n#" + COMBAT_LOOT_TABLES_ID + " table.hiscores-table img.image-icon-50\n{\n\twidth: auto;\n}\n\t\t");
		var REFRESH_LOOT_TABLE_ID = 'refresh-loot-table';
		var subTab = document.createElement('span');
		subTab.className = 'large-button';
		subTab.innerHTML = "<img class=\"image-icon-50\" src=\"images/combatDropTable.png\" style=\"filter: grayscale(100%);\">Loot";
		subTab.addEventListener('click', function ()
		{
			var _confirmDialogue = win.confirmDialogue;
			win.confirmDialogue = function () {};
			win.clicksOpenDropTable();
			win.confirmDialogue = _confirmDialogue;
			win.openSubTab('loot');
		});

		function setLootTabVisibility()
		{
			var show = settings.get(settings.KEY.showLootTab);
			subTab.style.display = show ? '' : 'none';
			var dropTableItemBox = document.getElementById('item-box-combatDropTable');
			if (dropTableItemBox)
			{
				dropTableItemBox.style.display = show ? 'none' : '';
			}
			if (show && !lootInfoInitialized)
			{
				updateLootTableInfo();
			}
		}
		setLootTabVisibility();
		settings.observe(settings.KEY.showLootTab, function ()
		{
			return setLootTabVisibility();
		});
		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 + "\">Loading...</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 = win.openSubTab;
		win.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 = win.loadDefaultCombatTab;
		win.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()
	{
		addLootTableTab();
		if (settings.get(settings.KEY.showLootTab))
		{
			updateLootTableInfo();
		}
	}
	combat.init = init;
})(combat || (combat = {}));

/**
 * farming improvements
 */
var farming;
(function (farming)
{
	farming.name = 'farming';
	var SEED_INFO_REGEX = {
		minLevel: />\s*Level:/
		, stopsDyingLevel: />\s*Stops\s+Dying\s+Level:/
		, bonemeal: />\s*Bonemeal:/
		, woodcuttingLevel: />\s*Woodcutting\s+Level:/
	};
	var seedInfoSpans = {};
	var seedInfo = {};
	var checkInfo = {
		bonemeal: function (amount)
		{
			return amount <= win.bonemeal;
		}
		, minLevel: function (level)
		{
			return level <= win.getLevel(win.farmingXp);
		}
		, stopsDyingLevel: function (level)
		{
			return level <= win.getLevel(win.farmingXp);
		}
		, woodcuttingLevel: function (level)
		{
			return level <= win.getLevel(win.woodcuttingXp);
		}
	};
	var RED = 'rgb(204, 0, 0)';

	function addBetterStyle()
	{
		var CLASS_NAME = 'seedHighlight';
		addStyle("\n#dialogue-plant-farming input.input-img-farming-patch-dialogue-seeds\n{\n\tpadding: 2px 4px;\n}\n#dialogue-plant-farming #dialogue-plant-grassSeeds\n{\n\theight: 75px;\n\tpadding: 0;\n\twidth: 75px;\n}\n#dialogue-plant-farming #dialogue-plant-treeSeeds,\n#dialogue-plant-farming #dialogue-plant-oakTreeSeeds,\n#dialogue-plant-farming #dialogue-plant-willowTreeSeeds,\n#dialogue-plant-farming #dialogue-plant-mapleTreeSeeds\n{\n\tpadding: 0;\n}\n\nbody." + CLASS_NAME + " #dialogue-plant-farming input.input-img-farming-patch-dialogue-seeds:hover\n{\n\tbackground-color: transparent;\n\tborder: 1px solid black;\n\tmargin: -1px;\n\ttransform: scale(1.1);\n}\n\t\t");
		// seedHighlight
		function updateHoverStyle()
		{
			document.body.classList[settings.get(settings.KEY.highlightUnplantableSeed) ? 'add' : 'remove'](CLASS_NAME);
		}
		updateHoverStyle();
		settings.observe(settings.KEY.highlightUnplantableSeed, function ()
		{
			return updateHoverStyle();
		});
	}

	function readSeedInfo(seedName, tooltipEl)
	{
		var spans = tooltipEl.querySelectorAll(':scope > span');
		var infoSpans = {
			bonemeal: null
			, minLevel: null
			, stopsDyingLevel: null
			, woodcuttingLevel: null
		};
		var info = {
			bonemeal: 0
			, minLevel: 0
			, stopsDyingLevel: 0
			, woodcuttingLevel: 0
		};
		var i = 2;
		for (var key in SEED_INFO_REGEX)
		{
			if (SEED_INFO_REGEX[key].test(spans[i].innerHTML))
			{
				infoSpans[key] = spans.item(i);
				var textNode = spans.item(i).lastChild;
				info[key] = parseInt(textNode.textContent || '', 10);
				i++;
			}
		}
		seedInfoSpans[seedName] = infoSpans;
		seedInfo[seedName] = info;
	}

	function checkSpan(span, fulfilled)
	{
		span.style.color = fulfilled ? '' : RED;
		span.style.fontWeight = fulfilled ? '' : 'bold';
	}

	function checkSeedInfo(seedName, init)
	{
		if (init === void 0)
		{
			init = false;
		}
		var highlight = settings.get(settings.KEY.highlightUnplantableSeed);
		var info = seedInfo[seedName];
		var spans = seedInfoSpans[seedName];
		var canBePlanted = true;
		for (var key in info)
		{
			var span = spans[key];
			if (span)
			{
				var fulfilled = checkInfo[key](info[key]);
				checkSpan(span, !highlight || fulfilled);
				canBePlanted = !highlight || canBePlanted && (key == 'stopsDyingLevel' || fulfilled);
			}
		}
		var itemBox = document.getElementById('item-box-' + seedName);
		if (itemBox)
		{
			itemBox.style.opacity = (!highlight || canBePlanted) ? '' : '.5';
		}
		var plantInput = document.getElementById('dialogue-plant-' + seedName);
		if (plantInput)
		{
			plantInput.style.backgroundColor = (!highlight || canBePlanted) ? '' : 'hsla(0, 100%, 50%, .5)';
		}
		if (init)
		{
			observer.add('bonemeal', function ()
			{
				return checkSeedInfo(seedName);
			});
			observer.add('farmingXp', function ()
			{
				return checkSeedInfo(seedName);
			});
			observer.add('woodcuttingXp', function ()
			{
				return checkSeedInfo(seedName);
			});
			settings.observe(settings.KEY.highlightUnplantableSeed, function ()
			{
				return checkSeedInfo(seedName);
			});
		}
	}

	function getSeedInfo(seedName)
	{
		return seedInfo[seedName];
	}
	farming.getSeedInfo = getSeedInfo;

	function init()
	{
		addBetterStyle();
		// read all seed information
		var tooltipEls = document.querySelectorAll('div[id^="tooltip-"][id$="Seeds"]');
		for (var i = 0; i < tooltipEls.length; i++)
		{
			var tooltipEl = tooltipEls[i];
			var seedName = tooltipEl.id.replace(/^tooltip-/, '');
			readSeedInfo(seedName, tooltipEl);
			checkSeedInfo(seedName, true);
		}
	}
	farming.init = init;
})(farming || (farming = {}));

/**
 * general features which doesn't really belong anywhere
 */
var general;
(function (general)
{
	general.name = 'general';
	// disable the drink button for 3 seconds
	var DRINK_DELAY = 3;

	function getSentBoat()
	{
		for (var i = 0; i < BOAT_LIST.length; i++)
		{
			if (getGameValue(BOAT_LIST[i] + 'Timer') > 0)
			{
				return BOAT_LIST[i];
			}
		}
		return null;
	}

	function checkBoat(boat)
	{
		var boatDialog = null;
		var sendBtn = null;
		var initiatedDialogs = document.querySelectorAll('div[role="dialog"]');
		for (var i = 0; i < initiatedDialogs.length; i++)
		{
			var dialog = initiatedDialogs[i];
			if (dialog.style.display !== 'none')
			{
				var btn = dialog.querySelector('input[type="button"][value="Send Boat"]');
				if (btn)
				{
					sendBtn = btn;
					boatDialog = dialog;
					break;
				}
			}
		}
		if (!boatDialog || !sendBtn)
		{
			return;
		}
		var smallboxes = boatDialog.querySelectorAll('div.basic-smallbox');
		var baitBox = smallboxes[0];
		var runningBox = smallboxes[1];
		if (smallboxes.length === 1)
		{
			runningBox = document.createElement('div');
			runningBox.className = 'basic-smallbox';
			runningBox.style.display = 'none';
			var parent_1 = baitBox.parentElement;
			var next = baitBox.nextElementSibling;
			if (parent_1)
			{
				if (next)
				{
					parent_1.insertBefore(runningBox, next);
				}
				else
				{
					parent_1.appendChild(runningBox);
				}
			}
		}
		var sentBoat = getSentBoat();
		baitBox.style.display = sentBoat !== null ? 'none' : '';
		runningBox.style.display = sentBoat !== null ? '' : 'none';
		// just in case Smitty changes this game mechanic somehow, don't disable the button:
		// sendBtn.disabled = sentBoat !== null;
		sendBtn.style.color = sentBoat !== null ? 'gray' : '';
		win.$(boatDialog).on('dialogclose', function ()
		{
			if (sendBtn)
			{
				sendBtn.style.color = '';
			}
		});
		if (sentBoat === boat)
		{
			runningBox.innerHTML = "<b>Returning in:</b> <span data-item-display=\"" + boat + "Timer\">" + format.timer(getGameValue(boat + 'Timer')) + "</span>";
		}
		else if (sentBoat !== null)
		{
			runningBox.innerHTML = "Wait for the other boat to return.";
		}
		else
		{
			var enoughBaitAndCoal = win.fishingBait >= win.fishingBaitCost(boat)
				&& (boat !== 'steamBoat' || win.charcoal >= 300);
			baitBox.style.color = enoughBaitAndCoal ? '' : 'red';
		}
	}

	function initBoatDialog()
	{
		var _clicksBoat = win.clicksBoat;
		win.clicksBoat = function (boat)
		{
			_clicksBoat(boat);
			checkBoat(boat);
		};
		var _doCommand = win.doCommand;
		win.doCommand = function (data)
		{
			_doCommand(data);
			if (data.startsWith('RUN_FUNC=SAIL_BOAT_WIND'))
			{
				checkBoat('sailBoat');
			}
		};
	}
	var potionDrinkEnable = null;
	var POTION_ACTIVE_HTML = "<br>It's already active.";

	function updateDialogEls(timerKey, dialog, close)
	{
		if (close === void 0)
		{
			close = false;
		}
		var timer = getGameValue(timerKey);
		var showActive = settings.get(settings.KEY.usePotionWarning) && timer > 0 && !close;
		var confirmText = document.getElementById('dialogue-confirm-text');
		var br = confirmText && confirmText.nextElementSibling;
		if (confirmText && br)
		{
			if (showActive)
			{
				confirmText.innerHTML += POTION_ACTIVE_HTML;
			}
			else
			{
				confirmText.innerHTML = confirmText.innerHTML.replace(POTION_ACTIVE_HTML, '');
			}
			br.style.display = showActive ? 'none' : '';
		}
		var confirmBtn = document.getElementById('dialogue-confirm-yes');
		if (confirmBtn && showActive)
		{
			confirmBtn.disabled = true;
			var i_1 = DRINK_DELAY;
			var updateValue_1 = function ()
			{
				confirmBtn.value = 'Drink' + (i_1 > 0 ? ' (' + i_1 + ')' : '');
				if (i_1 === 0)
				{
					potionDrinkEnable && potionDrinkEnable();
				}
				else
				{
					i_1--;
				}
			};
			var countDownInterval_1;
			var dialogClose_1 = function ()
			{
				return potionDrinkEnable && potionDrinkEnable();
			};
			potionDrinkEnable = function ()
			{
				potionDrinkEnable = null;
				win.$(dialog).off('dialogclose', dialogClose_1);
				countDownInterval_1 && clearInterval(countDownInterval_1);
				confirmBtn.disabled = false;
				confirmBtn.value = 'Drink';
			};
			updateValue_1();
			countDownInterval_1 = setInterval(function ()
			{
				return updateValue_1();
			}, 1e3);
			win.$(dialog).on('dialogclose', dialogClose_1);
		}
		else if (!showActive)
		{
			potionDrinkEnable && potionDrinkEnable();
		}
	}

	function checkPotionActive(potion)
	{
		var dialog = document.getElementById('dialogue-confirm');
		var parent = dialog && dialog.parentElement;
		if (!dialog || !parent || parent.style.display === 'none')
		{
			return;
		}
		var timerKey = potion + 'Timer';
		updateDialogEls(timerKey, dialog);
		var fn = observer.add(timerKey, function (key, oldValue, newValue)
		{
			if (oldValue < newValue && oldValue === 0
				|| oldValue > newValue && newValue === 0)
			{
				updateDialogEls(timerKey, dialog);
			}
		});
		win.$(dialog).on('dialogclose', function ()
		{
			updateDialogEls(timerKey, dialog, true);
			observer.remove(timerKey, fn);
		});
	}

	function initPotionDialog()
	{
		var _confirmDialogue = win.confirmDialogue;
		win.confirmDialogue = function (width, text, btn1Text, btn2Text, cmd)
		{
			potionDrinkEnable && potionDrinkEnable();
			_confirmDialogue(width, text, btn1Text, btn2Text, cmd);
		};
		var _clicksPotion = win.clicksPotion;
		win.clicksPotion = function (potion)
		{
			_clicksPotion(potion);
			checkPotionActive(potion);
		};
	}

	function init()
	{
		initBoatDialog();
		initPotionDialog();
	}
	general.init = init;
})(general || (general = {}));

/**
 * 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
		, farming
		, general
	];
	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 = win.doCommand;
	win.doCommand = function (data)
	{
		if (data.startsWith('REFRESH_ITEMS='))
		{
			oldValues = new Map();
			for (var _i = 0, _a = win.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(win.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 = win.refreshItemValues;
	win.refreshItemValues = function (itemKeyList, firstLoad)
	{
		_refreshItemValues(itemKeyList, firstLoad);
		for (var _i = 0, itemKeyList_2 = itemKeyList; _i < itemKeyList_2.length; _i++)
		{
			var key = itemKeyList_2[_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 (win.webSocket == null)
		{
			console.error('WebSocket instance not initialized!');
			return;
		}
		// cache old event listener
		var _onClose = win.webSocket.onclose;
		var _onError = win.webSocket.onerror;
		var _onMessage = win.webSocket.onmessage;
		var _onOpen = win.webSocket.onopen;
		var commandQueue = [];
		var _cBytes = win.cBytes;
		win.cBytes = function (command)
		{
			if (win.webSocket && win.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)
			{
				win.webSocket = new WebSocket(win.SSL_ENABLED);
				win.ignoreBytesTracker = Date.now();
				initWSListener(win.webSocket);
				reconnectAttempts++;
			}
			if (win.webSocket)
			{
				win.webSocket.close(WS_TIMEOUT_CODE, 'Connection timed out after ' + WS_TIMEOUT_SEC + ' seconds');
			}
		}

		function updateWSTimeout()
		{
			if (wsTimeout)
			{
				win.clearTimeout(wsTimeout);
			}
			wsTimeout = win.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)
			{
				win.clearTimeout(wsOpenTimeout);
				wsOpenTimeout = null;
			}
			// do the handshake first
			_onOpen.call(this, event);
			commandQueue.forEach(function (command)
			{
				return win.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 = win.setTimeout(onOpenTimeout, WS_OPEN_TIMEOUT_SEC * 1e3);
			}
			ws.onclose = onClose;
			ws.onerror = onError;
			ws.onmessage = onMessage;
			ws.onopen = onOpen;
		}
		initWSListener(win.webSocket);
		document.addEventListener('DOMContentLoaded', function ()
		{
			pageLoaded = true;
			messageQueue.forEach(function (event)
			{
				return win.webSocket.onmessage(event);
			});
		});
	}

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

	function isWebSocketScript(script)
	{
		return script.src.includes('socket.js');
	}
	var found = false;
	if (document.head)
	{
		var scripts = 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)
	win.mouseX = win.innerWidth / 2;
	win.mouseY = win.innerHeight / 2;
	var _confirm = win.confirm;
	win.confirm = function (message)
	{
		// don't show the annoying update confirm box (instead of a confirm box, an ingame dialog could be used...)
		if (message && message.indexOf('Ted\'s Market Script') !== -1)
		{
			return false;
		}
		return _confirm(message);
	};
})(main || (main = {}));

})();