yet another charsheet mod (view buffed dps unbuffed or buffs get counted twice)
// ==UserScript==
// @name HerdosUI Character
// @namespace https://hordes.io/
// @version 2026-03-13
// @description yet another charsheet mod (view buffed dps unbuffed or buffs get counted twice)
// @author Anwohner
// @match *://hordes.io/play*
// @icon https://hordes.io/data/ui/favicon32.png
// @run-at document-end
// ==/UserScript==
'use strict'
const state = {}
const observer = new MutationObserver((records) => {
for (const record of records) {
for (const node of record.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue
if (node.classList.contains('layout')) return reload()
if (node.classList.contains('window-pos')) onWindow(node)
}
if (record.type === 'characterData') {
const parent = record.target.parentNode
if (parent.classList.contains('statnumber')) {
if (parent.parentNode.id != 'mystatcol') {
return updateStats(parent.parentNode.parentNode)
}
}
}
}
})
const init = () => {
console.log('HerdosUI Character')
observer.observe(document.body, { childList: true })
}
window.addEventListener('load', init)
const reload = () => {
const container = document.querySelector(".container > .container.uiscaled").parentNode
container.querySelectorAll('.window-pos').forEach(node => onWindow(node))
observer.observe(container, { childList: true })
}
const onWindow = (node) => {
const grid = node.querySelector('.stats2')
if (!grid) return
if (!state.pClass) {
const icon = node.querySelector('div.slot > div:nth-child(1) > div:nth-child(1) img.texticon[src]')
state.pClass = parseInt(icon.src.split('/').pop())
}
updateStats(grid)
observer.observe(grid, { childList: true, subtree: true, characterData: true })
}
const updateStats = (node) => {
if (!node) return
if (node.querySelectorAll('.statcol > .textgreen').length) return // info from statpoint + mouseover
const stats = {
hp: parseInt(node.querySelector('div:nth-child(1) > span:nth-child(2)').textContent),
def: parseInt(node.querySelector('div:nth-child(1) > span:nth-child(10)').textContent),
blk: parseFloat(node.querySelector('div:nth-child(1) > span:nth-child(12)').textContent) / 100,
min: parseInt(node.querySelector('div:nth-child(2) > span:nth-child(2)').textContent),
max: parseInt(node.querySelector('div:nth-child(2) > span:nth-child(4)').textContent),
crit: parseFloat(node.querySelector('div:nth-child(2) > span:nth-child(8)').textContent) / 100,
haste: parseFloat(node.querySelector('div:nth-child(2) > span:nth-child(10)').textContent) / 100,
find: parseInt(node.querySelector('div:nth-child(3) > span:nth-child(6)').textContent) / 100,
}
const avg = (stats.min + stats.max) / 2
const hit = avg * (1 + stats.crit)
const dps = hit * (1 + stats.haste)
const gph = dps * (1 + stats.find) / 20
const red = 1 - Math.exp(.0022 * -stats.def)
const ehp = stats.hp / ((1 - red * .87) * (1 - stats.blk * (state.pClass == 0 ? .6 : .45)))
let bps = (avg + 32) * (1.12 + stats.crit) * (1.52 + stats.haste)
if (state.pClass == 0) bps *= 1.6
if (state.pClass == 1) bps *= 1.45
if (state.pClass == 2) bps = (avg + 32) * (1.12 + stats.crit) * (1.52 + stats.haste + .38) * 1.42
if (state.pClass == 3) bps = (avg + 32) * (1.12 + stats.crit) * (2.52 + stats.haste + 0.55)
const mystats = {
'Dmg. Red.': (red * 100).toFixed(1) + '%',
'Eff. HP': ehp.toFixed(0),
'Burst': hit.toFixed(0),
'DPS': dps.toFixed(0),
'DPS buffed': bps.toFixed(0),
'GPH approx': gph.toFixed(0),
}
let col = node.querySelector('#mystatcol')
if (!col) {
col = node.querySelector('div:nth-child(1)').cloneNode(true)
col.id = 'mystatcol'
node.appendChild(col)
node.classList.remove('three')
node.classList.add('four')
}
col.innerHTML = ''
for (const [key, value] of Object.entries(mystats)) {
col.insertAdjacentHTML('beforeend', `<span>${key}</span><span class="statnumber textprimary">${value}</span>`)
}
}