DHO Fixed

Improve Diamond Hunt Online and fix some inconsistencies

当前为 2017-01-29 提交的版本,查看 最新版本

// ==UserScript==
// @name         DHO Fixed
// @namespace    FileFace
// @description  Improve Diamond Hunt Online and fix some inconsistencies
// @version      0.54.6
// @author       Zorbing
// @grant        none
// @run-at       document-start
// @include      http://www.diamondhunt.co/game.php
// ==/UserScript==

/**
 * TODO:
 *	- restyle tabs (allow sub-tabs)
 *	- add activity log (intercepting the dialog and log some messages)
 *	- add new item style for mining engineer
 *	- hide browser notifications if the corresponding tab is opened
 */

(function ()
{
'use strict';

const settings = {
	reorderFarming: {
		title: 'Set seed orders coherent'
		, defaultValue: true
	}
	, applyNewItemStyle: {
		title: 'Apply a new item style'
		, defaultValue: true
	}
	, applyNewKeyItemStyle: {
		title: 'Apply a new key item and machinery style'
		, defaultValue: true
	}
	, improveDialogBtns: {
		title: 'Improve button captions in dialogs'
		, defaultValue: true
	}
	, improveMachineryDialog: {
		title: 'Improve the machinery dialog'
		, defaultValue: true
	}
	, hideSomeCraftRecipes: {
		title: 'Hide some crafting recipes'
		, defaultValue: true
	}
	, hideMaxRecipes: {
		title: 'Hide recipes of maxed machines'
		, defaultValue: true
	}
	, expandEquipment: {
		title: 'Expand crafting recipes of equipment'
		, defaultValue: true
	}
	, hideEquipment: {
		title: 'Hide inferiour equipment (only up to gold)'
		, defaultValue: true
	}
	, hideUnnecessaryPrice: {
		title: 'Hide unnecessary prices'
		, defaultValue: true
	}
	, useFastLevelCalculation: {
		title: 'Use fast level calculation'
		, defaultValue: true
	}
	, showNotifications: {
		title: 'Show notifications for events'
		, defaultValue: true
	}
	, useNewChat: {
		title: 'Use the new chat with local history'
		, defaultValue: true
	}
};
let fullyLoaded = false;
let notify = () => Promise.reject('Notifications disabled');



/**
 * global constants
 */

const maxLevel = 100;
const maxLevelVirtual = 1000;
const furnaceLevels = ['', 'stone', 'bronze', 'iron', 'silver', 'gold', 'ancient', 'promethium', 'runite', 'dragon'];
const ovenLevels = ['bronze', 'iron', 'silver', 'gold', 'ancient', 'promethium', 'runite', 'dragon'];
const barTypes = ['bronze', 'iron', 'silver', 'gold', 'promethium', 'runite'];
const oilConsumption = {
	'drill': 1
	, 'crusher': 15
	, 'giantDrill': 30
	, 'roadHeader': 50
	, 'bucketWheelExcavator': 150
	, 'giantBWE': 500
	, 'sandCollector': 5
};
const machineNames = {
	'drill': 'Mining Drill'
	, 'crusher': 'Crusher'
	, 'giantDrill': 'Giant Drill'
	, 'roadHeader': 'Road Header'
	, 'bucketWheelExcavator': 'Excavator'
	, 'giantBWE': 'Mega Excavator'
	, 'sandCollector': 'Sand Collector'
};



/**
 * observer stuff
 */

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

		const oldValue = window[key];
		const ret = oldLoadGlobals(key, newValue);
		if (oldValue !== newValue)
		{
			(observedKeys.get(key) || []).forEach(fn => fn(key, oldValue, newValue));
		}
		return ret;
	};
}



/**
 * settings
 */

function getSetting(key)
{
	if (!settings.hasOwnProperty(key))
	{
		return;
	}
	const name = 'setting.' + key;
	return localStorage.hasOwnProperty(name) ? JSON.parse(localStorage.getItem(name)) : settings[key].defaultValue;
}
function setSetting(key, value)
{
	if (!settings.hasOwnProperty(key))
	{
		return;
	}
	localStorage.setItem('setting.' + key, JSON.stringify(value));
}
function initSettings()
{
	if (!localStorage.hasOwnProperty('setSoundToggleDefault'))
	{
		const defaultSound = 'off';
		localStorage.setItem('soundToggle', defaultSound);
		localStorage.setItem('setSoundToggleDefault', true);
		document.getElementById('sound-toggle').innerHTML = defaultSound;
	}

	const table = document.getElementById('settings-tab').querySelector('table');
	if (!table)
	{
		return;
	}
	const headerRow = table.insertRow(-1);
	headerRow.innerHTML = `<th style="background-color:black;color:orange">
		Userscript "DHO Fixed"<br>
		<span style="font-size: 0.9rem;">(changes require reloading the tab)</span>
	</th>`;

	for (let key in settings)
	{
		let value = getSetting(key);
		const row = table.insertRow(-1);
		row.innerHTML = `<td id="fake-link-top">
			${settings[key].title}: <span style="color:cyan;" id="userscript-${key}">${value ? 'on' : 'off'}</span>
		</td>`;
		const indicator = document.getElementById('userscript-' + key);;
		row.addEventListener('click', () =>
		{
			value = !value;
			setSetting(key, value);
			indicator.textContent = value ? 'on' : 'off';
		});
	}

	const settingLink = document.querySelector('.top-menu td[onclick^="openTab"]');
	settingLink.addEventListener('click', function ()
	{
		const activeTab = document.querySelector('#tab-tr td[style^="background: linear-gradient(rgb"]');
		if (activeTab)
		{
			activeTab.style.background = 'linear-gradient(black, grey)';
		}
	});
}



/**
 * fix key items
 */

function fixKeyItems()
{
	// remove unnecessary br element
	const oilPump = document.getElementById('key-item-handheldOilPump-box');
	let br = oilPump && oilPump.nextElementSibling;
	if (!br)
	{
		br = document.createElement('br');
	}

	// add br element after img in oil pipe element
	const oilPipe = document.getElementById('key-item-bindedOilPipe-box');
	let img = oilPipe && oilPipe.children[0];
	img = img && img.children[0];
	img.parentNode.insertBefore(br, img.nextSibling);
}



/**
 * fix farming
 */

const seedOrder = ['bloodLeafSeeds', 'redMushroomSeeds', 'dottedGreenLeafSeeds', 'potatoSeeds', 'strawberrySeeds', 'greenLeafSeeds', 'redMushroomTreeSeeds', 'wheatSeeds', 'blewitMushroomSeeds', 'limeLeafSeeds', 'blewitMushroomTreeSeeds', 'snapeGrassSeeds', 'starDustSeeds', 'appleTreeSeeds', 'iceBerrySeeds', 'goldLeafSeeds', 'starDustTreeSeeds', 'stripedLeafSeeds', 'essenceSeeds', 'crystalLeafSeeds', 'megaDottedGreenLeafSeeds', 'megaRedMushroomSeeds', 'essenceTreeSeeds', 'megaGreenLeafSeeds', 'stripedCrystalLeafSeeds', 'megaBlewitMushroomSeeds', 'megaLimeLeafSeeds'];
const seeds = {
	bloodLeafSeeds: {
		title: 'Blood Leaf Seed'
		, level: 1
		, diesUntil: 0
		, time: 5
		, xp: 1e6
	}
	, redMushroomSeeds: {
		title: 'Red Mushroom Seed'
		, level: 1
		, diesUntil: 0
		, time: 15
		, xp: 100
	}
	, dottedGreenLeafSeeds: {
		title: 'Green Dotted Leaf Seed'
		, level: 1
		, diesUntil: 15
		, time: 30
		, xp: 250
	}
	, potatoSeeds: {
		title: 'Potato'
		, level: 5
		, diesUntil: 0
		, time: 15
		, xp: 35
	}
	, strawberrySeeds: {
		title: 'Strawberry Seed'
		, level: 10
		, diesUntil: 0
		, time: 30
		, xp: 85
	}
	, greenLeafSeeds: {
		title: 'Green Leaf Seed'
		, level: 10
		, diesUntil: 25
		, time: 60
		, xp: 500
	}
	, redMushroomTreeSeeds: {
		title: 'Red Mushroom Tree Seed'
		, level: 10
		, diesUntil: 30
		, time: 8*60
		, xp: 2e3
	}
	, wheatSeeds: {
		title: 'Wheat Seed'
		, level: 15
		, diesUntil: 0
		, time: 15
		, xp: 95
	}
	, blewitMushroomSeeds: {
		title: 'Blewit Mushroom Seed'
		, level: 15
		, diesUntil: 0
		, time: 20
		, xp: 200
	}
	, limeLeafSeeds: {
		title: 'Lime Leaf Seed'
		, level: 20
		, diesUntil: 40
		, time: 1.5*60
		, xp: 1500
	}
	, blewitMushroomTreeSeeds: {
		title: 'Blewit Mushroom Tree Seed'
		, level: 20
		, diesUntil: 40
		, time: 10*60
		, xp: 4e3
	}
	, snapeGrassSeeds: {
		title: 'Snape Grass Seed'
		, level: 25
		, diesUntil: 0
		, time: 30
		, xp: 300
	}
	, starDustSeeds: {
		title: 'Stardust Seed'
		, level: 30
		, diesUntil: 0
		, time: 30
		, xp: 750
	}
	, appleTreeSeeds: {
		title: 'Apple Tree Seed'
		, level: 30
		, diesUntil: 45
		, time: 8*60
		, xp: 5e3
	}
	, iceBerrySeeds: {
		title: 'Ice Berry Seed'
		, level: 35
		, diesUntil: 0
		, time: 60
		, xp: 450
	}
	, goldLeafSeeds: {
		title: 'Gold Leaf Seed'
		, level: 40
		, diesUntil: 55
		, time: 4*60
		, xp: 10e3
	}
	, starDustTreeSeeds: {
		title: 'Stardust Tree Seed'
		, level: 40
		, diesUntil: 55
		, time: 5*60
		, xp: 15e3
	}
	, stripedLeafSeeds: {
		title: 'Striped Gold Leaf Seed'
		, level: 55
		, diesUntil: 70
		, time: 7*60
		, xp: 25e3
	}
	, essenceSeeds: {
		title: 'Essence Seed'
		, level: 60
		, diesUntil: 0
		, time: 3*60
		, xp: 30e3
	}
	, crystalLeafSeeds: {
		title: 'Crystal Leaf Seed'
		, level: 70
		, diesUntil: 85
		, time: 10*60
		, xp: 40e3
	}
	, megaDottedGreenLeafSeeds: {
		title: 'Mega Dotted Green Leaf Seed'
		, level: 70
		, diesUntil: 0
		, time: 16*60
		, xp: 12500
	}
	, megaRedMushroomSeeds: {
		title: 'Mega Red Mushroom Seed'
		, level: 70
		, diesUntil: 0
		, time: 16*60
		, xp: 20500
	}
	, essenceTreeSeeds: {
		title: 'Essence Tree Seed'
		, level: 80
		, diesUntil: 90
		, time: 12*60
		, xp: 50500
	}
	, megaGreenLeafSeeds: {
		title: 'Mega Green Leaf Seed'
		, level: 80
		, diesUntil: 0
		, time: 20*60
		, xp: 21e3
	}
	, stripedCrystalLeafSeeds: {
		title: 'Striped Crystal Leaf Seed'
		, level: 85
		, diesUntil: 95
		, time: 15*60
		, xp: 90e3
	}
	, megaBlewitMushroomSeeds: {
		title: 'Mega Blewit Mushroom Seed'
		, level: 85
		, diesUntil: 0
		, time: 20*60
		, xp: 21500
	}
	, megaLimeLeafSeeds: {
		title: 'Mega Lime Leaf Seed'
		, level: 85
		, diesUntil: 0
		, time: 23*60
		, xp: 32e3
	}
};
function fixFarming()
{
	const inputs = document.querySelectorAll('#dialog-planter input[type="image"]');
	for (let i = inputs.length-1; i >= 0; i--)
	{
		const input = inputs[i];
		const key = input.id.replace('planter-input-img-', '');
		const seed = seeds[key];
		input.title = seed.title;
	}

	if (!getSetting('reorderFarming'))
	{
		return;
	}

	let planterEl = inputs[0];
	const planterParent = planterEl.parentNode;
	let boxEl = document.querySelector('#farming-tab .inventory-item-box-farming').parentNode;
	const boxParent = boxEl.parentNode;
	const btnParent = document.getElementById('seed-menu-popup');
	let btnEl = btnParent.firstElementChild;
	for (let i = seedOrder.length-1; i >= 0; i--)
	{
		const key = seedOrder[i];
		const input = document.getElementById('planter-input-img-' + key);
		if (input)
		{
			planterParent.insertBefore(input, planterEl);
			planterParent.insertBefore(document.createTextNode(' '), planterEl);
			planterEl = input;
		}
		const box = document.getElementById('item-' + key + '-box');
		if (box)
		{
			boxParent.insertBefore(box.parentNode, boxEl);
			boxParent.insertBefore(document.createTextNode(' '), boxEl);
			boxEl = box.parentNode;
		}
		const btn = document.getElementById('btn-' + key);
		if (btn)
		{
			btn.title = btn.title.replace('dieing', 'dying');
			btnParent.insertBefore(btn, btnEl);
			btnParent.insertBefore(document.createTextNode(' '), btnEl);
			btnEl = btn;
		}
	}
}



/**
 * fix server message
 */

function fixServerMsg()
{
	const serverMsgEl = document.querySelector('#server-inner-msg');
	if (!serverMsgEl)
	{
		return;
	}

	const serverMsg = serverMsgEl.textContent;
	const close = document.querySelector('#server-top-msg > *:last-child');
	if (localStorage.getItem('closedServerMsg') == serverMsg)
	{
		close.click();
		return;
	}

	close.addEventListener('click', function ()
	{
		localStorage.setItem('closedServerMsg', serverMsg);
	});
}



/**
 * highlight requirements
 */

