// ==UserScript==
// @name DH2 Fixed
// @namespace FileFace
// @description Improve Diamond Hunt 2
// @version 0.20.2
// @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)
{
const oldValue = window[key];
window[key] = newValue;
if (oldValue !== newValue)
{
(observedKeys.get(key) || []).forEach(fn => fn(key, oldValue, newValue));
}
}
/**
* global constants
*/
const tierLevels = ['empty', 'sapphire', 'emerald', 'ruby', 'diamond'];
const furnaceLevels = ['stone', 'bronze', 'iron', 'silver', 'gold'];
const furnaceCapacity = [10, 30, 75, 150, 300];
const ovenLevels = ['bronze', 'iron', 'silver', 'gold'];
const maxOilStorageLevel = 3; // 7
const oilStorageSize = [10e3, 50e3, 100e3];
/**
* 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');
}
/**
* hide crafting recipes of lower tiers or of maxed machines
*/
function hideTierRecipes(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);
}
const recipeRow = document.getElementById('crafting-' + key);
if (recipeRow)
{
const hide = level <= maxLevel;
recipeRow.style.display = hide ? 'none' : '';
}
}
if (init)
{
observe(keys2Observe, () => hideTierRecipes(max, getKey, 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);
const recipeRow = document.getElementById('crafting-' + key);
if (recipeRow)
{
const hide = (bound + unbound) >= maxValue;
recipeRow.style.display = hide ? 'none' : '';
}
if (init)
{
observe([key, boundKey], () => hideRecipe(key, max, false));
}
}
function hideCraftedRecipes()
{
function processRecipes(init)
{
// furnace
hideTierRecipes(
furnaceLevels.length
, i => furnaceLevels[i] + 'Furnace'
, init
);
// oil storage
hideTierRecipes(
7
, i => 'oilStorage' + (i+1)
, init
);
// oven recipes
hideTierRecipes(
ovenLevels.length
, i => ovenLevels[i] + 'Oven'
, init
);
// drills
hideRecipe('drills', 10, init);
// crushers
hideRecipe('crushers', 10, init);
// oil pipe
hideRecipe('oilPipe', 1, init);
// row boat
hideRecipe('rowBoat', 1, init);
}
processRecipes(true);
const oldProcessCraftingTab = window.processCraftingTab;
window.processCraftingTab = () =>
{
const reinit = !!window.refreshLoadCraftingTable;
oldProcessCraftingTab();
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);
const tierItemList = ['pickaxe', 'shovel', 'hammer', 'axe', 'rake', 'fishingRod'];
for (let tierItem of tierItemList)
{
for (let i = 0; i < tierLevels.length; i++)
{
const key = getTierKey(tierItem, i);
const tierSpan = addSpan2ItemBox(tierItem == 'rake' ? key : getBoundKey(key));
tierSpan.className = 'tier';
tierSpan.textContent = 'Tier: ' + 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 oldAddToChatBox = 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 href="$1">$1</a>');
oldAddToChatBox(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 oldProcessCraftingTab = window.processCraftingTab;
window.processCraftingTab = () =>
{
const reinit = !!window.refreshLoadCraftingTable;
oldProcessCraftingTab();
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 magic tab
const els = document.querySelectorAll('img[src="images/essence"], img[src="images/stardust"]');
for (let i = 0; i < els.length; i++)
{
els[i].src = els[i].src + '.png';
}
const oldAddRecipeToMagicTable = window.addRecipeToMagicTable;
window.addRecipeToMagicTable = (magicRecipe) =>
{
console.log('addRecipeToMagicTable');
const itemName = magicRecipe.itemName;
let htmlCode = `<tr id="magic-${itemName}">`
+ `<td>${getItemName(itemName)}</td>`
+ `<td><img src="images/hero/spells/${itemName}.png" style="margin-top:3px;border:1px solid black;" class="image-icon-40" /></td>`
+ `<td id="magic-recipe-level-req-${itemName}">${magicRecipe.levelReq}</td>`
;
//mats
htmlCode += `<td>`;
const recipeParts = magicRecipe.recipe;
for (let i = 0; i < recipeParts.length; i++)
{
// important part here: add ".png" to image src after recipe part
htmlCode += `<img src="images/${recipeParts[i]}.png" class="image-icon-30" /> `
+ `<span id="magic-recipe-cost-${itemName}-${i}">${magicRecipe.recipeCost[i]}</span>`
+ `<br />`
;
}
//special cases
switch (itemName)
{
}
//spanItemAmount.setAttribute("data-item-display", itemName);
htmlCode += `</td>`;
htmlCode += `<td>${magicRecipe.description}</td>`
+ `<td>" + magicRecipe.xp + "</td>`
+ `<td id='magic-charges-" + itemName + "'>0</td>`
;
window.$('#table-magic-recipe tr:last').after(htmlCode);
window.$('#table-magic-recipe tr:last').click(() =>
{
window.sendBytes("SPELL_CHARGE=" + itemName);
});
}
}
/**
* improve timer
*/
function improveTimer()
{
window.formatTime = (seconds) =>
{
return formatTimer(seconds);
};
window.formatTimeShort2 = (seconds) =>
{
return formatTimer(seconds);
};
const barInfo = {
1: {
name: 'bronze'
, timePerBar: 1
}
, 2: {
name: 'iron'
, timePerBar: 5
}
, 3: {
name: 'silver'
, timePerBar: 10
}
, 4: {
name: 'gold'
, timePerBar: 30
}
, 5: {
name: 'glass'
, timePerBar: 1
}
};
const smeltingPercEl = document.querySelector('span[data-item-display="smeltingPerc"]');
const smeltingTimerEl = document.createElement('span');
smeltingPercEl.style.display = 'none';
smeltingPercEl.parentNode.lastElementChild.style.display = 'none';
smeltingPercEl.parentNode.appendChild(smeltingTimerEl);
let interval = null;
let barName = '';
let remainingTime = 0;
function setRemainingTime()
{
smeltingTimerEl.textContent = formatTimer(Math.max(remainingTime, 0));
}
function updateSmeltingPerc()
{
const info = barInfo[window.smeltingBarType];
if (info)
{
const totalTime = info.timePerBar * window.smeltingTotalAmount;
const time = Math.round(totalTime * (1 - window.smeltingPerc / 100));
if (interval == null || barName != info.name)
{
barName = info.name;
remainingTime = time;
interval = window.setInterval(() =>
{
remainingTime--;
setRemainingTime();
}, 1000);
setRemainingTime();
}
// tolerate up to 2 seconds deviation (to make it smoother for the user)
else if (Math.abs(remainingTime - time) > 2)
{
remainingTime = time;
setRemainingTime();
}
}
else if (interval)
{
window.clearInterval(interval);
barName = '';
interval = null;
}
}
observe('smeltingPerc', () => updateSmeltingPerc());
updateSmeltingPerc();
}
/**
* init
*/
function init()
{
temporaryFixes();
hideCraftedRecipes();
improveItemBoxes();
fixWoodcutting();
fixChat();
improveTimer();
}
document.addEventListener('DOMContentLoaded', () =>
{
const oldDoCommand = 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('~');
itemArray.push(key);
updateValue(key, newValue);
}
window.refreshItemValues(itemArray, false);
if (window.firstLoadGame)
{
window.loadInitial();
window.firstLoadGame = false;
init();
}
else
{
window.clientGameLoop();
}
return;
}
return oldDoCommand(data);
};
});
/**
* fix web socket errors
*/
function webSocketLoaded(event)
{
if (window.webSocket == null)
{
console.error('no webSocket instance found!');
return;
}
const messageQueue = [];
const oldOnMessage = webSocket.onmessage;
webSocket.onmessage = (event) => messageQueue.push(event);
document.addEventListener('DOMContentLoaded', () =>
{
messageQueue.forEach(event => onMessage(event));
webSocket.onmessage = oldOnMessage;
});
const commandQueue = [];
const oldSendBytes = window.sendBytes;
window.sendBytes = (command) => commandQueue.push(command);
const oldOnOpen = webSocket.onopen;
webSocket.onopen = (event) =>
{
window.sendBytes = oldSendBytes;
commandQueue.forEach(command => window.sendBytes(command));
return oldOnOpen(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();
})();