DH2 Fixed

Improve Diamond Hunt 2

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

(function ()
{
'use strict';



/**
 * observer
 */

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

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



/**
 * global constants
 */

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



/**
 * general functions
 */

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



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

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

		setRecipeVisibility(key, level > maxLevel);
	}

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

	setRecipeVisibility(emptyKey, !hasTool);

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

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

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

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

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



/**
 * improve item boxes
 */

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

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

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

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

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

	// show boat progress
	function setTransitText(span, isInTransit)
	{
		span.textContent = isInTransit ? 'In transit' : 'Ready';
	}
	const boatSpan = addSpan2ItemBox('boundRowBoat');
	setTransitText(boatSpan, window.rowBoatTimer > 0);
	observe('rowBoatTimer', () => setTransitText(boatSpan, window.rowBoatTimer > 0));
	const canoeSpan = addSpan2ItemBox('boundCanoe');
	setTransitText(canoeSpan, window.canoeTimer > 0);
	observe('canoeTimer', () => setTransitText(canoeSpan, window.canoeTimer > 0));
}



/**
 * fix wood cutting
 */

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



/**
 * fix chat
 */

const lastMsg = new Map();
function isMuted(user)
{
	// return window.mutedPeople.some((name) => user.indexOf(name) > -1);
	return window.mutedPeople.includes(user);
}
function isSpam(user, msg)
{
	return lastMsg.has(user) &&
		lastMsg.get(user).msg == msg &&
		// last message in the last 30 seconds?
		(now() - lastMsg.get(user).time) < 30e3;
}
function handleSpam(user, msg)
{
	const msgObj = lastMsg.get(user);
	msgObj.time = now();
	msgObj.repeat++;
	// a user is allowed to repeat a message twice (write it 3 times in total)
	if (msgObj.repeat > 1)
	{
		window.mutedPeople.push(user);
	}
}
function fixChat()
{
	const _addToChatBox = window.addToChatBox;
	window.addToChatBox = (userChatting, iconSet, tagSet, msg, isPM) =>
	{
		if (isMuted(userChatting))
		{
			return;
		}
		if (isSpam(userChatting, msg))
		{
			return handleSpam(userChatting, msg);
		}
		lastMsg.set(userChatting, {
			time: now()
			, msg: msg
			, repeat: 0
		});

		// add clickable links
		msg = msg.replace(/(https?:\/\/[^\s"<>]+)/g, '<a target="_blank" href="$1">$1</a>');

		_addToChatBox(userChatting, iconSet, tagSet, msg, isPM);
	};

	// add checkbox instead of button for toggling auto scrolling
	const btn = document.querySelector('input[value="Toggle Autoscroll"]');
	const checkboxId = 'chat-toggle-autoscroll';
	// create checkbox
	const toggleCheckbox = document.createElement('input');
	toggleCheckbox.type = 'checkbox';
	toggleCheckbox.id = checkboxId;
	toggleCheckbox.checked = true;
	// create label
	const toggleLabel = document.createElement('label');
	toggleLabel.htmlFor = checkboxId;
	toggleLabel.textContent = 'Autoscroll';
	btn.parentNode.insertBefore(toggleCheckbox, btn);
	btn.parentNode.insertBefore(toggleLabel, btn);
	btn.style.display = 'none';

	toggleCheckbox.addEventListener('change', function ()
	{
		window.isAutoScrolling = this.checked;
		window.scrollText('none', this.checked ? 'lime' : 'red', (this.checked ? 'En' : 'Dis') + 'abled');
	});
}



/**
 * hopefully only temporary fixes
 */

function temporaryFixes()
{
	// fix recipe of oil storage 3
	const _processCraftingTab = window.processCraftingTab;
	window.processCraftingTab = () =>
	{
		const reinit = !!window.refreshLoadCraftingTable;
		_processCraftingTab();

		if (reinit)
		{
			// 200 instead of 100 gold bars
			window.craftingRecipes['oilStorage3'].recipeCost[2] = 200;
			document.getElementById('recipe-cost-oilStorage3-2').textContent = 200;
			window.showMateriesNeededAndLevelLabels('oilStorage3');
		}
	};

	// fix burn rate of ovens
	window.getOvenBurnRate = () =>
	{
		if (boundBronzeOven == 1)
		{
			return .5;
		}
		else if (boundIronOven == 1)
		{
			return .4;
		}
		else if (boundSilverOven == 1)
		{
			return .3;
		}
		else if (boundGoldOven == 1)
		{
			return .2;
		}
		return 1;
	};

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

	// fix exhaustion timer and updating brewing recipes
	const _clientGameLoop = window.clientGameLoop;
	window.clientGameLoop = () =>
	{
		_clientGameLoop();
		if (document.getElementById('tab-container-combat').style.display != 'none')
		{
			window.combatNotFightingTick();
		}
		if (currentOpenTab == 'brewing')
		{
			window.processBrewingTab();
		}
	};

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

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



/**
 * improve timer
 */

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

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

	// add tree grow timer
	const treeInfo = {
		1: {
			name: 'Normal tree'
			// 3h = 10800s
			, growTime: 3 * 60 * 60
		}
		, 2: {
			name: 'Oak tree'
			// 6h = 21600s
			, growTime: 6 * 60 * 60
		}
		, 3: {
			name: 'Willow tree'
			 // 8h = 28800s
			, growTime: 8 * 60 * 60
		}
	};
	function updateTreeInfo(place, nameEl, timerEl, init)
	{
		const idKey = 'treeId' + place;
		const growTimerKey = 'treeGrowTimer' + place;
		const lockedKey = 'treeUnlocked' + place;

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

		if (init)
		{
			observe(
				[idKey, growTimerKey, lockedKey]
				, () => updateTreeInfo(place, nameEl, timerEl, false)
			);
		}
	}
	for (let i = 0; i < 6; i++)
	{
		const treePlace = i+1;
		const treeContainer = document.getElementById('wc-div-tree-' + treePlace);
		treeContainer.style.position = 'relative';
		const infoEl = document.createElement('div');
		infoEl.setAttribute('style', 'position: absolute; top: 0; left: 0; right: 0; pointer-events: none; margin-top: 5px; color: white;');
		const treeName = document.createElement('div');
		treeName.style.fontSize = '1.2rem';
		infoEl.appendChild(treeName);
		const treeTimer = document.createElement('div');
		infoEl.appendChild(treeTimer);
		treeContainer.appendChild(infoEl);

		updateTreeInfo(treePlace, treeName, treeTimer, true);
	}
}



/**
 * improve smelting dialog
 */

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

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

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



/**
 * add chance to time calculator
 */

/**
 * calculates the number of seconds until the event with the given chance happened at least once with the given
 * probability p (in percent)
 */
function calcSecondsTillP(chancePerSecond, p)
{
	return Math.round(Math.log(1 - p/100) / Math.log(1 - chancePerSecond));
}
function addChanceTooltip(headline, chancePerSecond, elId, targetEl)
{
	// ensure element existence
	const tooltipElId = 'tooltip-chance-' + elId;
	let tooltipEl = document.getElementById(tooltipElId);
	if (!tooltipEl)
	{
		tooltipEl = document.createElement('div');
		tooltipEl.id = tooltipElId;
		tooltipEl.style.display = 'none';
		document.getElementById('tooltip-list').appendChild(tooltipEl);
	}

	// set elements content
	const percValues = [1, 10, 20, 50, 80, 90, 99];
	let percRows = '';
	for (let p of percValues)
	{
		percRows += `
			<tr>
				<td>${p}%</td>
				<td>${formatTime2NearestUnit(calcSecondsTillP(chancePerSecond, p), true)}</td>
			</tr>`;
	}
	tooltipEl.innerHTML = `<h2>${headline}</h2>
		<table class="chance">
			<tr>
				<th>Probability</th>
				<th>Time</th>
			</tr>
			${percRows}
		</table>
	`;

	// ensure binded events to show the tooltip
	if (targetEl.dataset.tooltipId == null)
	{
		targetEl.setAttribute('data-tooltip-id', tooltipElId);
		window.$(targetEl).bind({
			mousemove: window.changeTooltipPosition
			, mouseenter: window.showTooltip
			, mouseleave: window.hideTooltip
		});
	}
}
function chance2TimeCalculator()
{
	addStyle(`
table.chance
{
	border-spacing: 0;
}
table.chance th
{
	border-bottom: 1px solid gray;
}
table.chance td:first-child
{
	border-right: 1px solid gray;
	text-align: center;
}
table.chance th,
table.chance td
{
	padding: 4px 8px;
}
table.chance tr:nth-child(2n) td
{
	background-color: white;
}
	`);

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

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

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

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



/**
 * add notification boxes
 */

function addNotifBox(id, icon)
{
	const notificationArea = document.getElementById('notifaction-area');
	const notifBox = document.createElement('span');
	notifBox.className = 'notif-box';
	notifBox.id = 'notif-' + id;
	notifBox.innerHTML = `<img src="images/${icon}" class="image-icon-50" id="notif-${id}-img">`;
	notificationArea.appendChild(notifBox);
	return notifBox;
}
function addClickableNotifBox(id, icon, tabName)
{
	const notifBox = addNotifBox(id, icon);
	notifBox.style.cursor = 'pointer';
	notifBox.addEventListener('click', () => window.openTab(tabName));
	return notifBox;
}
function showStageNotification(stagePrefix, notifBox, init)
{
	const keys2Observe = [];
	let show = false;
	for (let i = 1; i <= 6; i++)
	{
		const key = stagePrefix + 'Stage' + i;
		keys2Observe.push(key);
		show = show || window[key] == 4;
	}
	notifBox.style.display = show ? '' : 'none';

	if (init)
	{
		observe(keys2Observe, () => showStageNotification(stagePrefix, notifBox, false));
	}
}
function addNotificationBoxes()
{
	// tree / wood cutting notification
	const treeNotifBox = addClickableNotifBox('woodCutter', 'icons/woodcutting.png', 'woodcutting');
	treeNotifBox.title = 'There is some wood to chop';
	window.$(treeNotifBox).tooltip();
	showStageNotification('tree', treeNotifBox, true);

	// farming notification
	const harvestNotifBox = addClickableNotifBox('farming', 'icons/watering-can.png', 'farming');
	harvestNotifBox.title = 'Some plants are ready for harvest';
	window.$(harvestNotifBox).tooltip();
	showStageNotification('farmingPatch', harvestNotifBox, true);

	// combat cooldown timer
	const combatNotifBox = addNotifBox('combatCooldown', 'icons/combat.png');
	// const combatNotifBox = addNotifBox('combatCooldown', 'icons/hourglass.png');
	const combatTimer = document.createElement('span');
	combatNotifBox.appendChild(combatTimer);
	function updateCombatTimer()
	{
		const cooldown = parseInt(window.combatGlobalCooldown, 10);
		const show = cooldown > 0;
		combatNotifBox.style.display = show ? '' : 'none';
		combatTimer.textContent = formatTimer(cooldown);
	}
	observe('combatGlobalCooldown', () => updateCombatTimer());
	updateCombatTimer();
}



/**
 * add tooltips for recipes
 */

function updateRecipeTooltips(recipeKey, recipes)
{
	const table = document.getElementById('table-' + recipeKey + '-recipe');
	const rows = table.rows;
	for (let i = 1; i < rows.length; i++)
	{
		const row = rows[i];
		const key = row.id.replace(recipeKey + '-', '');
		const recipe = recipes[key];
		const requirementCell = row.cells[3];
		requirementCell.title = recipe.recipe
			.map((name, i) =>
			{
				return formatNumber(recipe.recipeCost[i]) + ' '
					+ name.replace(/[A-Z]/g, (match) => ' ' + match.toLowerCase())
				;
			})
			.join(' + ')
		;
		window.$(requirementCell).tooltip();
	}
}
function addRecipeTooltips()
{
	const _processCraftingTab = window.processCraftingTab;
	window.processCraftingTab = () =>
	{
		const reinit = !!window.refreshLoadCraftingTable;
		_processCraftingTab();

		if (reinit)
		{
			updateRecipeTooltips('crafting', window.craftingRecipes);
		}
	};

	const _processBrewingTab = window.processBrewingTab;
	window.processBrewingTab = () =>
	{
		const reinit = !!window.refreshLoadBrewingTable;
		_processBrewingTab();

		if (reinit)
		{
			updateRecipeTooltips('brewing', window.brewingRecipes);
		}
	}

	const _processMagicTab = window.processMagicTab;
	window.processMagicTab = () =>
	{
		const reinit = !!window.refreshLoadCraftingTable;
		_processMagicTab();

		if (reinit)
		{
			updateRecipeTooltips('magic', window.magicRecipes);
		}
	}
}



/**
 * fix formatting of numbers
 */

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

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



/**
 * style tweaks
 */

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

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



/**
 * init
 */

function init()
{
	temporaryFixes();

	hideCraftedRecipes();
	improveItemBoxes();
	fixWoodcutting();
	fixChat();
	improveTimer();
	improveSmelting();
	chance2TimeCalculator();
	addNotificationBoxes();
	addRecipeTooltips();

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

			window.refreshItemValues(itemArray, false);

			if (window.firstLoadGame)
			{
				window.loadInitial();
				window.firstLoadGame = false;
				init();
			}
			else
			{
				window.clientGameLoop();
			}
			return;
		}
		return _doCommand(data);
	};
});



/**
 * fix web socket errors
 */

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

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

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

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

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

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

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