DH2 Fixed

Improve Diamond Hunt 2

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         DH2 Fixed
// @namespace    FileFace
// @description  Improve Diamond Hunt 2
// @version      0.51.1
// @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;
}
// use time format established in DHQoL (https://greasyfork.org/scripts/16041-dhqol)
function formatTimer(timer)
{
	timer = parseInt(timer, 10);
	const hours = Math.floor(timer / 3600);
	const minutes = Math.floor((timer % 3600) / 60);
	const seconds = timer % 60;
	return padLeft(hours, '0') + ':' + padLeft(minutes, '0') + ':' + padLeft(seconds, '0');
}
const timeSteps = [
	{
		threshold: 1
		, name: 'second'
		, short: 'sec'
		, padp: 0
	}
	, {
		threshold: 60
		, name: 'minute'
		, short: 'min'
		, padp: 0
	}
	, {
		threshold: 3600
		, name: 'hour'
		, short: 'h'
		, padp: 1
	}
	, {
		threshold: 86400
		, name: 'day'
		, short: 'd'
		, padp: 2
	}
];
function formatTime2NearestUnit(time, long = false)
{
	let step = timeSteps[0];
	for (let i = timeSteps.length-1; i > 0; i--)
	{
		if (time >= timeSteps[i].threshold)
		{
			step = timeSteps[i];
			break;
		}
	}
	const factor = Math.pow(10, step.padp);
	const num = Math.round(time / step.threshold * factor) / factor;
	const unit = long ? step.name + (num === 1 ? '' : 's') : step.short;
	return num + ' ' + unit;
}
const storePrefix = 'dh2-';
const store = {
	get: (key) =>
	{
		const value = localStorage.getItem(storePrefix + key);
		try
		{
			return JSON.parse(value);
		}
		catch (e) {}
		return value;
	}
	, has: (key) =>
	{
		return localStorage.hasOwnProperty(storePrefix + key);
	}
	, persist: (key, value) =>
	{
		localStorage.setItem(storePrefix + key, JSON.stringify(value));
	}
	, remove: (key) =>
	{
		localStorage.removeItem(storePrefix + key);
	}
};



/**
 * 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);
	hideNumberInItemBox('cooksBook', true);
	hideNumberInItemBox('cooksPage', true);
	for (let tierItem of tierItemList)
	{
		for (let i = 0; i < tierLevels.length; i++)
		{
			const key = getTierKey(tierItem, i);
			const toolKey = tierItem == 'rake' ? key : getBoundKey(key);
			const tierSpan = addSpan2ItemBox(toolKey);
			tierSpan.className = 'tier';
			tierSpan.textContent = tierNames[i];
		}
	}

	// show boat progress
	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
 */