const highlightBgColor = 'hsla(0, 100%, 90%, 1)';
const imgSrc2Key = {
	'bronzebar': 'bronzeBar'
	, 'ironbar': 'ironBar'
	, 'silverbar': 'silverBar'
	, 'goldbar': 'goldBar'
	, 'stonefurnace': 'stoneFurnace'
	, 'bronzefurnace': 'bronzeFurnace'
	, 'ironfurnace': 'ironFurnace'
	, 'silverfurnace': 'silverFurnace'
	, 'goldfurnace': 'goldFurnace'
	, 'pic_coin': 'coins'
	, 'stardust': 'starDust'
	, 'treasureKey': 'treasureChestKey'
	, 'dottedgreenleaf': 'dottedGreenLeaf'
	, 'redmushroom': 'redMushroom'
	, 'greenleaf': 'greenLeaf'
	, 'limeleaf': 'limeLeaf'
	, 'blewitmushroom': 'blewitMushroom'
	, 'goldleaf': 'goldLeaf'
	, 'pureWater': 'pureWaterPotion'
	, 'snapegrass': 'snapeGrass'
	, 'crystalleaf': 'crystalLeaf'
	, 'starDustConverter': 'starGemPotion'
	, 'superStargemPotion': 'superStarGemPotion'
	, 'superoilpotion': 'superOilPotion'
	, 'wooden_slave': 'miners'
	, 'fishingRodFarmer': 'fishingRod'
	, 'goldenStriper': 'goldenStriperPotion'
	, 'orb': 'orbOfTransformation'
	, 'anyorb': 'emptyBlueOrb'
	, 'anyorb2': 'emptyGreenOrb'
	, 'upgradedOrb': 'superOrbOfTransformation'
};
const imgSrc2LevelKey = {
	'watering-can': 'merchanting'
	, 'cookingskill': 'cooking'
	, 'archaeology': 'exploring'
	, 'wizardHatIcon': 'magic'
};
function amount2Int(str)
{
	return parseInt(str.replace(/M/i, '000000').replace(/B/i, '000000000').replace(/\D/g, ''), 10);
}
function checkRequirements(row, xpKey, init = true)
{
	const isRed = row.style.backgroundColor == 'rgb(255, 128, 128)';
	let everythingFulfilled = true;
	let keys2Observe = [];

	const levelEl = row.cells[2];
	const neededLevel = parseInt(levelEl.textContent, 10);
	const levelHighEnough = neededLevel <= window.getLevel(window[xpKey]);
	levelEl.style.color = levelHighEnough ? '' : 'red';
	everythingFulfilled = everythingFulfilled && levelHighEnough;
	keys2Observe.push(xpKey);

	const reqEl = row.cells[3];
	const children = reqEl.children;
	// check for each requirement if it is fulfilled
	for (let i = 0; i < children.length; i++)
	{
		const el = children[i];
		if (el.tagName != 'IMG')
		{
			continue;
		}
		const imgKey = el.src.replace(/^.+images\/.*?([^\/]+)\..+$/, '$1');
		const key = imgSrc2Key[imgKey] || imgKey;
		// wrap the amount with a span element
		let valueSpan = el.nextSibling;
		if (valueSpan.nodeType == Node.TEXT_NODE)
		{
			const valueTextNode = valueSpan;
			valueSpan = document.createElement('span');
			valueTextNode.parentNode.insertBefore(valueSpan, valueTextNode);
			valueSpan.appendChild(valueTextNode);
			const text = valueTextNode.textContent;
			if (/^\s?\d[\d',\.]*$/.test(text))
			{
				valueTextNode.textContent = ' ' + formatNumber(text.replace(/\D/g, ''));
			}
		}

		const amount = amount2Int(valueSpan.textContent);
		const has = parseInt(window[key] || '0', 10);
		const isSkill = imgSrc2LevelKey.hasOwnProperty(key);
		let fulfilled = has >= amount;
		if (isSkill)
		{
			const xpKey = imgSrc2LevelKey[key] + 'Xp';
			fulfilled = window.getLevel(window[xpKey]) >= amount;
			keys2Observe.push(xpKey);
		}
		else if (key == 'gem')
		{
			fulfilled = window.sapphire >= amount || window.emerald >= amount || window.ruby >= amount || window.diamond >= amount;
			keys2Observe.push('sapphire', 'emerald', 'ruby', 'diamond');
		}
		else if (/furnace/i.test(key))
		{
			const furnaceLevel = furnaceLevels.indexOf(key.replace(/furnace/i, ''));
			fulfilled = fulfilled || parseInt(window.bindedFurnaceLevel, 10) >= furnaceLevel;
			keys2Observe.push(key, 'bindedFurnaceLevel');
		}
		else if (key == 'anybar')
		{
			const amountArray = valueSpan.parentNode.getAttribute('tooltip').replace(/\D*$/, '').split('/')
				.map(str => amount2Int(str));
			fulfilled = false;
			for (let i = 0; i < barTypes.length; i++)
			{
				const bar = barTypes[i];
				fulfilled = fulfilled || window[bar + 'Bar'] >= amountArray[i];
				keys2Observe.push(bar);
			}
		}
		else if (/(?:wand|staff)$/i.test(key))
		{
			const bindedKey = 'binded' + key[0].toUpperCase() + key.substr(1);
			fulfilled = fulfilled || window[bindedKey] > 0;
			keys2Observe.push(key, bindedKey);
		}
		else
		{
			if (!window.hasOwnProperty(imgKey) && !imgSrc2Key.hasOwnProperty(imgKey))
			{
				console.debug('missing key handling:', key, el);
			}
			keys2Observe.push(key);
		}
		valueSpan.style.color = fulfilled ? '' : 'red';
		everythingFulfilled = everythingFulfilled && (isSkill || fulfilled);
	}
	levelEl.style.backgroundColor = everythingFulfilled ? '' : highlightBgColor;
	reqEl.style.backgroundColor = everythingFulfilled ? '' : highlightBgColor;
	row.style.backgroundColor = everythingFulfilled ? 'rgb(194, 255, 133)' : 'rgb(255, 128, 128)';

	if (init)
	{
		observe(keys2Observe, () => checkRequirements(row, xpKey, false));
	}
}

function highlightRequirements()
{
	const craftingTables = {
		'crafting': {
			tabId: 'crafting'
			, xp: 'crafting'
		}
		, 'brewing': {
			tabId: 'brewing'
			, xp: 'brewing'
		}
		, 'achCraft': {
			tabId: 'archaeology-crafting'
			, xp: 'crafting'
		}
		, 'cooking': {
			tabId: 'cooking'
			, xp: 'cooking'
		}
		, 'magicCraft': {
			tabId: 'magiccrafting'
			, xp: 'crafting'
		}
		, 'spellbook': {
			tabId: 'spellbook'
			, xp: 'magic'
		}
	};
	for (let key in craftingTables)
	{
		const info = craftingTables[key];
		const xpName = info.xp + 'Xp';
		const table = document.querySelector('#' + info.tabId + '-tab table.table-stats');
		const rows = table.rows;
		for (let i = 0; i < rows.length; i++)
		{
			const row = rows[i];
			if (row.getElementsByTagName('th').length > 0 || row.id == 'craft-ghostKey')
			{
				continue;
			}

			checkRequirements(row, xpName, true);
		}
	}

	// hightlight mining level for mining table
	function highlightMiningLevel()
	{
		const miningLevel = window.getLevel(window.miningXp);
		const table = document.querySelector('#mining-tab table.table-stats');
		const rows = table.rows;
		for (let i = 2; i < rows.length; i++)
		{
			const row = rows[i];
			const level = parseInt(row.cells[2].textContent, 10);
			const tooLow = level > miningLevel;
			row.cells[2].style.color = tooLow ? 'red' : '';
			row.style.backgroundColor = tooLow ? 'hsla(0, 100%, 90%, 1)' : '';
		}
	}
	highlightMiningLevel();
	observe('miningXp', () => highlightMiningLevel());

	const oldLoadGhostPirates = window.loadGhostPirates;
	const ghostKeyRow = document.getElementById('craft-ghostKey');
	window.loadGhostPirates = () =>
	{
		oldLoadGhostPirates();
		if (ghostEssenceTimer > 0)
		{
			// this method is called once per second, so there is no need for observing any values
			checkRequirements(ghostKeyRow, 'craftingXp', false);
		}
	};

	function highlightFarmingLevel()
	{
		const farmingLevel = window.getLevel(window.merchantingXp);
		const seedBtns = document.querySelectorAll('#seed-menu-popup > div.dialogue-seed-btn');
		for (let i = 0; i < seedBtns.length; i++)
		{
			const seedBtn = seedBtns[i];
			const table = seedBtn.firstElementChild;
			const levelCell = table.rows[0].cells[1];
			const level = parseInt(levelCell.textContent.replace(/\D/g, ''), 10);
			const tooLow = level > farmingLevel;
			seedBtn.style.backgroundColor = tooLow ? 'hsla(0, 50%, 75%, 1)' : '';
			levelCell.style.color = tooLow ? 'red' : '';
			levelCell.style.textShadow = tooLow ? '0 0 5px white' : '';
		}
	}
	highlightFarmingLevel();
	observe('merchantingXp', () => highlightFarmingLevel());

	// achievement upgrades
	function highlightAchievementUpgrades()
	{
		const points = parseInt(window.achPoints, 10);
		const spans = document.querySelectorAll('span[id^="cost-ach-"][id$="AchUpgrade"]');
		for (let i = 0; i < spans.length; i++)
		{
			const span = spans[i];
			const notEnough = parseInt(span.textContent, 10) > points;
			span.style.setProperty('color', notEnough ? 'red' : '', 'important');
			span.style.fontWeight  = notEnough ? 'bold' : '';
		}
	}
	highlightAchievementUpgrades();
	observe('achPoints', () => highlightAchievementUpgrades());
}



/**
 * fix market
 */

function fixMarket()
{
	// create an observer instance
	const observer = new MutationObserver(function(mutations)
	{
		mutations.forEach(function(mutation)
		{
			// hide duplicate orb
			var orbs = mutation.target.querySelectorAll('input[alt="upgradeEnchantedRake"]');
			if (orbs.length > 1)
			{
				orbs[1].style.display = 'none';
			}
		});
	});
	// configuration of the observer:
	const config = {
		childList: true
	};

	const table = document.getElementById('selling-tradable-table');
	observer.observe(table, config);

	// fix loading icons
	const loadingImgs = document.querySelectorAll('[src="images/loading_statique.png"]');
	for (var i = 0; i < loadingImgs.length; i++)
	{
		loadingImgs[i].src = 'images/loading.gif';
	}

	const oldFilterBuyables = window.filterBuyables;
	let lastFilterText = null;
	window.filterBuyables = (text) =>
	{
		lastFilterText = text;
		return oldFilterBuyables(text);
	};
	const oldApplyToBuyingTable = window.applyToBuyingTable;
	window.applyToBuyingTable = (...args) =>
	{
		const ret = oldApplyToBuyingTable(...args);
		if (lastFilterText != null)
		{
			window.filterBuyables(lastFilterText);
		}
		return ret;
	};

	// add "clear search"-button
	const searchInput = document.querySelector('input[onkeyup^="filterBuyables"]');
	searchInput.id = 'market-search';
	const tmpWrapper = document.createElement('templateWrapper');
	tmpWrapper.innerHTML = `<input type="button" value="Clear search" style="float: left; margin-left: 10px;" onclick="$('#market-search').val('').keyup()">`;
	const parent = searchInput.parentNode;
	const el = searchInput.nextSibling;
	const childNodes = tmpWrapper.childNodes;
	for (let i = 0; i < childNodes.length; i++)
	{
		parent.insertBefore(childNodes[i], el);
	}

	// fix icon paths
	const oldGetImagePath = window.getImagePath;
	window.getImagePath = (itemVar) =>
	{
		if (itemVar == 'dragonFurnace')
		{
			return 'images/crafting/dragonFurnace.gif';
		}
		return oldGetImagePath(itemVar);
	};

	// auto focus the search input
	const oldSelectItemToTradeDialog = window.selectItemToTradeDialog;
	window.selectItemToTradeDialog = (sellOrBuy, slot) =>
	{
		oldSelectItemToTradeDialog(sellOrBuy, slot);
		window.$('#id_search').focus();
	};
}



/**
 * improve level calculation
 */

let levelXp = new Array(maxLevelVirtual+1);
function calcLevelXp(level)
{
	return level > 0 ? Math.round(Math.pow((level-1), 3 + ((level-1) / 200))) : 0;
}
function getLevelXp(level)
{
	return levelXp[level-1] || calcLevelXp(level);
}
const getDynamicLevel = (function ()
{
	const size = Math.pow(2, Math.ceil(Math.log2(maxLevel)));
	let xpTree = new Array(size);
	let levelTree = new Array(size);
	const sizeVirtual = Math.pow(2, Math.ceil(Math.log2(maxLevelVirtual)));
	let xpTreeVirtual = new Array(sizeVirtual);
	let levelTreeVirtual = new Array(sizeVirtual);
	createNode(xpTree, levelTree, 1, maxLevel, 0);
	createNode(xpTreeVirtual, levelTreeVirtual, 1, maxLevelVirtual, 0);

	function createNode(xpArray, levelArray, start, end, i)
	{
		const current = start + Math.pow(2, Math.floor(Math.log2(end - start + 1))) - 1;
		xpArray[i] = getLevelXp(current);
		levelArray[i] = current;

		if (current - start > 0)
		{
			createNode(xpArray, levelArray, start, current-1, 2*i + 1);
		}
		if (end - current > 0)
		{
			createNode(xpArray, levelArray, current+1, end, 2*i + 2);
		}
	}

	function getDynamicLevel(playerXP, useVirtual = false)
	{
		const isVirtual = window.virtualLevelsOn !== 0 && useVirtual === true;
		const xpArray = isVirtual ? xpTreeVirtual : xpTree;
		const levelArray = isVirtual ? levelTreeVirtual : levelTree;
		let i = 0;
		let level = 0;
		while (xpArray[i] != null)
		{
			if (playerXP == xpArray[i])
			{
				return levelArray[i];
			}
			else if (playerXP < xpArray[i])
			{
				i = 2*i+1;
			}
			else if (playerXP > xpArray[i])
			{
				level = levelArray[i];
				i = 2*i+2;
			}
		}
		return level;
	}

	return getDynamicLevel;
})();
function getLevel(playerXP)
{
	return getDynamicLevel(playerXP, false);
}
function getVirtualLevel(playerXP)
{
	return getDynamicLevel(playerXP, true);
}

function getGlobalLevel()
{
	return getDynamicGlobalLevel(false);
}
function getDynamicGlobalLevel(useVirtual = false)
{
	return Math.floor(getDynamicLevel(parseInt(window.miningXp, 10), useVirtual))
		+ Math.floor(getDynamicLevel(parseInt(window.craftingXp, 10), useVirtual))
		+ Math.floor(getDynamicLevel(parseInt(window.brewingXp, 10), useVirtual))
		+ Math.floor(getDynamicLevel(parseInt(window.merchantingXp, 10), useVirtual))
		+ Math.floor(getDynamicLevel(parseInt(window.exploringXp, 10), useVirtual))
		+ Math.floor(getDynamicLevel(parseInt(window.cookingXp, 10), useVirtual))
		+ Math.floor(getDynamicLevel(parseInt(window.magicXp, 10), useVirtual))
	;
}
function improveLevelCalculation()
{
	if (!getSetting('useFastLevelCalculation'))
	{
		return;
	}

	for (var i = 1; i < maxLevelVirtual; i++)
	{
		levelXp[i-1] = calcLevelXp(i);
	}
	window.getLevel = getLevel;
	window.getVirtualLevel = getVirtualLevel;
	window.getGlobalLevel = getGlobalLevel;
}



/**
 * fix inventory
 */

function fixInventory()
{
	if (!getSetting('hideUnnecessaryPrice'))
	{
		return;
	}

	const tab = document.getElementById('gatherings-tab');
	const coinImgs = tab.querySelectorAll('span[id^="item-"][id$="-box"] img[src="images/pic_coin.png"]');
	for (let i = 0; i < coinImgs.length; i++)
	{
		const coinImg = coinImgs[i];
		const price = coinImg.nextSibling;
		if (price.nodeType == Node.TEXT_NODE && !/\d/.test(price.textContent))
		{
			const parent = coinImg.parentNode;
			parent.removeChild(coinImg);
			parent.removeChild(price);
		}
	}
}



/**
 * fix machinery
 */

function getMachineCount(machine)
{
	return window['binded' + machine[0].toUpperCase() + machine.substr(1)];
}
function getOilValueFromMachine(machine)
{
	return (oilConsumption[machine] || 0) * getMachineCount(machine);
}
function updateRepairCost(machine)
{
	const input = document.getElementById('machineryChosenPopup');
	const repairCost = document.getElementById('repair-price-dialog');
	if (!input || !repairCost)
	{
		return;
	}

	machine = machine || input.value;
	const percent = window[machine + 'Repair'];
	const cost = window.getRepairCost(machine, percent);
	repairCost.textContent = formatNumber(cost);
}
function openOilDialogue(varname)
{
	const gearOnPath = 'images/spinning-gear.gif';
	const gearOffPath = 'images/spinning-gear-off.gif';
	const oilArea = document.getElementById('oilUsage-area');
	const oilValue = document.getElementById('oilUsage-value');
	const repairArea = document.getElementById('machinery-repair-area');

	let machine = varname.replace(/key-item-binded([^-]+)-box/, '$1');
	machine = machine[0].toLowerCase() + machine.substr(1);

	// machine name + count
	const name = machineNames[machine];
	const count = getMachineCount(machine);
	const max = 10; // don't know if there is a machine with a different limit...
	let title = document.getElementById('machinery-name');
	if (!title)
	{
		title = document.createElement('h3');
		title.style.marginTop = 0;
		title.id = 'machinery-name';
		const parent = document.getElementById('machinery-dialog');
		parent.insertBefore(title, parent.firstChild);
	}
	title.innerHTML = `${name} <span style="float: right;font-size: 1.2rem;">${count}<span style="font-weight: normal;">/${max}</span><span></span></span>`;

	// PROGRESS BAR
	var hasRepair = window.bindedPromethiumWrench > 0;
	if (machine == 'sandCollector')
	{
		// hide repair part (ensure, it is hidden)
		repairArea.setAttribute('style', 'padding: 0; width: 0px; height: 0px; overflow: hidden; border: 0;');
	}
	else
	{
		// show repair part if available
		repairArea.setAttribute('style', 'display: ' + (hasRepair ? 'block' : 'none') + ';');

		const progressBar = document.getElementById('progress-bar-repair-opened');
		const percent = window[machine + 'Repair'];
		const bgColor = percent < 20 ? 'yellow' : (percent >= 50 ? 'lime' : 'yellow');
		progressBar.style.backgroundColor = bgColor;
		progressBar.style.width = percent + '%';

		let repairButton = document.getElementById('repair-current-machine');
		if (!repairButton)
		{
			repairButton = document.createElement('button');
			repairButton.id = 'repair-current-machine';
			repairButton.style.lineHeight = '24px';
			repairButton.style.margin = '10px 5% 0';
			repairButton.style.width = '90%';
			repairButton.style.position = 'relative';
			repairButton.innerHTML = `<img id="bindedPromethiumWrenchOrb-img" src="images/crafting/promethiumWrench.png" alt="workers" width="23px" height="23px" style="position: absolute; top: 3px; left: 13px;">Repair for <span id="repair-price-dialog"></span><img src="images/pic_coin.png" width="25px" height="25px" style="vertical-align: middle;">`;
			repairButton.onclick = () =>
			{
				const machine = document.getElementById('machineryChosenPopup').value;
				window.send('REPAIR_MACHINERY=' + machine);
			};
			const parent = document.getElementById('machinery-repair-area');
			parent.appendChild(repairButton);
		}
		updateRepairCost(machine);
	}
	// END PROGRESS BAR

	oilValue.innerHTML = window.getOilValueFromMachine(machine);
	document.getElementById('machineryChosenPopup').value = machine;
	const isOn = window[machine + 'AreOn'] == 1;
	document.getElementById('myonoffswitch').checked = isOn;
	document.getElementById('myonoffswitch-gear').src = isOn ? gearOnPath : gearOffPath;
	oilArea.style.display = isOn ? '' : 'none';

	window.$('#machinery-dialog').dialog(
	{
		width: 400
	});
}
function fixMachinery()
{
	const oldChangeSmeltingValue = window.changeSmeltingValue;
	window.changeSmeltingValue = () =>
	{
		window.setSmeltingBarAgain(
			window.barTypeSelectedToSmeltGlobal
			, document.getElementById('smeltingAmountRequested')
		);
	};
	const oldOpenFurnaceDialogue = window.openFurnaceDialogue;
	window.openFurnaceDialogue = () =>
	{
		const ret = oldOpenFurnaceDialogue();
		if (furnacePerc == 0)
		{
			window.changeSmeltingValue();
		}
		return ret;
	};

	const oldRapairMachinery = window.rapairMachinery;
	window.rapairMachinery = () =>
	{
		oldRapairMachinery();
		document.getElementById('perc-all-cost').innerHTML = window.getRepairCost('all', 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
	};

	const furnaceCapacaties = [0, 10, 30, 75, 150, 300, 500, 750, 1000, 1250];
	function upgradeFurnaceOrb()
	{
		if (window.bindedUpgradeFurnaceOrb != 1)
		{
			return;
		}

		for (let i = 1; i < furnaceLevels.length; i++)
		{
			let furnaceType = furnaceLevels[i];
			furnaceType = furnaceType[0].toUpperCase() + furnaceType.substr(1);
			const capacity = 1.5 * furnaceCapacaties[i];
			const box = document.getElementById('key-item-binded' + furnaceType + 'Furnace-box');
			let textNode = box.lastChild;
			if (textNode.nodeType !== Node.TEXT_NODE)
			{
				textNode = textNode.lastChild;
			}
			textNode.textContent = ' ' + formatNumber(capacity);
		}
	}
	upgradeFurnaceOrb();
	observe('bindedUpgradeFurnaceOrb', () => upgradeFurnaceOrb());

	if (!getSetting('improveMachineryDialog'))
	{
		return;
	}

	window.getOilValueFromMachine = getOilValueFromMachine;
	window.openOilDialogue = openOilDialogue;

	observe(['drillRepair', 'crusherRepair', 'giantDrillRepair', 'roadHeaderRepair', 'bucketWheelExcavatorRepair', 'giantBWERepair'], () => updateRepairCost());
}



/**
 * fix brewing
 */

const potionRequirements = {
	'seedPotion': {
		level: 5
		, dottedGreenLeaf: 5
		, redMushroom: 100
		, greenLeaf: 1
	}
};
let oldCanBrewItem;
function canBrewItem(command)
{
	var requirements = potionRequirements[command];
	if (!requirements)
	{
		return oldCanBrewItem(command);
	}

	for (var key in requirements)
	{
		if (key == 'level')
		{
			if (getLevel(brewingXp) < requirements.level)
			{
				return false;
			}
		}
		else if (window[key] < requirements[key])
		{
			return false;
		}
	}
	return true;
}
function fixBrewing()
{
	oldCanBrewItem = window.canBrewItem;
	window.canBrewItem = canBrewItem;

	const marginFix = '5px 20px';
	const potionItems = document.querySelectorAll('#brewing-tab [id$="Potion-box"] img.item-box-img');
	for (let i = 0; i < potionItems.length; i++)
	{
		potionItems[i].style.margin = marginFix;
	}
	const smallImgItems = ['vial','enchantedVial','compost'];
	for (let item of smallImgItems)
	{
		document.querySelector('#item-' + item + '-box img.item-box-img').style.margin = marginFix;
	}
}



/**
 * fix tabs
 */

const tabs2Fix = {
	repair: {
		name: 'Machinery'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/machinery'
	}
	, store: {
		name: 'Market'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/market'
	}
	, 'npc-store': {
		name: 'Game Shop'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/market/game'
	}
	, 'donor-store': {
		name: 'Donor Shop'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/market/donor'
	}
	, 'player-store': {
		name: 'Player Market'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/market/player'
	}
	, stats: {
		name: 'Leaderboards'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/stats'
	}
	, coop: {
		name: 'Group Tasks'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/coop'
	}
	, collectables: {
		name: 'Collectables'
	}
	, miningEngineer: {
		name: 'Mining Engineer'
	}
	, 'ach-explore': {
		name: 'Exploring — Equipment'
		, url: 'https://www.reddit.com/r/DiamondHunt/wiki/online/tabs/exploration#wiki_equipment'
	}
};
function tabTitleLink2Span(title)
{
	const span = title.parentNode;
	span.appendChild(title.firstChild);
	span.removeChild(title);
	span.setAttribute('tooltip', '');
	span.style.color = 'gold';
	span.style.fontSize = '24pt';
}
function fixTabs()
{
	function removeElement(el)
	{
		el.parentNode.removeChild(el);
	}
	/**
	 * some special treatment
	 */

	const achievementTitle = document.querySelector('#ach-tab a');
	tabTitleLink2Span(achievementTitle);

	const npcH1 = document.querySelector('#npc-store-tab h1');
	removeElement(npcH1);

	const vendorBr = document.querySelector('#vendor-tab > br:first-child');
	removeElement(vendorBr);
	const vendorTitle = document.querySelector('#vendor-tab a');
	tabTitleLink2Span(vendorTitle);

	const wizardBr = document.querySelector('#wizard-tab > br:first-child');
	removeElement(wizardBr);

	const achTitle = document.querySelector('#archaeology-tab a');
	achTitle.title = '';
	const achCraftTitle = document.querySelector('#archaeology-crafting-tab a');
	achCraftTitle.textContent = 'Exploring — Crafting'.toUpperCase();
	tabTitleLink2Span(achCraftTitle);
	const cookingTitle = document.querySelector('#cooking-tab a');
	cookingTitle.textContent = 'Exploring — Cooking'.toUpperCase();
	cookingTitle.title = '';
	cookingTitle.parentNode.setAttribute('tooltip', 'Open Wiki');

	const magicSpellbookTitle = document.querySelector('#spellbook-tab a');
	magicSpellbookTitle.textContent = 'Magic — spellbook'.toUpperCase();
	const magicCraftTitle = document.querySelector('#magiccrafting-tab a');
	magicCraftTitle.textContent = 'Magic — Crafting'.toUpperCase();
	tabTitleLink2Span(magicCraftTitle);

	removeElement(document.querySelector('#repair-tab > br:last-child'));
	removeElement(document.querySelector('#repair-tab > br:last-child'));
	removeElement(document.querySelector('#miningEngineer-tab br'));
	removeElement(document.querySelector('#brewing-tab br'));
	removeElement(document.querySelector('#archaeology-tab br'));
	removeElement(document.querySelector('#ach-explore-tab br'));
	removeElement(document.querySelector('#ach-explore-tab br'));
	const archCraftBr = document.querySelector('#archaeology-crafting-tab br');
	archCraftBr.parentNode.insertBefore(document.createElement('br'), archCraftBr);
	removeElement(document.querySelector('#cooking-tab br'));
	removeElement(document.querySelector('#cooking-tab br'));
	removeElement(document.querySelector('#cooking-tab br'));
	removeElement(document.querySelector('#magic-tab br'));
	removeElement(document.querySelector('#magiccrafting-tab br'));
	removeElement(document.querySelector('#magiccrafting-tab br'));
	for (let i = 0; i < 10; i++)
	{
		removeElement(document.querySelector('#magiccrafting-tab > span + br'));
	}
	removeElement(document.querySelector('#store-tab br'));
	removeElement(document.querySelector('#player-store-tab br'));
	removeElement(document.querySelector('#stats-tab br'));
	removeElement(document.querySelector('#stats-tab br'));
	removeElement(document.querySelector('#grouptasks-createorjoin br'));
	removeElement(document.querySelector('#grouptasks-notstarted br'));
	removeElement(document.querySelector('#grouptasks-notstarted br'));
	removeElement(document.querySelector('#grouptasks-started br'));


	for (let key in tabs2Fix)
	{
		const tab = tabs2Fix[key];
		const tabEl = document.getElementById(key + '-tab');
		const tmpEl = document.createElement('tmpWrapper');
		let html = '<center>';
		if (tab.url)
		{
			html += `<span class="activate-tooltip">
				<a class="title-link" href="${tab.url}" target="_blank" title="Open Wiki">${tab.name.toUpperCase()}</a>
			</span>`;
		}
		else
		{
			html += `<span class="activate-tooltip" style="color: gold; font-size: 24pt;">
				${tab.name.toUpperCase()}
			</span>`;
		}
		html += '</center><br>';
		tmpEl.innerHTML = html;
		let el = tabEl.firstElementChild;
		for (let i = tmpEl.children.length-1; i >= 0; i--)
		{
			const child = tmpEl.children[i];
			tabEl.insertBefore(child, el);
			el = child;
		}
	}
}



/**
 * hide crafting recipes
 */

const recipes = {
	shovel: ['shovel']
	, promethiumWrench: ['promethiumWrench', 'bindedPromethiumWrench']
	, glassBlowingPipe: ['glassBlowingPipe', 'bindedGlassBlowingPipe']
	, oilPipe: ['oilPipe', 'bindedOilPipe']
	, planter: ['planter', 'bindedPlanter']
	, trowel: ['trowel', 'bindedTrowel']
	, shootingStarCrystal: ['shootingStarCrystal', 'bindedShootingStarCrystal']
	, brewingKit: ['brewingKit', 'brewingKitBinded']
	, rocket: ['rocket', 'bindedRocket']
	, redPumpJack: ['redPumpJack', 'bindedRedPumpJack']
	, explorersBrush: ['explorersBrush', 'bindedExplorersBrush']
	, oilFactory: ['oilFactory', 'bindedOilFactory']
	, robot: ['robot', 'bindedRobot']
	, oilRefinery: ['oilRefinery', 'bindedOilRefinery']
	, superRobot: ['superRobot', 'bindedSuperRobot']
	// , fishingRod: ['bronzeRod', 'ironRod', 'goldRod', 'promethiumRod', 'fishingRod', 'dragonFishingRod', 'bindedDragonFishingRod']
	, fishingRod: ['fishingRod', 'dragonFishingRod', 'bindedDragonFishingRod']
	, fishingBoat: ['fishingBoat', 'bindedFishingBoat']
	, largeFishingBoat: ['largeFishingBoat', 'bindedLargeFishingBoat']
};
function hideCraftingRecipes()
{
	if (!getSetting('hideSomeCraftRecipes'))
	{
		return;
	}

	(function hideFurnaceRecipes(init = false)
	{
		let maxFurnaceLevel = parseInt(window.bindedFurnaceLevel, 10);
		let keys2Observe = ['bindedFurnaceLevel'];
		for (let i = furnaceLevels.length-1; i >= 0; i--)
		{
			const varName = furnaceLevels[i] + 'Furnace';
			if (window[varName] > 0)
			{
				maxFurnaceLevel = Math.max(maxFurnaceLevel, i);
			}

			const row = document.getElementById('craft-' + furnaceLevels[i] + 'Furnace');
			if (row)
			{
				const hide = i <= maxFurnaceLevel;
				row.style.display = hide ? 'none' : '';
				keys2Observe.push(varName);
			}
		}

		if (init)
		{
			observe(keys2Observe, () => hideFurnaceRecipes(false));
		}
	})(true);

	function hideRecipe(key, nameList, init = false)
	{
		const hide = nameList.some(name => window[name] != 0);
		document.getElementById('craft-' + key).style.display = hide ? 'none' : '';

		if (init)
		{
			observe(nameList, () => hideRecipe(key, nameList, false));
		}
	}
	for (let key in recipes)
	{
		hideRecipe(key, recipes[key], true);
	}

	// exploring - crafting
	(function hideOvenRecipes(init = false)
	{
		let maxOvenLevel = -1;
		let keys2Observe = [];
		for (let i = ovenLevels.length-1; i >= 0; i--)
		{
			const type = ovenLevels[i];
			const ovenName = type + 'Oven';
			const bindedOvenName = 'binded' + ovenName[0].toUpperCase() + ovenName.substr(1);
			if (window[ovenName] > 0 || window[bindedOvenName] > 0)
			{
				maxOvenLevel = Math.max(maxOvenLevel, i);
			}
			const row = document.getElementById('craft-' + type + 'Oven');
			if (row)
			{
				const hide = maxOvenLevel >= i;
				row.style.display = hide ? 'none' : '';
				keys2Observe.push(ovenName, bindedOvenName);
			}
		}

		if (init)
		{
			observe(keys2Observe, () => hideOvenRecipes(false));
		}
	})(true);

	// exploring - equipment
	function hideEquipmentRecipe(key, type, init = false)
	{
		let highestLevel = parseInt(window[key + 'SlotId'], 10) - 1;
		for (let i = barTypes.length-1; i >= 0; i--)
		{
			const bar = barTypes[i];
			if (window[bar + type] > 0)
			{
				highestLevel = Math.max(highestLevel, i);
			}
			const row = document.getElementById('craft-' + bar + type);
			if (row && highestLevel >= i)
			{
				row.style.display = 'none';
			}
		}

		if (init)
		{
			observe(key + 'SlotId', () => hideEquipmentRecipe(key, type, false));
			for (let i = barTypes.length-1; i >= 0; i--)
			{
				const bar = barTypes[i];
				observe(bar + type, () => hideEquipmentRecipe(key, type, false));
			}
		}
	}
	const equipmentTypes = {
		'weapon': 'Sword'
		, 'helmet': 'Helmet'
		, 'body': 'Body'
		, 'leg': 'Legs'
	};
	for (let key in equipmentTypes)
	{
		hideEquipmentRecipe(key, equipmentTypes[key], true);
	}

	// magic - crafting
	const magicRodTypes = ['gold', 'promethium', 'runite', 'dragon'];
	(function hideWandRecipe(init = false)
	{
		let maxWandLevel = -1;
		let keys2Observe = [];
		for (let i = magicRodTypes.length-1; i >= 0; i--)
		{
			const type = magicRodTypes[i];
			const wandName = type + 'Wand';
			const bindedWandName = 'binded' + wandName[0].toUpperCase() + wandName.substr(1);
			if (window[wandName] > 0 || window[bindedWandName] > 0)
			{
				maxWandLevel = Math.max(maxWandLevel, i);
			}
			const wandRow = document.getElementById('craft-' + type + 'Wand');
			if (wandRow)
			{
				const hide = maxWandLevel >= i;
				wandRow.style.display = hide ? 'none' : '';
				keys2Observe.push(wandName, bindedWandName);
			}
		}

		if (init)
		{
			observe(keys2Observe, () => hideWandRecipe(false));
		}
	})(true);
	(function hideStaffRecipe(init = false)
	{
		let maxStaffLevel = -1;
		let keys2Observe = [];
		for (let i = magicRodTypes.length-1; i >= 0; i--)
		{
			const type = magicRodTypes[i];
			const staffName = type + 'Staff';
			const bindedStaffName = 'binded' + staffName[0].toUpperCase() + staffName.substr(1);
			if (window[staffName] > 0 || window[bindedStaffName] > 0)
			{
				maxStaffLevel = Math.max(maxStaffLevel, i);
			}
			const staffRow = document.getElementById('craft-' + type + 'Staff');
			if (staffRow)
			{
				const hide = maxStaffLevel >= i;
				staffRow.style.display = hide ? 'none' : '';
				keys2Observe.push(staffName, bindedStaffName);
			}
		}

		if (init)
		{
			observe(keys2Observe, () => hideWandRecipe(false));
		}
	})(true);
}



/**
 * hide equipment
 */

const equipmentId2Type = {
	general: ['', 'Bronze', 'Iron', 'Silver', 'Gold', 'Promethium', 'Runite', 'Dragon']
	, amulet: ['', 'Amulet of the Sea', 'Moonstone Amulet'/*??? TBD*/, 'Enchanted Amulet of the Sea', 'Dragon Amulet']
	, shield: ['', 'Ancient Shield']
	, ring: ['', 'Coin Ring', 'Pure Water Ring', 'Lava Ring']
	, secondRing: ['', 'Looting Gloves']
};
const equipmentTypes = {
	'weapon': 'Sword'
	, 'helmet': 'Helmet'
	, 'body': 'Body'
	, 'leg': 'Legs'
};
const equipmentTypeList = ['helmet', 'amulet', 'weapon', 'body', 'shield', 'ring', 'leg', 'secondRing'];
const equipmentLevels = ['bronze', 'iron', 'silver', 'gold', 'promethium', 'runite', 'dragon'];
const equipmentType2Name = {
	'weapon': 'sword'
	, 'leg': 'legs'
};
function setEquippedList(listCell, init)
{
	let keys2Observe = [];
	let list = [];
	for (let type of equipmentTypeList)
	{
		const id = parseInt(window[type + 'SlotId'], 10);
		keys2Observe.push(type + 'SlotId');
		type = equipmentType2Name[type] || type;
		if (!equipmentId2Type.hasOwnProperty(type))
		{
			list.push(equipmentId2Type.general[id] + ' ' + type[0].toUpperCase() + type.substr(1));
		}
		else
		{
			list.push(equipmentId2Type[type][id]);
		}
	}
	listCell.textContent = list.filter(str => str != '').join(', ');

	if (init)
	{
		for (let key of keys2Observe)
		{
			observe(key, () => setEquippedList(listCell, false));
		}
	}
}
function examineEquipmentRecipes(key, type, init = false)
{
	const currentLevel = parseInt(window[key + 'SlotId'], 10);
	// hide not more than gold equipment
	for (let i = 0; i < equipmentLevels.length; i++)
	{
		const el = document.getElementById('item-' + equipmentLevels[i] + type + '-box');
		const hide = i < 4 && i < currentLevel;
		if (el)
		{
			el.parentNode.style.display = hide ? 'none' : '';
		}
	}

	if (init)
	{
		observe(key + 'SlotId', () => examineEquipmentRecipes(key, type, false));
	}
}
function hideEquipment()
{
	const table = document.querySelector('#ach-explore-tab table.equipement-area-table');
	const row = table.insertRow(-1);
	row.style.borderTop = '1px dashed';
	const nameCell = row.insertCell(-1);
	nameCell.style.verticalAlign = 'top';
	nameCell.textContent = 'Equipped:';
	const listCell = row.insertCell(-1);
	listCell.colSpan = 2;
	listCell.textContent = '';
	setEquippedList(listCell, true);

	if (!getSetting('hideEquipment'))
	{
		return;
	}

	for (let key in equipmentTypes)
	{
		examineEquipmentRecipes(key, equipmentTypes[key], true);
	}
}



/**
 * improve dialog buttons
 */

function improveDialogBtns()
{
	if (!getSetting('improveDialogBtns'))
	{
		return;
	}

	const oldOpenDialogue = window.openDialogue;
	window.openDialogue = (title, message, yesButtonVal) =>
	{
		const [okBtn, cancelBtn] = document.querySelectorAll('#dialog #buttonCommandYes ~ input[type="button"]');
		// restore default state
		const empty = yesButtonVal == null || yesButtonVal == '';
		okBtn.style.display = empty ? 'none' : '';
		okBtn.value = 'OK';
		cancelBtn.value = empty ? 'Close' : 'Cancel';
		if (/stardust/i.test(title))
		{
			okBtn.value = 'Smash it';
		}
		else if (/bind/i.test(title))
		{
			okBtn.value = 'Bind';
		}
		else if (/drink/i.test(title))
		{
			okBtn.value = 'Drink';
		}
		else if (/^EXPLORE/.test(yesButtonVal))
		{
			okBtn.value = 'Start expedition';
		}

		return oldOpenDialogue(title, message, yesButtonVal);
	};

	const oldOpenDialogueWidth = window.openDialogueWidth;
	window.openDialogueWidth = (title, message, yesButtonVal, widthWanted) =>
	{
		const [okBtn, cancelBtn] = document.querySelectorAll('#dialog #buttonCommandYes ~ input[type="button"]');
		// restore default state
		const empty = yesButtonVal == null || yesButtonVal == '';
		okBtn.style.display = empty ? 'none' : '';
		okBtn.value = 'OK';
		cancelBtn.value = empty ? 'Close' : 'Cancel';
		return oldOpenDialogueWidth(title, message, yesButtonVal, widthWanted);
	};

	const oldClicksKeyItem = window.clicksKeyItem;
	window.clicksKeyItem = (varname) =>
 	{
		oldClicksKeyItem(varname);
		if (varname == 'key-item-bindedRocket-box' && rocketTimer == 0)
		{
			const [okBtn, cancelBtn] = document.querySelectorAll('#dialog #buttonCommandYes ~ input[type="button"]');
			okBtn.value = 'Start rocket';
			const textEl = document.querySelector('#dialog #dialog-text');
			textEl.removeChild(textEl.lastChild);
			textEl.removeChild(textEl.lastChild);
			textEl.removeChild(textEl.lastChild);
		}
	};

	const oldOpenFurnaceDialogue = window.openFurnaceDialogue;
	window.openFurnaceDialogue = () =>
	{
		oldOpenFurnaceDialogue();
		if (furnacePerc > 0)
		{
			const [okBtn, cancelBtn] = document.querySelectorAll('#dialog #buttonCommandYes ~ input[type="button"]');
			okBtn.value = 'Cancel smelting';
			cancelBtn.value = 'Close';
		}
	};

	const oldOpenAreaDialogue = window.openAreaDialogue;
	window.openAreaDialogue = () =>
	{
		oldOpenAreaDialogue();
		if (parseInt(exploringTimer) > 0)
		{
			const [okBtn, cancelBtn] = document.querySelectorAll('#dialog #buttonCommandYes ~ input[type="button"]');
			okBtn.value = 'Cancel trip';
			cancelBtn.value = 'Close';
		}
	};
}



/**
 * expand equipment
 */

function expandEquipment()
{
	if (!getSetting('expandEquipment'))
	{
		return;
	}

	const equipmentRows = document.querySelectorAll('tr[onclick^="openCraftSwordDialogue"]');
	const rowParent = equipmentRows[0].parentNode;
	let newRows = [];
	for (let i = 0; i < equipmentRows.length; i++)
	{
		const row = equipmentRows[i];
		const type = row.getAttribute('onclick').replace(/openCraftSwordDialogue\('([^']+)'\);/, '$1');
		const levels = row.cells[2].textContent.split('/');
		const barCosts = row.cells[3].getAttribute('tooltip').replace(/\D*$/, '').split('/');
		for (let i = 0; i < barTypes.length; i++)
		{
			const bar = barTypes[i];
			const newRow = row.cloneNode(true);
			newRow.id = 'craft-' + bar + type;
			newRow.setAttribute('onclick', '');
			newRow.cells[0].textContent = bar[0].toUpperCase() + bar.substr(1) + ' ' + type;
			newRow.cells[1].firstElementChild.src = 'images/exploring/equipement/' + bar + type + '.png';
			newRow.cells[2].textContent = levels[i];
			newRow.cells[3].firstElementChild.src = 'images/minerals/' + bar + 'Bar.png';
			newRow.cells[3].lastChild.textContent = ' ' + barCosts[i];
			((item) =>
			{
				newRow.addEventListener('click', () => window.craftItem(item));
			})(bar + type);
			newRows.push({
				level: parseInt(levels[i], 10)
				, row: newRow
			});
		}
		rowParent.removeChild(row);
	}
	newRows = newRows.sort((a, b) => a.level - b.level);

	// insert new rows into table
	const rows = rowParent.rows;
	let idx = 0;
	for (let i = 0; i < rows.length && idx < newRows.length; i++)
	{
		const row = rows[i];
		if (row.getElementsByTagName('th').length > 0)
		{
			continue;
		}

		const thisLevel = parseInt(row.cells[2].textContent, 10);
		while (newRows[idx] && newRows[idx].level < thisLevel)
		{
			rowParent.insertBefore(newRows[idx].row, row);
			idx++;
		}
	}
	for (; idx < newRows.length; idx++)
	{
		rowParent.appendChild(newRows[idx].row);
	}
}



/**
 * apply new item style
 */

function applyNewItemStyle()
{
	if (!getSetting('applyNewItemStyle'))
	{
		return;
	}

	// change how the items are styled
	const style = document.createElement('style');
	style.innerHTML = `
span[class^="inventory-item-box"],
#vendor-tab span.shop-box,
#ach-tab span.shop-box-ach,
div[id$="-store-tab"] span.shop-box,
span.shop-box-ach
{
	position: relative;
}
span[class^="inventory-item-box"] img.item-box-img,
#vendor-tab img[id^="vendor-item-img"],
#ach-tab span.shop-box-ach img:first-of-type,
div[id$="-store-tab"] span.shop-box img:first-of-type,
#grp-shop-tab span.shop-box-ach img:first-of-type
{
	position: absolute;
	margin: 0 !important;
}
img.item-box-img[height="30px"]  { top: 52.5px; }
img.item-box-img[height="55px"]  { top: 40px; }
img.item-box-img[height="60px"]  { top: 37.5px; }
img.item-box-img[height="70px"]  { top: 32.5px; }
img.item-box-img[height="75px"]  { top: 30px; }
img.item-box-img[height="80px"]  { top: 27.5px; }
img.item-box-img[height="85px"]  { top: 25px; }
img.item-box-img[height="90px"]  { top: 20px; }
img.item-box-img[height="100px"] { top: 9px; }
span[id^="item-binded"] > img.item-box-img[height="100px"]  { top: 17.5px; }
img.item-box-img[height="110px"] { top: 12.5px; }
img.item-box-img[width="55px"]  { left: 42.5px; }
img.item-box-img[width="60px"]  { left: 40px; }
img.item-box-img[width="70px"]  { left: 35px; }
img.item-box-img[width="75px"]  { left: 32.5px; }
img.item-box-img[width="80px"]  { left: 30px; }
img.item-box-img[width="90px"]  { left: 25px; }
img.item-box-img[width="100px"] { left: 20px; }
img.item-box-img[width="110px"] { left: 15px; }
img.item-box-img[width="120px"] { left: 10px; }
span[class^="inventory-item-box"] img.item-box-img[height="60px"][width="60px"] { transform: scale(1.2); }
span[class^="inventory-item-box"] img.item-box-img[height="70px"][width="80px"] { transform: scale(1.2); }
/* this is a special case (converting items into stardust) */
#wizard-tab img.item-box-img[height="50px"] { top: 22px; }
#wizard-tab img.item-box-img[width="50px"] { left: 45px; }

#vendor-tab img[id^="vendor-item-img"]
{
	transform: scale(1.1);
}
/* height: 155px */
img[id^="vendor-item-img"][height="85x"]  { top: 35px; }
/* width: 150px (110px + 40px) */
img[id^="vendor-item-img"][width="80px"]  { left: 35px; }

span[class^="inventory-item-box"] span[id$="mount"],
#ancientCrystalChargesSpan,
#vendor-tab span.box-title,
#ach-tab span[id^="cost-ach-"][id$="AchUpgrade"],
div[id$="-store-tab"] span[id$="-cost"],
#grp-shop-tab span[id$="-cost"]
{
	background-color: black;
	border-top: 1px solid rgba(255, 255, 255, 0.5);
	color: white !important;
	font-weight: normal;
	margin: 0 !important;
	padding: 3px;
	text-align: center;
	position: absolute;
	bottom: 0;
	left: 0;
	right: 0;
}
span[class^="inventory-item-box"] span[id$="mount"]:not(#energy-amount),
#vendor-tab span.box-title
{
	padding-right: 9px;
}
span[class^="inventory-item-box"] span[id$="mount"]:not(#energy-amount)::before,
#vendor-tab span.box-title::before
{
	content: '${String.fromCharCode(215)}';
	margin-right: 3px;
}
#fishfarmer-img
{
	top: 18px;
}
#hasMapOfTheSea-fishermen
{
	position: absolute;
	bottom: 3px;
	left: 3px;
}
span[class^="inventory-item-box"] span[id$="-price"],
#vendor-tab span[id^="vendor-item-cost"]
{
	font-weight: normal;
	padding-left: 20px;
	position: relative;
	top: 1px;
}
#vendor-tab span[id^="vendor-item-cost"]
{
	font-size: inherit !important;
}
span[class^="inventory-item-box"] span[id$="-price"]:empty,
#sandstone-price,
#moonStone-price,
#glass-price,
#brewingKitBinded-price,
#stripedLeaf-price,
#greenMushroom-price,
#whaleTooth-price,
#snapeGrass-price,
#strangeLeaf-price,
#pureWaterPotion-price,
#cactusWater-price,
#ghostRemains-price,
span[id$="Potion-price"]
{
	visibility: hidden;
}
span[class^="inventory-item-box"] span[id$="-price"]::before,
#vendor-tab span[id^="vendor-item-cost"]::before,
div[id$="-store-tab"] span[id$="-cost"]::before,
#grp-shop-tab span[id$="-cost"]::before
{
	content: '';
	display: inline-block;
	width: 20px;
	height: 20px;
	position: absolute;
	left: 0;
	background-image: url('images/pic_coin.png');
	background-size: 20px 20px;
}
#shop-ghostPirates-cost::before
{
	background-image: url('images/pic_coin2.png');
}
#grp-shop-tab span[id$="-cost"]::before
{
	background-image: url('images/icons/groupTaskTokens.png');
}
#grp-shop-tab #grp-chests-badge-cost::before
{
	background-image: url('images/icons/groupTaskBadge4.png');
}
span[class^="inventory-item-box"] img[src="images/pic_coin.png"],
#vendor-tab img[src="images/pic_coin.png"],
#npc-store-tab img[src^="images/pic_coin"],
#npc-store-tab img[src="images/icons/stats.png"],
#npc-store-tab img[src="images/crafting/anyOrb.png"],
#npc-store-tab img[src="images/spinning-gear-off.gif"],
#donor-store-tab img ~ img[src="images/donor_coin.png"],
#donor-store-tab span[id$="-cost"] img[src="images/donor_coin.png"],
#grp-shop-tab img[src^="images/icons/groupTask"][id^="group-"],
#grp-shop-tab #grp-shop-badge-price-img
{
	display: none;
}
#ach-tab span[id^="cost-ach-"][id$="AchUpgrade"]::before
{
	content: '';
	display: inline-block;
	width: 20px;
	height: 20px;
	position: absolute;
	top: 2px;
	left: 4px;
	background-image: url('images/shop/ach.png');
	background-size: 20px 20px;
}
#ach-tab span.box-title
{
	font-size: 14pt;
	font-weight: normal;
	position: relative;
	top: 4px;
}
#ach-tab span.shop-box-ach img:first-of-type[height="60px"] { top: 47.5px; }
#ach-tab span.shop-box-ach img:first-of-type[height="80px"] { top: 37.5px; }
#ach-tab span.shop-box-ach img:first-of-type[width="55px"] { left: 47.5px; }
#ach-tab span.shop-box-ach img:first-of-type[width="80px"] { left: 35px; }
#ach-tab span.shop-box-ach img[src="images/shop/ach.png"]
{
	display: none;
}
#ach-tab span.shop-box-ach img[src="images/division/check.png"],
#grp-shop-tab img[src="images/division/check.png"]
{
	position: absolute;
	bottom: 5px;
	left: calc(50% - 10px);
}

#npc-store-tab span.box-title,
#donor-store-tab span.box-title,
#grp-shop-tab span.box-title
{
	font-size: 1.1rem;
	font-weight: bold;
	margin: 0;
	padding: 4px;
	position: absolute;
	left: 0;
	right: 0;
}
#donor-store-tab span.box-title
{
	font-size: 1.02rem;
}
#shop-coop-level-cost,
#shop-miningEngineer-machines-cost,
#grp-shop-tab #grp-chests-badge-cost
{
	bottom: 26px;
}
#shop-coop-level-cost::before
{
	background-image: url('images/icons/stats.png');
	left: 1px;
}
#shop-wizard-cost::before
{
	background-image: url('images/crafting/anyOrb.png');
}
#shop-miningEngineer-machines-cost::before
{
	background-color: white;
	background-image: url('images/spinning-gear-off.gif');
}
#npc-store-tab img:first-of-type[height="60px"] { top: 47.5px; }
#npc-store-tab img:first-of-type[height="65px"] { top: 45px; }
#npc-store-tab img:first-of-type[height="70px"] { top: 42.5px; }
#npc-store-tab img:first-of-type[height="80px"] { top: 37.5px; }
#npc-store-tab img:first-of-type[height="85x"] { top: 35px; }
#npc-store-tab img:first-of-type[height="100x"] { top: 27.5px; }
#npc-store-tab img:first-of-type[width="60px"] { left: 45px; }
#npc-store-tab img:first-of-type[width="65px"] { left: 42.5px; }
#npc-store-tab img:first-of-type[width="80px"] { left: 35px; }
#npc-store-tab img:first-of-type[width="100px"] { left: 25px; }

#donor-store-tab img:first-of-type[height="80px"] { top: 37.5px; }
#donor-store-tab img:first-of-type[width="80px"] { left: 35px; }
#donor-store-tab span[id$="-cost"]::before
{
	background-image: url('images/donor_coin.png');
}

#grp-shop-tab img[id^="grp-shop-"][height="80px"] { top: 37.5px; }
#grp-shop-tab img[id^="grp-shop-"][width="80px"] { left: 35px; }
#grp-shop-tab img[id^="grp-shop-"][width="90px"] { left: 30px; }
#grp-shop-tab img[src="images/division/check.png"] + span
{
	display: none;
}
	`;
	document.head.appendChild(style);
	// remove line breaks
	const brs = document.querySelectorAll(
		'[class^="inventory-item-box"] br'
		+ ', span.shop-box br'
		+ ', #ach-tab span.shop-box-ach br'
		+ ', #grp-shop-tab span.shop-box-ach br'
	);
	let i = 0;
	while (brs[i] != null)
	{
		brs[i++].parentNode && brs[--i].parentNode.removeChild(brs[i]);
	}
	// give the emerald image the correct class name
	const emeraldAmount = document.getElementById('emeraldAmount');
	const previous = emeraldAmount && emeraldAmount.previousElementSibling;
	previous && previous.classList.add('item-box-img');

	// wrap some requirements in npc-shop
	const shopBoxes = document.querySelectorAll('div[id$="-store-tab"] span.shop-box');
	for (let i = 0; i < shopBoxes.length; i++)
	{
		const box = shopBoxes[i];
		const children = box.childNodes;
		let foundImg = false;
		let wrapper;
		const idList = {
			'shop-coopUnlocked-box': ['shop-coop-cost', 'shop-coop-level-cost']
			, 'shop-hasVendor-box': ['shop-vendor-cost']
			, 'shop-wizard-box': ['shop-wizard-cost']
			, 'shop-achShop-box': ['shop-achShop-cost']
			, 'shop-miningEngineer-box': ['shop-miningEngineer-cost', 'shop-miningEngineer-machines-cost']
			, 'donor-shop-hasExtraOfflineTimer-box': ['shop-extraOfflineTimer-cost']
			, '': ['shop-offlineTimer-cost']
		}[box.id] || [];
		for (let j = 0; j < children.length; j++)
		{
			const child = children[j];
			if (!foundImg && child.tagName == 'IMG')
			{
				foundImg = true;
			}
			else if (foundImg && child.nodeType == Node.TEXT_NODE)
			{
				wrapper = document.createElement('span');
				wrapper.id = idList.shift() || '';
				box.insertBefore(wrapper, child);
				wrapper.appendChild(child);
			}
			else if (foundImg && wrapper != null)
			{
				wrapper.appendChild(child);
				j--;
			}
		}
	}

	// wrap some requirements in group shop
	const grpBoxes = document.querySelectorAll('#grp-shop-tab span.shop-box-ach');
	const idList = ['grp-badge-cost', 'grp-more-points-cost', 'grp-eels-cost', 'grp-promethium-cost', 'grp-chests-cost', 'grp-chests-badge-cost', 'grp-gloves-cost'];
	for (let i = 0; i < grpBoxes.length; i++)
	{
		const box = grpBoxes[i];
		const children = box.childNodes;
		let foundImg = false;
		let wrapper;
		for (let j = 0; j < children.length; j++)
		{
			const child = children[j];
			if (!foundImg && child.tagName == 'IMG')
			{
				foundImg = true;
			}
			else if (foundImg && wrapper == null)
			{
				wrapper = document.createElement('span');
				wrapper.id = idList.shift();
				box.insertBefore(wrapper, child);
				wrapper.appendChild(child);
			}
			else if (foundImg && wrapper != null)
			{
				if (child.nodeName == 'IMG')
				{
					box.insertBefore(child, wrapper);
					wrapper = null;
				}
				else
				{
					wrapper.appendChild(child);
					j--;
				}
			}
		}
	}
}



/**
 * apply new key item style
 */

function applyNewKeyItemStyle()
{
	if (!getSetting('applyNewKeyItemStyle'))
	{
		return;
	}

	// change how key items and machinery is styled
	const style = document.createElement('style');
	style.innerHTML = `
span[class$="-inventory-item-box"]
{
	position: relative;
}
span.item-box-title
{
	color: blue;
	font-weight: bold;
	padding: 4px 8px;
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	z-index: 10;
}
span.item-box-title > img[src="images/oil.png"]
{
	display: block;
	margin: 0 auto;
}
span.item-box-title > img[src^="images/spinning-gear"]
{
	margin-right: 3px;
	margin-left: -10px;
}
span[class$="-inventory-item-box"] > img
{
	position: absolute;
}
/* heights */
span[class$="-inventory-item-box"] > img[height="80px"]  { top: 60px; }
span[class$="-inventory-item-box"] > img[height="90px"]  { top: 55px; }
span[class$="-inventory-item-box"] > img[height="100px"]  { top: 50px; }
span[class$="-inventory-item-box"] > img[height="110px"]  { top: 45px; }
span[class$="-inventory-item-box"] > img[height="120px"]  { top: 40px; }
span[class$="-inventory-item-box"] > img[height="130px"]  { top: 35px; }
span[class$="-inventory-item-box"] > img[height="140px"]  { top: 30px; }
/* widths */
span[class$="-inventory-item-box"] > img[width="70px"]  { left: 35px; }
span[class$="-inventory-item-box"] > img[width="80px"]  { left: 30px; }
span[class$="-inventory-item-box"] > img[width="90px"]  { left: 25px; }
span[class$="-inventory-item-box"] > img[width="100px"]  { left: 20px; }
span[class$="-inventory-item-box"] > img[width="110px"]  { left: 15px; }
span[class$="-inventory-item-box"] > img[width="120px"]  { left: 10px; }
#key-item-bindedGlassBlowingPipe-box img
{
	top: 75px;
}
span.ghostPipe-wrapper
{
	display: flex;
	flex-wrap: wrap;
	position: absolute;
	top: 66px;
	left: 19px;
	width: 102px;
}
span.ghostPipe-wrapper > img
{
	box-shadow: 0 0 2px black;
	margin: 2px;
}
span.text-wrapper
{
	background-color: black;
	border-top: 1px solid rgba(255, 255, 255, 0.5);
	color: white;
	font-weight: normal;
	line-height: 22px;
	position: absolute;
	bottom: 0;
	left: 0;
	right: 0;
}
span.text-wrapper span[id$="Amount"],
#level-global-4
{
	font-weight: bold;
}
#key-item-handheldOilPump-box span.text-wrapper
{
	display: none;
}
span.text-wrapper img
{
	margin-right: 3px;
}
span.text-wrapper .small-perc-bar
{
	background-color: black;
	border: 0;
	margin: 0;
	padding: 0;
	position: absolute;
	top: -11px;
	left: 0;
	right: 0;
	width: auto;
}
span.text-wrapper .small-perc-bar-inner
{
	background-color: rgba(0, 210, 0, 1) !important;
}
	`;
	document.head.appendChild(style);

	const brs = document.querySelectorAll('span[class$="-inventory-item-box"] br');
	let i = 0;
	while (brs[i] != null)
	{
		const br = brs[i];
		const parent = br.parentNode;
		if (!parent)
		{
			i++;
			continue;
		}
		if (parent.classList.contains('item-box-title'))
		{
			parent.insertBefore(document.createTextNode(' '), br);
		}
		parent.removeChild(br);
	}

	const spans = document.querySelectorAll('span[class$="-inventory-item-box"]');
	let ghostWrapper;
	for (let i = 0; i < spans.length; i++)
	{
		const span = spans[i];
		const childs = span.childNodes;
		let wrapper;
		let foundImg = false;
		for (let j = 0; j < childs.length; j++)
		{
			const child = childs[j];
			if (!foundImg && child.tagName == 'IMG')
			{
				if (/ghostPipeHolder/.test(child.id))
				{
					if (!ghostWrapper)
					{
						ghostWrapper = document.createElement('span');
						ghostWrapper.className = 'ghostPipe-wrapper';
						child.parentNode.insertBefore(ghostWrapper, child);
						j++;
					}
					ghostWrapper.appendChild(child);
					j--;

					if (child.id == 'ghostPipeHolder6')
					{
						foundImg = true;
					}
				}
				else
				{
					foundImg = true;
				}
			}
			else if (foundImg)
			{
				if (!wrapper)
				{
					wrapper = document.createElement('span');
					wrapper.className = 'text-wrapper';
					span.insertBefore(wrapper, child);
					j++;
				}
				wrapper.appendChild(child);
				j--;
			}
		}
		if (wrapper && wrapper.textContent == '')
		{
			wrapper.parentNode.removeChild(wrapper);
		}
	}
}



/**
 * hide recipes of maxed machinery
 */

function hideMaxRecipes()
{
	if (!getSetting('hideMaxRecipes'))
	{
		return;
	}

	function hideMachineRecipe(key, max, init)
	{
		const bindedKey = 'binded' + key[0].toUpperCase() + key.substr(1);
		const row = document.getElementById('craft-' + key);
		if (row)
		{
			const amount = parseInt(window[key], 10) + parseInt(window[bindedKey], 10);
			const hide = amount >= max();
			row.style.display = hide ? 'none' : '';
			if (init)
			{
				observe(key, () => hideMachineRecipe(key, max, false));
				observe(bindedKey, () => hideMachineRecipe(key, max, false));
			}
		}
	}
	const machinery = ['drill', 'crusher', 'giantDrill', 'sandCollector', 'roadHeader', 'bucketWheelExcavator', 'giantBWE'];
	for (let key of machinery)
	{
		hideMachineRecipe(key, () => 10, true);
	}

	// handle pump jacks with its upgrades
	function calcPumpjackMax()
	{
		let maxPumpjacks = 10;
		if (window.bindedUpgradePumpJackOrb == 1)
		{
			maxPumpjacks += 5;
		}
		if (window.bindedGreenPumpjackOrb == 1)
		{
			maxPumpjacks += 10;
		}
		return maxPumpjacks;
	}
	hideMachineRecipe('pumpJack', calcPumpjackMax, true);
	observe('bindedUpgradePumpJackOrb', () => hideMachineRecipe('pumpJack', calcPumpjackMax, false));
	observe('bindedGreenPumpjackOrb', () => hideMachineRecipe('pumpJack', calcPumpjackMax, false));
}



/**
 * fix magic
 */

const essenceMultiplier = {
	mineral: {
		stone: 20e6
		, copper: 10e6
		, tin: 10e6
		, iron: 5e6
		, silver: 2e6
		, gold: 1e6
		, quartz: 100e3
		, flint: 50e3
		, marble: 10e3
		, titanium: 5e3
		, promethium: 100
		, runite: 2
	}
	, oil: {
		oil: 5e7
		, rocketFuel: 1
	}
	, nature: {
		dottedGreenRoots: 18
		, greenRoots: 13
		, limeRoots: 6
		, goldRoots: 3
		, stripedGoldRoots: 1
		, crystalRoots: v => parseInt(v / 2) + 1
		, stripedCrystalRoots: v => parseInt(v / 4) + 1
	}
	, metallic: {
		bronzeBar: 1000
		, ironBar: 700
		, silverBar: 500
		, goldBar: 300
		, promethiumBar: 25
		, runiteBar: 1
	}
	, energy: {
		shrimp: 50
		, sardine: 20
		, tuna: 4
		, swordfish: 1
		, shark: v => parseInt(v / 3) + 1
		, whale: v => parseInt(v / 6) + 1
	}
	, orb: {
		blue: 3
		, green: 1
		, red: v => parseInt(v / 3) + 1
	}
	, gem: {
		sapphire: 8
		, emerald: 3
		, ruby: 1
		, diamond: v => parseInt(v / 5) + 1
	}
};
const essenceObserver = new Map();
function essenceCellStyle(cell, fulfilled)
{
	cell.style.backgroundColor = fulfilled ? '' : 'red';
	cell.style.color = fulfilled ? '' : 'white';
}
function essenceSetFulfilled(key, value, el)
{
	const fulfilled = window[key] >= value;
	essenceCellStyle(el.previousElementSibling, fulfilled);
	essenceCellStyle(el, fulfilled);
	essenceCellStyle(el.nextElementSibling, fulfilled);
}
function essenceRequirements(amount, type)
{
	if (essenceObserver.has(type))
	{
		essenceObserver.get(type).forEach((fn, key) =>
		{
			unobserve(key, fn);
		});
	}

	const observerMap = new Map();
	const makeString = 'make' + type[0].toUpperCase() + type.substr(1) + 'Essence';
	for (let key in essenceMultiplier[type])
	{
		const elId = makeString + key[0].toUpperCase() + key.substr(1) + '-needed';
		const el = document.getElementById(elId);
		const mult = essenceMultiplier[type][key];
		const value = amount == 0 ? 0 : (typeof mult === 'function' ? mult(amount) : amount * mult);
		el.textContent = formatNumber(value);

		const windowKey = type == 'orb' ? 'empty' + key[0].toUpperCase() + key.substr(1) + 'Orb' : key;
		essenceSetFulfilled(windowKey, value, el);
		const observeFn = observe(windowKey, () => essenceSetFulfilled(windowKey, value, el));
		observerMap.set(windowKey, observeFn);
	}
	essenceObserver.set(type, observerMap);
}
function fixMagic()
{
	// move roots to magic panel
	const parent = document.getElementById('magic-tab');
	const roots = document.querySelectorAll('[id$="Roots-box"]');
	for (let i = 0; i < roots.length; i++)
	{
		const el = roots[i].parentNode;
		el.setAttribute('tooltip', el.getAttribute('tooltip').replace(/ \(Used in the magic skill\)$/, ''));
		parent.appendChild(el);
	}
	const style = document.createElement('style');
	style.innerHTML = `
#magic-tab .inventory-item-box-farming
{
	float: left;
}
	`;
	document.head.appendChild(style);

	// improve tooltip of spell book
	const magicBook = document.getElementById('item-magicBook-box').parentNode;
	function updateTooltip()
	{
		const pages = [];
		for (let i = 1; i <= 6; i++)
		{
			if (window['bindedMagicPage' + i] == '1')
			{
				pages.push(i);
			}
		}
		const pagesString = pages.length === 0 ? '-' : pages.join(', ');
		magicBook.setAttribute('tooltip', `Spell Book (binded pages: ${pagesString})`);
	}
	updateTooltip();
	observe([
		'bindedMagicPage1'
		, 'bindedMagicPage2'
		, 'bindedMagicPage3'
		, 'bindedMagicPage4'
		, 'bindedMagicPage5'
		, 'bindedMagicPage6'
	], () => updateTooltip());

	const oldEmptyEssenceDialogue2 = window.emptyEssenceDialogue2;
	window.emptyEssenceDialogue2 = (type) =>
	{
		oldEmptyEssenceDialogue2(type);
		if (type == 'nature')
		{
			const input = document.querySelector(
				'#emptyEssence2-dialog-nature table.table-stats tr:last-child > td:last-child input'
			);
			if (input && input.style.display != 'none')
			{
				input.style.display = 'none';
				const inputCell = input.parentNode;
				inputCell.style.border = 0;
				const neededCell = inputCell.previousElementSibling;
				neededCell.style.border = 0;
				const imgCell = neededCell.previousElementSibling;
				imgCell.style.border = 0;
			}
		}
	};
	window.refreshOresValuesWhenMakingEssences = (amount) =>
	{
		essenceRequirements(amount, 'mineral');
	};
	window.refreshOilValuesWhenMakingEssences = (amount) =>
	{
		essenceRequirements(amount, 'oil');
	};
	window.refreshSeedsValuesWhenMakingEssences = (amount) =>
	{
		essenceRequirements(amount, 'nature');
	};
	window.refreshBarValuesWhenMakingEssences = (amount) =>
	{
		essenceRequirements(amount, 'metallic');
	};
	window.refreshFoodValuesWhenMakingEssences = (amount) =>
	{
		essenceRequirements(amount, 'energy');
	};
	window.refreshOrbValuesWhenMakingEssences = (amount) =>
	{
		essenceRequirements(amount, 'orb');
	};
	window.refreshGemValuesWhenMakingEssences = (amount) =>
	{
		essenceRequirements(amount, 'gem');
	};
}



/**
 * fix number format
 */

function formatNumber(num)
{
	// return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
	return parseFloat(num).toLocaleString('en');
}
function fixNumberFormat()
{
	// fix achievements
	const achievementFixes = {
		'achABiggerWall': 'Sell excactly ' + formatNumber(1e7) + ' stone to the shop.'
		, 'ach1000Potions': 'Drink a total of ' + formatNumber(1e3) + ' potions. '
		, 'achMaxInt': 'Have a total of ' + formatNumber(Math.pow(2, 31)-1) + ' ores in your inventory. '
		, 'achSmelter': 'Smelt a total of ' + formatNumber(5e5) + ' bars. '
	};
	for (let id in achievementFixes)
	{
		const row = document.getElementById(id);
		row.cells[1].firstChild.textContent = achievementFixes[id];
	}
	const oldLoadAchievements = window.loadAchievements;
	window.loadAchievements = () =>
	{
		oldLoadAchievements();
		document.getElementById('total-potions-drank').innerHTML = formatNumber(window.totalPotionsDrank) + ' potions drank';
		document.getElementById('total-bars-smelted').innerHTML = formatNumber(window.totalBarsSmelted) + ' bars smelted';
		document.getElementById('statVendor2').innerHTML = formatNumber(window.statVendor);
		document.getElementById('total-spellsCasted-ach').innerHTML = formatNumber(window.spellsCasted);
	};

	function checkAchievementRow(row)
	{
		if (window[row.id] != 1)
		{
			return false;
		}
		const pinkSpan = row.cells[1].children[0];
		if (pinkSpan)
		{
			pinkSpan.style.color = 'blue';
		}
		return true;
	}
	const table = document.querySelector('#ach-tab table.table-stats');
	const rows = table.rows;
	for (let i = 0; i < rows.length; i++)
	{
		const row = rows[i];
		if (row.id && !checkAchievementRow(row))
		{
			observe(row.id, () => checkAchievementRow(row));
		}
	}

	// fix explorers energy
	const oldExplorerTick = window.explorerTick;
	window.explorerTick = () =>
	{
		oldExplorerTick();
		const energyElement = document.getElementById('energy-amount');
		energyElement.innerHTML = 'Energy: ' + formatNumber(window.energy);
	};

	// fix leveling up in crafting
	window.setConvertBarToXpOnKeyDown = (amount) =>
	{
		var starDustCounter = window.bindedUpgradeEnchantedHammer >= 1 ? 10 : 13;
		document.getElementById('enchantedHammer-XP-hint-box').style.display = 'block';
		document.getElementById('enchantedHammer-XP-earned').innerHTML = '+'
			+ formatNumber(getXPEarnedWhenConvertingBar(barToConvertToXpType) * amount.value);

		document.getElementById('enchantedHammer-XP-total-stardust-cost').innerHTML = '-'
			+ formatNumber(getXPEarnedWhenConvertingBar(barToConvertToXpType) * amount.value * starDustCounter);
	};

	// fix blue coins
	const oldLoadCoins = window.loadCoins;
	window.loadCoins = () =>
	{
		oldLoadCoins();
		document.getElementById('platinumCoinsAmount-statusbar').innerHTML = formatNumber(window.platinumCoins);
	};

	// fix xp in "select a seed" dialog
	const seedButtons = document.querySelectorAll('#seed-menu-popup > div[id^="btn-"]');
	for (let i = 0; i < seedButtons.length; i++)
	{
		const cells = seedButtons[i].children[0].rows[0].cells;
		const xpNode = cells[cells.length-1].lastChild;
		const text = xpNode.textContent.replace(/(\d)\D+(?=\d)/g, '$1');
		const xp = parseInt(text, 10);
		xpNode.textContent = text.replace(xp.toString(10), formatNumber(xp));
	}
	
	// fix oil
	const oldLoadMiscVariables = window.loadMiscVariables;
	window.loadMiscVariables = () =>
	{
		oldLoadMiscVariables();
		document.getElementById('span-oilPerSecond').innerHTML = formatNumber(window.oilPerSeconds);
		document.getElementById('span-oilLosePerSecond').innerHTML = '-' + formatNumber(oilLosePerSeconds);
	};

	// fix artifact star dust calculation
	const artifactInput = document.getElementById('amount-to-convert-artifact');
	artifactInput.onkeyup = function ()
	{
		const xpRate = document.getElementById('artifact-xp-rate').value;
		const sdRate = window.bindedExploringOrb == 1 ? 22 : 26;
		document.getElementById('artifact-xp-earned').innerHTML = formatNumber(xpRate * this.value);
		document.getElementById('artifact-stardust-needed').innerHTML = formatNumber(xpRate * this.value * 22);
	};
	const oldOpenArtifactDialogue = window.openArtifactDialogue;
	window.openArtifactDialogue = (artifact, xp) =>
	{
		oldOpenArtifactDialogue(artifact, xp);
		artifactInput.onkeyup(null);
	};
}



/**
 * initialize notifications
 */

function observeTimer(k, onComplete, zero = 0)
{
	observe(k, (key, oldValue, newValue) =>
	{
		if (oldValue > zero && newValue == zero)
		{
			onComplete(key);
		}
	});
}
function notifyCoop(msg)
{
	window.send('OPEN_TAB=COOP');
	return notify('Group Task', {
		body: msg.replace(/!$/, '.')
		, icon: 'images/icons/coop.png'
	}).then((n) =>
	{
		n.onclick = () =>
		{
			window.focus();
			window.openTab('coop');
			n.close();
		};
		return n;
	});
}
function notifyVendor()
{
	/*
"I have changed my items, come check them out."<br><br><img src="images/shop/vendor.png" width="120px" height="140px">
	*/
	return notify('Vendor', {
		body: 'The vendor changed his items.'
		, icon: 'images/shop/vendor.png'
	})
		.then((n) =>
		{
			n.onclick = () =>
			{
				window.focus();
				window.openTab('vendor');
				n.close();
			};
			return n;
		})
	;
}
function notifyBoat(msg)
{
	/*
<b>Your boat brings back:</b><br><br><span class="exploring-norm-loot"><img class="small-img" src="images/exploring/rawSardine.png"> 2</span> <span class="exploring-norm-loot"><img class="small-img" src="images/exploring/rawTuna.png"> 1</span> 
	*/
	const tmp = document.createElement('templateWrapper');
	tmp.innerHTML = msg;
	const loot = [];
	const lootEls = tmp.querySelectorAll('.exploring-norm-loot');
	for (let i = 0; i < lootEls.length; i++)
	{
		const el = lootEls[i];
		const num = parseInt(el.textContent, 10);
		const match = el.innerHTML.match(/\/([^\/]+)\.png"/);
		if (match)
		{
			const itemName = match[1].replace(/([a-z])([A-Z])/, (wholeMatch, m1, m2) =>
			{
				return m1 + ' ' + m2.toLowerCase();
			});
			loot.push(num + ' ' + itemName);
		}
		else
		{
			loot.push(el.innerHTML);
		}
	}
	return notify('Fishing boat returns', {
		body: 'Your boat brings back: ' + loot.join(', ')
		, icon: 'images/exploring/fishingBoat.png'
	})
		.then((n) =>
		{
			n.onclick = () =>
			{
				window.focus();
				window.openTab('archaeology');
				n.close();
			};
			return n;
		})
	;
}
function notifyAchievement()
{
	/*
You have completed an achievement
	*/
	return notify('Achievement got', {
		body: 'You have completed an achievement.'
		, icon: 'images/shop/ach.png'
	})
		.then((n) =>
		{
			n.onclick = () =>
			{
				window.focus();
				window.openTab('ach');
				n.close();
			};
			return n;
		})
	;
}
function notifyMsg(msg)
{
	if (msg === 'You have completed your group task!' ||
		/ has completed his group task\.$/.test(msg))
	{
		return notifyCoop(msg);
	}
	else if (/I have changed my items, come check them out/.test(msg))
	{
		return notifyVendor();
	}
	else if (/Your boat brings back:/.test(msg))
	{
		notifyBoat(msg);
	}
	else if (msg === 'You have completed an achievement')
	{
		notifyAchievement();
	}
	else if (document.hidden || !document.hasFocus())
	{
		/*
Your account has been running for 234 Minutes
		*/
		notify('Message from server', {
			body: msg
			// , icon: 'images/minerals/diamond.png'
		}).then((n) =>
		{
			n.onclick = () =>
			{
				window.focus();
				n.close();
			};
		});
	}
	return Promise.reject();
}
function initNotifications()
{
	if (!getSetting('showNotifications'))
	{
		return;
	}

	notify = (title, options) =>
	{
		if (!("Notification" in window) ||
			Notification.permission === 'denied')
		{
			console.info('notification:', title, options);
			return Promise.reject('Notification permission denied');
		}

		if (Notification.permission === 'granted')
		{
			return Promise.resolve(new Notification(title, options));
		}
		return Notification.requestPermission().then(() => notify(title, options));
	};
	Notification.requestPermission().then(function (result)
	{
		if (result == 'denied')
		{
			console.error('Permission to show notifications has been denied by the user.');
		}
	});
	// don't send TAB_OFF
	window.checkIfTabIsOpen = () =>
	{
		if (window.tabOn == 0)
		{
			window.send('TAB_ON');
			window.tabOn = 1;
		}
	};

	function addClickListener(n, tabName)
	{
		n.onclick = () =>
		{
			window.focus();
			window.openTab(tabName);
			n.close();
		};
		return n;
	}
	let lastFarmingNotification;
	observeTimer(['farmingPatchTimer1', 'farmingPatchTimer2', 'farmingPatchTimer3', 'farmingPatchTimer4', 'farmingPatchTimer5', 'farmingPatchTimer6'], (key) =>
	{
		const now = (new Date).getTime();
		const timeDiff = now - (lastFarmingNotification || 0);
		if (timeDiff < 10e3)
		{
			return;
		}

		lastFarmingNotification = now;
		notify('Harvest', {
			body: 'One or more of your crops is ready for harvest.'
			, icon: 'images/icons/watering-can.png'
		}).then((n) => addClickListener(n, 'farming'));
	}, 1);
	observeTimer('exploringTimer', (key) =>
	{
		notify('Explorer ready', {
			body: 'Your explorer is back.'
			, icon: 'images/icons/archaeology.png'
		}).then((n) => addClickListener(n, 'archaeology'));
	});
	observeTimer('furnaceCurrentTimer', (key) =>
	{
		notify('Furnace ready', {
			body: 'Your smelting has finished.'
			, icon: 'images/crafting/' + furnaceLevels[window.bindedFurnaceLevel] + 'Furnace.gif'
		}).then((n) => addClickListener(n, 'repair'));
	});
	observeTimer('rocketTimer', (key) =>
	{
		notify('Rocket ready', {
			body: 'You landed on the moon.'
			, icon: 'images/crafting/rocket.png'
		}).then((n) => addClickListener(n, 'repair'));
	});
	observeTimer('robotTimer', (key) =>
	{
		notify('Robot ready', {
			body: 'Your robot is back.'
			, icon: 'images/crafting/robot.png'
		}).then((n) => addClickListener(n, 'repair'));
	});
	observeTimer('fishingBoatTimer', (key) =>
	{
		notify('Fishing boat ready', {
			body: 'Your fishing boat is back.'
			, icon: 'images/exploring/fishingBoat.png'
		}).then((n) => addClickListener(n, 'archaeology'));
	});
	observeTimer('largeFishingBoatTimer', (key) => (key) =>
	{
		notify('Large fishing boat ready', {
			body: 'Your large fishing boat is back.'
			, icon: 'images/exploring/largeFishingBoat.png'
		}).then((n) => addClickListener(n, 'archaeology'));
	});
	/*
	// potions
	'starDustPotionTimer'
	'coinPotionTimer'
	'seedPotionTimer'
	'smeltingPotionTimer'
	'oilPotionTimer'
	'miningPotionTimer'
	'superStarDustPotionTimer'
	'fastFurnacePotionTimer'
	'superCompostPotionTimer'
	'megaStarDustPotionTimer'
	'superOilPotionTimer'
	'whaleFishingPotionTimer'
	'fishingPotionTimer'
	'essencePotionTimer'
	'megaOilPotionTimer'
	'superEssencePotionTimer'
	'sparklingCompostPotionTimer'
	'engineeringPotionTimer'

	// magic effects
	'superDrillsTimer'
	'superGemFinderTimer'
	'smallSipsTimer'
	'superPirateTimer'
	'superCrushersTimer'
	'superGiantDrillsTimer'
	'fastVendorTimer'
	'superRoadHeadersTimer'
	'animatedAxeTimer'
	'superExcavatorsTimer'

	// ?
	'compostTimer'
	'eatingTimer'
	'exploringTimeReductionPerc'
	'ghostEssenceTimer'
	*/
}



/**
 * fix level bar
 */

function fixLevelBar()
{
	// size changing: 1267x65 -> 1256x105
	document.getElementById('level-status-up').style.lineHeight = '102px';

	const style = document.createElement('style');
	style.innerHTML = `
tr[id^="level-status-row"] > td > img:first-child[width="40px"]
{
	margin: 0 5px;
}
#level-status-row2 > td > img:first-child
{
	height: 50px;
}
.unlock-skill-btn
{
	margin-left: 5px;
}
span[id^="progress-percentage-"][id$="-small"]
{
	height: calc(90% + 2px);
	margin: 0;
}

#fishingBoat-timer > img:first-child
{
	width: 53px;
	height: 40px;
}
	`;
	document.head.appendChild(style);
}



/**
 * fix message box
 */

function fixMsgBox()
{
	const oldDialogFn = window.$.fn.dialog;
	window.$.fn.dialog = function (...args)
	{
		if (args[0] != 'close')
		{
			$('.ui-widget-header').show();
		}
		return oldDialogFn.apply(this, args);
	};

	const oldMessageBox = window.messageBox;
	let timeout;
	window.messageBox = (msg) =>
	{
		const $el = $('#dialog-timer');
		if ($el.hasClass('ui-dialog-content'))
		{
			$el.dialog('destroy');
		}

		document.getElementById('dialog-text-timer').innerHTML = msg;

		$el.dialog(
		{
			create: function (event, ui)
			{
				$('.ui-widget-header').hide();
			}
			, width: 550
			, height: 100
			, show:
			{
				effect: 'fade'
				, duration: 50
			}
			, hide:
			{
				effect: 'fade'
				, delay: 1000
				, duration: 1000
			}
		}).dialog('close');
	};
}



/**
 * add a notification box (like the harvest one) for coop events
 */

function addCoopNotificationBox()
{
	const notifBox = document.createElement('span');
	notifBox.id = 'coop-notif';
	notifBox.classList.add('notification-timer-box');
	notifBox.style.width = 'auto';
	notifBox.style.cursor = 'pointer';
	notifBox.style.display = 'none';
	notifBox.style.padding = '0 10px';
	notifBox.onclick = () =>
	{
		window.openTab('coop');
		window.send('OPEN_TAB=COOP');
	};
	notifBox.innerHTML = `<span class="activate-tooltip" title="Group task is finished">
		<img width="46px" height="40px" style="vertical-align: middle; padding: 5px 0px 5px 0px;" src="images/icons/coop.png">
		<span class="progress"></span>
	</span>`;
	document.getElementById('farming-notif').parentNode.appendChild(notifBox);
	const coopProgress = notifBox.querySelector('span.progress');

	const oldLoadCoop = window.loadCoop;
	window.loadCoop = (data) =>
	{
		const dataArray = data == 'none' ? [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] : data.split('~');
		const player = [
			  dataArray[0]
			, dataArray[1]
			, dataArray[2]
			, dataArray[3]
		];
		const task_id = [
			  dataArray[4]
			, dataArray[5]
			, dataArray[6]
			, dataArray[7]
		];
		const task_value = [
			  dataArray[8]
			, dataArray[9]
			, dataArray[10]
			, dataArray[11]
		];
		const task_neededValue = [
			  dataArray[12]
			, dataArray[13]
			, dataArray[14]
			, dataArray[15]
		];

		function isPlayer(i)
		{
			return player[i] !== 'none' && player[i] !== 'claimed';
		}
		const started = task_id.every((id, i) => !isPlayer(i) || id != 0);
		const totalNum = player.filter((name, i) => isPlayer(i)).length;
		const finishedNum = task_value.filter((value, i) => isPlayer(i) && value == task_neededValue[i]).length;
		const showBox = started && finishedNum > 0;
		notifBox.style.display = showBox ? '' : 'none';
		coopProgress.textContent = finishedNum == totalNum ? '' : finishedNum + '/' + totalNum;

		const i = player.indexOf(window.username);
		const thisFinished = started && task_value[i] == task_neededValue[i];
		coopProgress.style.color = thisFinished ? 'lime' : '';

		for (let j = 1; j <= 4; j++)
		{
			const row = document.getElementById('started-row-p' + j);
			if (row)
			{
				row.style.backgroundColor = j == (i+1) ? 'lightblue' : '';
			}
		}

		return oldLoadCoop(data);
	};
	window.send('OPEN_TAB=COOP');
}



/**
 * fix chat
 */

const chatHistoryKey = 'chatHistory';
const maxChatHistoryLength = 100;
const reloadedChatData = {
	timestamp: 0
	, username: ''
	, userlevel: 0
	, sigil: 0
	, tag: 0
	, type: -1
	, msg: '[...]'
};
let chatHistory = [];
function add2ChatHistory(data)
{
	const splitArray = data.split('~');
	data = {
		timestamp: (new Date()).getTime()
		, username: splitArray[0]
		, userlevel: parseInt(splitArray[1], 10)
		, sigil: parseInt(splitArray[3], 10)
		, tag: parseInt(splitArray[2], 10)
		, type: parseInt(splitArray[5], 10)
		, msg: splitArray[4]
	};
	if (data.type == 2)
	{
		data.userlevel = window.getGlobalLevel();
	}

	chatHistory.push(data);
	chatHistory = chatHistory.slice(-maxChatHistoryLength);
	localStorage.setItem(chatHistoryKey, JSON.stringify(chatHistory));
	return data;
}
function getChatTab(username)
{
	const chatTabs = document.getElementById('chat-tabs');
	let tab = chatTabs.querySelector('div.chat-tab[data-username="' + username + '"]');
	if (!tab)
	{
		tab = document.createElement('div');
		tab.className = 'chat-tab';
		tab.dataset.username = username;
		tab.dataset.new = 0;
		const filler = chatTabs.querySelector('.filler');
		if (filler)
		{
			chatTabs.insertBefore(tab, filler);
		}
		else
		{
			chatTabs.appendChild(tab);
		}
	}
	return tab;
}
function getChatDiv(username)
{
	const id = 'chat-' + (username == '' ? 'area-div' : 'pm-' + username);
	let div = document.getElementById(id);
	if (!div)
	{
		div = document.createElement('div');
		div.setAttribute('disabled', 'disabled');
		div.id = 'chat-pm-' + username;
		div.className = 'chat-area-div';

		const height = document.getElementById('chat-area-div').style.height;
		div.style.height = height;

		const generalChat = document.getElementById('chat-area-div');
		generalChat.parentNode.insertBefore(div, generalChat);
	}
	return div;
}
function changeChatTab(oldTab, newTab)
{
	const oldChatDiv = getChatDiv(oldTab.dataset.username);
	oldChatDiv.classList.remove('selected');
	const newChatDiv = getChatDiv(newTab.dataset.username);
	newChatDiv.classList.add('selected');

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

	if (window.isAutoScrolling)
	{
		setTimeout(() => newChatDiv.scrollTop = newChatDiv.scrollHeight);
	}
}
const chatSigils = [
	null
	, { key: 'maxLevel',		title: 'Maxed Skills' }
	, { key: 'maxMining',		title: 'Master in Mining' }
	, { key: 'maxCrafting',		title: 'Master in Crafting' }
	, { key: 'maxBrewing',		title: 'Master in Brewing' }
	, { key: 'maxFarming',		title: 'Master in Farming' }
	, { key: 'hardcore',		title: 'Hardcore Account' }
	, { key: 'halloween2015',	title: 'Halloween 2015' }
	, { key: 'maxExploring',	title: 'Master in Exploring' }
	, { key: 'christmas2015',	title: 'Chirstmas 2015' }
	, { key: 'maxMagic',		title: 'Master in Magic' }
	, { key: 'easter2016',		title: 'Holiday' }
	, { key: 'coop',			title: 'COOP' }
	, { key: 'maxCooking',		title: 'Master in Cooking' }
	, { key: 'halloween2016',	title: 'Halloween 2016' }
	, { key: 'christmas2016',	title: 'Chirstmas 2016' }
];
const chatTags = [
	null
	, { key: 'donor', name: '' }
	, { key: 'contributor', name: 'Contributor' }
	, null
	, { key: 'mod', name: 'Moderator' }
	, { key: 'dev', name: 'Dev' }
];
const linkParseRegex = /(^|\s)(https?:\/\/\S+|\S*www\.|\S+\.(?:com|ca|co|net|us))(\s|$)/;
function isPM(data)
{
	return data.type == 1 || data.type == 2;
}
const locale = 'en-US';
const localeOptions = {
	hour12: false
	, year: 'numeric'
	, month: 'long'
	, day: 'numeric'
	, hour: '2-digit'
	, minute: '2-digit'
	, second: '2-digit'
};
function newRefreshChat(data)
{
	// username is 3-12 characters long
	let chatbox = document.getElementById('chat-area-div');

	if (mutedPeople.some((name) => name == data.username))
	{
		return;
	}

	const isThisPm = isPM(data);
	const msgUsername = data.type == 2 ? window.username : data.username;

	const historyIndex = chatHistory.indexOf(data);
	const dataBefore = historyIndex != -1 &&
		chatHistory.slice(0, historyIndex).reverse().find((d) =>
		{
			return isThisPm && isPM(d) || !isThisPm && !isPM(d);
		})
	;
	let isSameUser = false;
	let isSameTime = false;
	if (dataBefore)
	{
		const beforeUsername = dataBefore.type == 2 ? window.username : dataBefore.username;
		isSameUser = beforeUsername === msgUsername;
		isSameTime = Math.floor(data.timestamp / 1000 / 60) - Math.floor(dataBefore.timestamp / 1000 / 60) === 0;
	}

	const d = new Date(data.timestamp);
	const hour = (d.getHours() < 10 ? '0' : '') +  d.getHours();
	const minute = (d.getMinutes() < 10 ? '0' : '') +  d.getMinutes();
	const sigil = chatSigils[data.sigil] || { key: '', title: '' };
	const tag = chatTags[data.tag] || { key: '', name: '' };
	const formattedMsg = data.msg.replace(new RegExp(linkParseRegex, 'g'), (wholeMatch, before, link, after) =>
	{
		if (/%22|%27|%3E|%3C|&#62|&#60;|;|~|\\"|<|>|javascript:|window|document|cookie/.test(link))
		{
			return wholeMatch;
		}
		link = (link.startsWith('http') ? '' : 'http://') + link;
		return before + `<a href="${link}" target="_blank">${link}</a>` + after;
	});

	const msgTitle = data.type == -1 ? 'Chat loaded on ' + d.toLocaleString(locale, localeOptions) : '';
	let chatSegment = `<span class="chat-msg" data-type="${data.type}" data-tag="${tag.key}">`
		+ `<span
			class="timestamp"
			data-hour="${hour}"
			data-minute="${minute}"
			title="${d.toLocaleString(locale, localeOptions)}"
			data-same-time="${isSameTime}"></span>`
		+ `<span class="user" data-name="${data.username}" data-same-user="${isSameUser}">`
			+ `<span class="sigil ${sigil.key}" title="${sigil.title}"></span>`
			+ `<span class="tag chat-tag-${tag.key}">${tag.name}</span>`
			+ `<span
				class="name"
				data-level="${data.userlevel}"
				oncontextmenu="searchPlayerHicores('${msgUsername}');return false;"
				onclick="preparePM('${msgUsername}')">${msgUsername}</span>`
		+ `</span>`
		+ `<span class="msg" title="${msgTitle}">${formattedMsg}</span>`
	+ `</span>`;

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

	const tmp = document.createElement('templateWrapper');
	tmp.innerHTML = chatSegment;
	while (tmp.childNodes.length > 0)
	{
		chatbox.appendChild(tmp.childNodes[0]);
	}

	if (window.isAutoScrolling)
	{
		setTimeout(() => chatbox.scrollTop = chatbox.scrollHeight);
	}
}
function applyChatStyle()
{
	const style = document.createElement('style');
	style.innerHTML = `
span.chat-msg
{
	display: flex;
	margin-bottom: 1px;
}
.chat-msg .timestamp::before
{
	color: hsla(0, 0%, 50%, 1);
	font-size: .9rem;
}
.chat-msg .timestamp[data-same-time="true"]::before
{
}
#chat-toggleTimestamps:checked ~ div[id^="chat-"] .chat-msg:not([data-type="-1"]) .timestamp::before
{
	content: attr(data-hour) ':' attr(data-minute);
	display: inline-block;
	margin: 0 5px;
	width: 2.5rem;
}

.chat-msg[data-type="1"] { color: purple; }
.chat-msg[data-type="2"] { color: purple; }
.chat-msg[data-type="3"] { color: blue; }
.chat-msg[data-tag="contributor"] { color: green; }
.chat-msg[data-tag="mod"] { color: #669999; }
.chat-msg[data-tag="dev"] { color: #666600; }
.chat-msg .user
{
	margin-right: 5px;
	white-space: nowrap;
}
.chat-msg .user[data-same-user="true"]:not([data-name="none"])
{
	opacity: .3;
}
.chat-msg .user .name:not([data-level=""])::after
{
	content: ' (' attr(data-level) '):';
}

.chat-msg .user .sigil:not([class$=" "])::before
{
	background-size: 20px 20px;
	content: '';
	display: inline-block;
	margin-right: 1px;
	width: 20px;
	height: 20px;
	vertical-align: middle;
}
.chat-msg .user .sigil.maxLevel::before { background-image: url('images/icons/stats.png'); }
.chat-msg .user .sigil.maxCrafting::before { background-image: url('images/icons/anvil.png'); }
.chat-msg .user .sigil.maxMining::before { background-image: url('images/icons/pickaxe.png'); }
.chat-msg .user .sigil.maxBrewing::before { background-image: url('images/brewing/vialofwater_chat.png'); }
.chat-msg .user .sigil.maxFarming::before { background-image: url('images/icons/watering-can.png'); }
.chat-msg .user .sigil.maxExploring::before { background-image: url('images/icons/archaeology.png'); }
.chat-msg .user .sigil.maxCooking::before { background-image: url('images/icons/cookingskill.png'); }
.chat-msg .user .sigil.maxMagic::before { background-image: url('images/magic/wizardHatIcon.png'); }
.chat-msg .user .sigil.hardcore::before { background-image: url('images/icons/hardcoreIcon.png'); }
.chat-msg .user .sigil.coop::before { background-image: url('images/icons/groupTaskBadge5.png'); }
.chat-msg .user .sigil.halloween2015::before { background-image: url('images/icons/halloween2015.png'); }
.chat-msg .user .sigil.christmas2015::before { background-image: url('images/sigils/christmas2015.png'); }
.chat-msg .user .sigil.easter2016::before { background-image: url('images/sigils/easter2016.png'); }
.chat-msg .user .sigil.halloween2016::before { background-image: url('images/sigils/halloween2016.png'); }
.chat-msg .user .sigil.christmas2016::before { background-image: url('images/sigils/christmas2016.png'); }

.chat-msg .user .tag
{
	margin-right: 3px;
}
.chat-msg .user .tag.chat-tag-
{
	display: none;
}
.chat-msg .user .tag.chat-tag-donor::before
{
	background-image: url('images/icons/donor-icon.gif');
	background-size: 20px 20px;
	content: '';
	display: inline-block;
	height: 20px;
	width: 20px;
	vertical-align: middle;
}

.chat-msg .user .name
{
	color: rgba(0, 0, 0, 0.7);
}
.chat-msg[data-type="-1"] .user > *,
.chat-msg[data-type="1"] .user > .sigil,
.chat-msg[data-type="1"] .user > .tag,
.chat-msg[data-type="2"] .user > .sigil,
.chat-msg[data-type="2"] .user > .tag,
.chat-msg[data-type="3"] .user > *
{
	display: none;
}
.chat-msg[data-type="3"] .user::before
{
	background: -webkit-linear-gradient(#004747, #00FFFF);
	background: -o-linear-gradient(#004747, #00FFFF);
	background: -moz-linear-gradient(#004747, #00FFFF);
	background: linear-gradient(#004747, #00FFFF);
	border: 1px solid black;
	color: white;
	content: 'Server Message';
	font-family: Comic Sans MS, "Times New Roman", Georgia, Serif;
	font-size: 9pt;
	padding: 0px 5px 2px 5px;
}

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

#chat-box-area .chat-area-div
{
	width: 100%;
	height: 130px;
	display: none;
}
#chat-box-area .chat-area-div.selected
{
	display: block;
}
#chat-tabs
{
	background-color: hsla(0, 0%, 90%, 1);
	display: flex;
	margin: 10px -10px -10px;
	flex-wrap: wrap;
}
#chat-tabs .chat-tab
{
	background-color: gray;
	border-top: 1px solid black;
	border-right: 1px solid black;
	cursor: pointer;
	display: inline-block;
	font-weight: normal;
	padding: 0.3rem .6rem;
}
#chat-tabs .chat-tab.selected
{
	background-color: silver;
	border-top-color: silver;
}
#chat-tabs .chat-tab.filler
{
	background-color: transparent;
	border-right: 0;
	box-shadow: inset 5px 5px 5px -5px rgba(0, 0, 0, 0.5);
	color: transparent;
	cursor: default;
	flex-grow: 1;
}
#chat-tabs .chat-tab::before
{
	content: attr(data-username);
}
#chat-tabs .chat-tab:not(.filler)[data-username=""]::before
{
	content: 'Server';
}
#chat-tabs .chat-tab::after
{
	content: '(' attr(data-new) ')';
	font-size: .9rem;
	font-weight: bold;
	margin-left: .4rem;
}
#chat-tabs .chat-tab[data-new="0"]::after
{
	font-weight: normal;
}
	`;
	document.head.appendChild(style);
}
function fixChat()
{
	if (!getSetting('useNewChat'))
	{
		return;
	}

	const chatBoxArea = document.getElementById('chat-box-area');

	const toggles = chatBoxArea.querySelectorAll('input[value^="Toggle"]');
	for (let i = 0; i < toggles.length; i++)
	{
		const toggle = toggles[i];
		const parent = toggle.parentNode;
		const toggleWhat = toggle.value.replace('Toggle ', '');
		const id = 'chat-toggle' + toggleWhat;
		const checkbox = document.createElement('input');
		checkbox.type = 'checkbox';
		checkbox.id = id;
		checkbox.checked = ['Autoscroll', 'Timestamps'].includes(toggleWhat);
		parent.insertBefore(checkbox, toggle);
		const label = document.createElement('label');
		label.htmlFor = id;
		label.textContent = toggleWhat;
		parent.insertBefore(label, toggle);
		toggle.style.display = 'none';

		checkbox.addEventListener('change', () =>
		{
			if (toggleWhat == 'Autoscroll')
			{
				window.isAutoScrolling = !window.isAutoScrolling;
			}
			else if (toggleWhat == 'Timestamps')
			{
				window.showTimestamps = !window.showTimestamps;
			}
			else
			{
				toggle.click();
			}
		});
	}

	// add chat tabs
	const chatTabs = document.createElement('div');
	chatTabs.id = 'chat-tabs';
	chatTabs.addEventListener('click', (event) =>
	{
		const newTab = event.target;
		if (!newTab.classList.contains('chat-tab') || newTab.classList.contains('filler'))
		{
			return;
		}

		const oldTab = chatTabs.querySelector('.chat-tab.selected');
		if (newTab == oldTab)
		{
			return;
		}
		oldTab.classList.remove('selected');
		newTab.classList.add('selected');
		newTab.dataset.new = 0;

		changeChatTab(oldTab, newTab);
	});
	chatBoxArea.appendChild(chatTabs);

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

	const oldSendChat = window.sendChat;
	window.sendChat = (msg) =>
	{
		const selectedTab = document.querySelector('.chat-tab.selected');
		if (selectedTab.dataset.username != '')
		{
			msg = '/pm ' + selectedTab.dataset.username + ' ' + msg;
		}
		oldSendChat(msg);
	};

	const oldChatBoxZoom = window.chatBoxZoom;
	window.chatBoxZoom = (zoom) =>
	{
		oldChatBoxZoom(zoom);

		const height = document.getElementById('chat-area-div').style.height;
		const chatDivs = chatBoxArea.querySelectorAll('div[id^="chat-pm-"]');
		for (let i = 0; i < chatDivs.length; i++)
		{
			chatDivs[i].style.height = height;
		}
	};

	chatHistory = JSON.parse(localStorage.getItem(chatHistoryKey) || JSON.stringify(chatHistory));
	const lastNotPM = chatHistory.slice(0).reverse().find((d) =>
	{
		return d.type != 1 && d.type != 2;
	});
	if (lastNotPM && lastNotPM.type != -1)
	{
		reloadedChatData.timestamp = (new Date()).getTime();
		chatHistory.push(reloadedChatData);
	}
	chatHistory.forEach(d => newRefreshChat(d));
	// reset the new counter for all tabs
	const tabs = document.querySelectorAll('.chat-tab');
	for (let i = 0; i < tabs.length; i++)
	{
		tabs[i].dataset.new = 0;
	}
	applyChatStyle();

	const oldRefreshChat = window.refreshChat;
	window.refreshChat = (data) =>
	{
		data = add2ChatHistory(data);
		return newRefreshChat(data);
	};
}



/**
 * fix crafting
 */

function fixCrafting()
{
	// show selection for the bar type
	const oldSetConvertBarToXpAgain = window.setConvertBarToXpAgain;
	window.setConvertBarToXpAgain = (barType, amount) =>
	{
		oldSetConvertBarToXpAgain(barType, amount);

		const selector = (bar = '') => `#enchanted-hammer-boxes input[type="image"][src$="${bar}bar.png"]`;
		const barImages = document.querySelectorAll(selector());
		for (let i = 0; i < barImages.length; i++)
		{
			barImages[i].style.backgroundColor = '';
		}
		const img = document.querySelector(selector(barType));
		img.style.backgroundColor = 'red';
	};
}



/**
 * activity log
 */

const activityLogKey = 'activityLog';
const maxActivityLogLength = 200;
let activityLog = [];
function add2ActivityLog(cmd)
{
	const split = cmd.split('=');
	const data = {
		type: split[0]
		, msg: split.slice(1).join('=')
		, read: false
	};

	activityLog.push(data);
	activityLog = activityLog.slice(-maxActivityLogLength);
	localStorage.setItem(activityLogKey, JSON.stringify(activityLog));
	return data;
}
function updateActivity(data)
{
	const activityLogEl = document.getElementById('activity-log');
	data.read = activityLogEl.classList.contains('open');
}
function initActivityLog()
{
	activityLog = JSON.parse(localStorage.getItem(activityLogKey) || JSON.stringify(activityLog));
	activityLog.forEach(d => updateActivity(d));
}



/**
 * init
 */

function init()
{
	console.info('[%s] "DHO Fixed" up and running!', (new Date).toLocaleTimeString());

	initObservable();
	initSettings();
	initActivityLog();
	initNotifications();

	fixKeyItems();
	fixFarming();
	fixServerMsg();
	applyNewItemStyle();
	applyNewKeyItemStyle();

	expandEquipment();
	highlightRequirements();
	fixMarket();
	improveLevelCalculation();
	fixInventory();
	fixMachinery();
	fixBrewing();
	fixTabs();
	hideCraftingRecipes();
	hideEquipment();
	improveDialogBtns();
	hideMaxRecipes();
	fixMagic();
	fixNumberFormat();
	fixLevelBar();
	fixMsgBox();
	fixChat();
	addCoopNotificationBox();
	fixCrafting();
}
document.addEventListener('DOMContentLoaded', () =>
{
	const oldLoadCommand = window.loadCommand;
	const msgPrefix = 'MESSAGE=';
	const msgBoxPrefix = 'MSG_BOX=';
	window.loadCommand = (cmd) =>
	{
		if (!fullyLoaded && cmd.startsWith('ITEMS_DATA='))
		{
			const ret = oldLoadCommand(cmd);
			fullyLoaded = true;
			init();
			return ret;
		}

		if (!/^(?:ITEMS_DATA|SET_TRADABLE_ITEMS|REFRESH_OFFER_TRADE|REFRESH_CHAT|PLAY_SOUND|COOP|MESSAGE|MSG_BOX)=/.test(cmd))
		{
			console.debug('new cmd:', cmd);
		}
		// add some activity log here!
		if (/^(?:QUESTION|MESSAGE|MSG_BOX)=/.test(cmd))
		{
			const data = add2ActivityLog(cmd);
			updateActivity(data);
		}
		if (cmd === msgBoxPrefix + 'You have completed an achievement')
		{
			const msg = cmd.substr(msgBoxPrefix.length);
			notifyMsg(msg)
				.catch(() => oldLoadCommand(cmd))
			;
			return;
		}
		else if (cmd.startsWith(msgPrefix))
		{
			const msg = cmd.substr(msgPrefix.length);
			notifyMsg(msg)
				.catch(() => oldLoadCommand(cmd))
			;
			return;
		}
		return oldLoadCommand(cmd);
	};
});

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址