// ==UserScript==
// @name DH2 Fixed
// @namespace FileFace
// @description Improve Diamond Hunt 2
// @version 0.29.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 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');
}
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 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);
hideNumberInItemBox('planter', 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 target="_blank" 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 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
const oldClientGameLoop = window.clientGameLoop;
window.clientGameLoop = () =>
{
oldClientGameLoop();
if (document.getElementById('tab-container-combat').style.display != 'none')
{
combatNotFightingTick();
}
};
// fix scrollText (e.g. when joining the game and receiving xp at that moment)
window.mouseX = window.innerWidth / 2;
window.mouseY = window.innerHeight / 2;
}
/**
* 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 stopTimerTick()
{
interval && window.clearInterval(interval);
barName = '';
interval = null;
}
function setRemainingTime(time)
{
remainingTime = time;
if (remainingTime < 0)
{
stopTimerTick();
}
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)
{
stopTimerTick();
barName = info.name;
interval = window.setInterval(() => setRemainingTime(remainingTime-1), 1000);
setRemainingTime(time);
}
// tolerate up to 2 seconds deviation (to make it smoother for the user)
else if (Math.abs(remainingTime - time) > 2)
{
setRemainingTime(time);
}
}
else if (interval)
{
stopTimerTick();
}
}
observe('smeltingPerc', () => updateSmeltingPerc());
observe('smeltingTotalAmount', () => updateSmeltingPerc());
updateSmeltingPerc();
// 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' : 'Nothing';
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 oldSelectBar = 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);
}
if (amountInput.value > maxAmount)
{
smeltingValue = amountInput.value;
amountInput.value = maxAmount;
}
else if (smeltingValue != null)
{
amountInput.value = Math.min(smeltingValue, maxAmount);
if (smeltingValue <= maxAmount)
{
smeltingValue = null;
}
}
return oldSelectBar(bar, inputElement, inputBarsAmountEl, capacity);
};
const oldOpenFurnaceDialogue = window.openFurnaceDialogue;
window.openFurnaceDialogue = (furnace) =>
{
if (smeltingBarType == 0)
{
amountInput.max = getFurnaceCapacity(furnace);
}
return oldOpenFurnaceDialogue(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 oldClicksShovel = window.clicksShovel;
window.clicksShovel = () =>
{
oldClicksShovel();
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 oldClicksFishingRod = window.clicksFishingRod;
window.clicksFishingRod = () =>
{
oldClicksFishingRod();
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);
}
};
}
/**
* init
*/
function init()
{
temporaryFixes();
hideCraftedRecipes();
improveItemBoxes();
fixWoodcutting();
fixChat();
improveTimer();
improveSmelting();
chance2TimeCalculator();
}
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('~');
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 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();
})();