function isMuted(user)
{
	// return window.mutedPeople.some((name) => user.indexOf(name) > -1);
	return window.mutedPeople.includes(user);
}
function handleScrolling(chatbox)
{
	if (window.isAutoScrolling)
	{
		setTimeout(() => chatbox.scrollTop = chatbox.scrollHeight);
	}
}
const chatHistoryKey = 'chatHistory';
const maxChatHistoryLength = 100;
const TYPE_RELOAD = -1;
const TYPE_NORMAL = 0;
const TYPE_PM_FROM = 1;
const TYPE_PM_TO = 2;
const TYPE_SERVER_MSG = 3;
const reloadedChatData = {
	timestamp: 0
	, username: ''
	, userlevel: 0
	, icon: 0
	, tag: 0
	, type: TYPE_RELOAD
	, msg: '[...]'
};
let chatHistory = [];
function processChatData(username, icon, tag, msg, isPM)
{
	let userlevel = 0;
	let type = tag == 5 ? TYPE_SERVER_MSG : TYPE_NORMAL;
	if (isPM == 1)
	{
		const match = msg.match(/^\s*\[(.+) ([A-Za-z0-9 ]+)\]: (.+?)\s*$/) || ['', '', username, msg];
		type = match[1] == 'Sent to' ? TYPE_PM_TO : TYPE_PM_FROM;
		username = match[2];
		msg = match[3];
	}
	else if (tag != 5)
	{
		const match = msg.match(/^\s*\((\d+)\): (.+?)\s*$/);
		if (match)
		{
			userlevel = match[1];
			msg = match[2];
		}
		else
		{
			userlevel = window.getGlobalLevel();
		}
	}
	const data = {
		timestamp: now()
		, username: username
		, userlevel: userlevel
		, icon: icon
		, tag: tag
		, type: type
		, msg: msg
	};
	return data;
}
function add2ChatHistory(data)
{
	chatHistory.push(data);
	chatHistory = chatHistory.slice(-maxChatHistoryLength);
	store.persist(chatHistoryKey, chatHistory);
}
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;
}
const chatBoxId = 'div-chat';
const generalChatId = 'div-chat-area';
const pmChatPrefix = 'div-chat-pm-';
const msgInputId = 'chat-input-text';
function getChatDiv(username)
{
	const id = username == '' ? generalChatId : pmChatPrefix + username;
	let div = document.getElementById(id);
	if (!div)
	{
		div = document.createElement('div');
		div.setAttribute('disabled', 'disabled');
		div.id = id;
		div.className = 'div-chat-area';

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

		const generalChat = document.getElementById(generalChatId);
		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(msgInputId).placeholder = newTextPlaceholder;

	if (window.isAutoScrolling)
	{
		setTimeout(() => newChatDiv.scrollTop = newChatDiv.scrollHeight);
	}
}
const chatIcons = [
	null
	, { key: 'halloween2015',	title: 'Halloween 2015' }
	, { key: 'christmas2015',	title: 'Chirstmas 2015' }
	, { key: 'easter2016',		title: 'Holiday' }
	, { key: 'halloween2016',	title: 'Halloween 2016' }
	, { key: 'christmas2016',	title: 'Chirstmas 2016' }
	, { key: 'dh1Max',			title: 'Max Level in DH1' }
	, { key: 'hardcore',		title: 'Hardcore Account' }
	, { key: 'quest',			title: 'Questmaster' }
];
const chatTags = [
	null
	, { key: 'donor', name: '' }
	, { key: 'contributor', name: 'Contributor' }
	, { key: 'mod', name: 'Moderator' }
	, { key: 'dev', name: 'Dev' }
	, { key: 'yell', name: 'Server Message' }
];
function isPM(data)
{
	return data.type == TYPE_PM_TO || data.type == TYPE_PM_FROM;
}
const locale = 'en-US';
const localeOptions = {
	hour12: false
	, year: 'numeric'
	, month: 'long'
	, day: 'numeric'
	, hour: '2-digit'
	, minute: '2-digit'
	, second: '2-digit'
};
function add2Chat(data)
{
	// username is 3-12 characters long
	let chatbox = getChatDiv('');

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

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

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

	const msgTitle = data.type == TYPE_RELOAD ? 'Chat loaded on ' + d.toLocaleString(locale, localeOptions) : '';
	let levelAppendix = data.type == TYPE_NORMAL ? ' (' + data.userlevel + ')' : '';
	let chatSegment = `<span class="chat-msg" data-type="${data.type}" data-tag="${tag.key}">`
		+ `<span
			class="timestamp"
			title="${d.toLocaleString(locale, localeOptions)}"
			data-same-time="${isSameTime}">${hour}:${minute}</span>`
		+ `<span class="user" data-name="${msgUsername}" data-same-user="${isSameUser}">`
			+ `<span class="icon ${icon.key}" title="${icon.title}"></span>`
			+ `<span
				class="name chat-tag-${tag.key}"
				title="${tag.name}">${msgUsername}${levelAppendix}:</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.lastPMUser = data.username;
		chatbox = getChatDiv(data.username);
	}

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

	handleScrolling(chatbox);
}
function applyChatStyle()
{
	addStyle(`
span.chat-msg
{
	display: flex;
	margin-bottom: 1px;
}
span.chat-msg:nth-child(2n)
{
	background-color: hsla(0, 0%, 90%, 1);
}
.chat-msg[data-type="${TYPE_RELOAD}"]
{
	font-size: 0.8rem;
}
.chat-msg .timestamp
{
	display: none;
}
.chat-msg:not([data-type="${TYPE_RELOAD}"]) .timestamp
{
	color: hsla(0, 0%, 50%, 1);
	display: inline-block;
	font-size: .9rem;
	margin: 0;
	margin-right: 5px;
	width: 2.5rem;
}
.chat-msg .timestamp[data-same-time="true"]
{
	opacity: .1;
}

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

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

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

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

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

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

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

	const chatArea = document.getElementById(generalChatId);
	function setAutoScrolling(value, full)
	{
		if (window.isAutoScrolling != value)
		{
			toggleCheckbox.checked = value;
			window.isAutoScrolling = value;
			window.scrollText(
				'none'
				, value ? 'lime' : 'red'
				, (value ? 'En' : 'Dis') + 'abled' + (full ? ' Autoscroll' : '')
			);
			return true;
		}
		return false;
	}
	toggleCheckbox.addEventListener('change', function ()
	{
		setAutoScrolling(this.checked);
		if (this.checked && intScrollCheckbox.checked)
		{
			chatArea.scrollTop = chatArea.scrollHeight - chatArea.clientHeight;
		}
	});

	// does not consider pm tabs; may be changed in a future version?
	chatArea.addEventListener('scroll', () =>
	{
		if (intScrollCheckbox.checked)
		{
			const scrolled2Bottom = (chatArea.scrollTop + chatArea.clientHeight) >= chatArea.scrollHeight;
			setAutoScrolling(scrolled2Bottom, true);
		}
	});
}
function clickTab(newTab)
{
	const oldTab = document.querySelector('#chat-tabs .chat-tab.selected');
	if (newTab == oldTab)
	{
		return;
	}
	oldTab.classList.remove('selected');
	newTab.classList.add('selected');
	newTab.dataset.new = 0;

	changeChatTab(oldTab, newTab);
}
function addChatTabs()
{
	const chatBoxArea = document.getElementById(chatBoxId);
	const chatTabs = document.createElement('div');
	chatTabs.id = 'chat-tabs';
	chatTabs.addEventListener('click', (event) =>
	{
		const newTab = event.target;
		if (!newTab.classList.contains('chat-tab') || newTab.classList.contains('filler'))
		{
			return;
		}

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

	window.addToChatBox = newAddToChatBox;

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

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

		const userTab = getChatTab(username);
		clickTab(userTab);
		document.getElementById(msgInputId).focus();
	});
}
function fixChat()
{
	newChat();
	addIntelligentScrolling();

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

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

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

	// load history
	chatHistory = store.get(chatHistoryKey) || chatHistory;
	// find index of last message which is not a pm
	const lastNotPM = chatHistory.slice(0).reverse().find((d) =>
	{
		return !isPM(d);
	});
	// insert a placeholder for a reloaded chat
	if (lastNotPM && lastNotPM.type != TYPE_RELOAD)
	{
		reloadedChatData.timestamp = (new Date()).getTime();
		chatHistory.push(reloadedChatData);
	}
	chatHistory.forEach(d => add2Chat(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;
	}
}



/**
 * hopefully only temporary fixes
 */

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

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

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

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



/**
 * 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 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();
	addRecipeTooltips();

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

			window.refreshItemValues(itemArray, false);

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



/**
 * fix web socket errors
 */

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

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

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

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

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

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

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