// ==UserScript==
// @name Building Health Bars
// @namespace https://github.com/Nudo-o
// @version 1
// @description Shows the health of buildings.
// @author @nudoo
// @match *://moomoo.io/*
// @match *://*.moomoo.io/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=moomoo.io
// @require https://update.greasyfork.org/scripts/480301/1283571/CowJS.js
// @require https://update.greasyfork.org/scripts/480303/1282926/MooUI.js
// @license MIT
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
"use strict"
const { Cow, CowUtils, MooUI } = window
let settings = {
"health-bars": true,
"circle-bars": false,
"in-look-dir": false,
"in-weapon-range": false,
"weapon-range-mult": "1",
"bars-color": "#933db8",
"hit-counter": false
}
const settingsMap = Object.entries(settings)
const storageName = "building-health-settings"
function setVisualSetting(key, value) {
settings[key] = value
localStorage.setItem(storageName, JSON.stringify(settings))
}
for (let i = 0; i < settingsMap.length; i++) {
const visualSettings = JSON.parse(localStorage.getItem(storageName) || null)
if (!visualSettings) {
localStorage.setItem(storageName, JSON.stringify(settings))
break
}
if (!visualSettings.hasOwnProperty(settingsMap[i][0])) {
setVisualSetting(settingsMap[i][0], settingsMap[i][1])
}
}
settings = JSON.parse(localStorage.getItem(storageName))
const columnsSettings = {
settings: {
targetColumn: "settings",
togglers: [{
key: "health-bars",
name: "Health bars",
description: "Shows the health of buildings.",
isActive: settings["health-bars"],
options: [
new MooUI.OptionCheckbox({
key: "circle-bars",
name: "Circle bars",
description: "If enabled, the bars will be displayed as circles",
isActive: settings["circle-bars"]
}),
new MooUI.OptionCheckbox({
key: "in-look-dir",
name: "In look dir",
description: "Bars will be drawn only when you look in their direction.",
isActive: settings["in-look-dir"]
}),
new MooUI.OptionCheckbox({
key: "in-weapon-range",
name: "In weapon range",
description: "Bars will only be drawn when your weapon can reach them.",
isActive: settings["in-weapon-range"]
}),
new window.MooUI.OptionIRange({
key: "weapon-range-mult",
name: "Weapon range mult",
description: "Adds the distance to the range of the weapon so that the drawing of the bars is further than the distance of the weapon.",
min: 1,
max: 3,
step: "any",
fixValue: 1,
value: settings["weapon-range-mult"]
}),
new window.MooUI.OptionIColor({
key: "bars-color",
name: "Color",
description: "Color of bars",
value: settings["bars-color"]
})
]
}, {
key: "hit-counter",
name: "Hit counter",
description: "Shows how many hits you need to hit the building.",
isActive: settings["hit-counter"]
}]
}
}
class MenuBuilder {
constructor() {
this.menu = void 0
this.settings = new MooUI.Column()
}
buildTogglers() {
for (const columnSettings of Object.values(columnsSettings)) {
const column = this[columnSettings.targetColumn]
for (const toggler of columnSettings.togglers) {
column.add(new MooUI.Checkbox(toggler))
}
}
}
build() {
this.menu = MooUI.createMenu({
toggleKey: {
code: "Escape"
},
appendNode: document.getElementById("gameUI")
})
document.head.insertAdjacentHTML("beforeend", `<style>
.column-container {
border-radius: 0 0 6px 6px !important;
}
.ui-model {
border-radius: 4px !important;
}
.ui-model.show-options {
border-radius: 4px 4px 0px 0px !important;
}
.options-container {
border-radius: 0px 0px 4px 4px !important;
}
.ui-option-input-color {
border-radius: 4px !important;
}
</style>`)
this.settings.setHeaderText("Settings")
this.settings.collisionWidth = -999999
this.buildTogglers()
this.menu.add(this.settings)
this.menu.onModelsAction(setVisualSetting)
this.menu.columns.forEach((column) => {
column.header.element.style.borderRadius = "6px"
column.header.element.addEventListener("mousedown", (event) => {
if (event.button !== 2) return
column.header.isOpen ??= false
column.header.isOpen = !column.header.isOpen
column.header.element.style.borderRadius = column.header.isOpen ? "6px 6px 0 0" : "6px"
})
})
}
}
const menuBuilder = new MenuBuilder()
let menu = void 0
let lastWeaponRangeMultChange = null
window.addEventListener("DOMContentLoaded", () => {
menuBuilder.build()
menu = menuBuilder.menu
menu.getModel("weapon-range-mult").on("input", () => {
lastWeaponRangeMultChange = Date.now()
})
})
function drawCircleBar(color, width, scale, endAngle) {
const { context } = Cow.renderer
context.strokeStyle = color
context.lineWidth = width
context.lineCap = "round"
context.beginPath()
context.arc(0, 0, scale, 0, endAngle)
context.stroke()
context.closePath()
}
Cow.addRender("building-health-bars", () => {
if (!Cow.player) return
const { context } = Cow.renderer
const weaponRange = (Cow.player.weapon.range + Cow.player.scale / 2) * parseFloat(menu.getModelValue("weapon-range-mult"))
if ((Date.now() - lastWeaponRangeMultChange) <= 1500) {
const color = menu.getModelValue("bars-color")
context.save()
context.fillStyle = color
context.strokeStyle = color
context.globalAlpha = .3
context.lineWidth = 4
context.translate(Cow.player.renderX, Cow.player.renderY)
context.beginPath()
context.arc(0, 0, weaponRange, 0, Math.PI * 2)
context.fill()
context.globalAlpha = .7
context.stroke()
context.closePath()
context.restore()
} else {
lastWeaponRangeMultChange = null
}
Cow.objectsManager.eachVisible((object) => {
if (!object.isItem) return
const distance = CowUtils.getDistance(Cow.player, object) - object.scale
const angle = CowUtils.getDirection(object, Cow.player)
if (menu.getModelActive("in-weapon-range") && distance > weaponRange) return
if (menu.getModelActive("in-look-dir") && CowUtils.getAngleDist(angle, Cow.player.lookAngle) > Cow.config.gatherAngle) return
if (menu.getModelActive("hit-counter")) {
const damage = Cow.player.weapon.dmg * Cow.items.variants[Cow.player.weaponVariant].val
const damageAmount = damage * (Cow.player.weapon.sDmg || 1) * (Cow.player.skin?.id === 40 ? 3.3 : 1)
const hits = Math.ceil(object.health / damageAmount)
const offsetY = menu.getModelActive("circle-bars") ? 2 : 22
context.save()
context.font = `18px Hammersmith One`
context.fillStyle = "#fff"
context.textBaseline = "middle"
context.textAlign = "center"
context.lineWidth = 8
context.lineJoin = "round"
context.translate(object.renderX, object.renderY)
context.strokeText(hits, 0, offsetY)
context.fillText(hits, 0, offsetY)
context.restore()
}
if (!menu.getModelActive("health-bars")) return
if (menu.getModelActive("circle-bars")) {
const endAngle = ((object.health / object.maxHealth) * 360) * (Math.PI / 180)
const width = 14
const scale = 22
context.save()
context.translate(object.renderX, object.renderY)
context.rotate(object.dir ?? object.dir2)
drawCircleBar("#3d3f42", width, scale, endAngle)
drawCircleBar(menu.getModelValue("bars-color"), width / 2.5, scale, endAngle)
context.restore()
return
}
const { healthBarWidth, healthBarPad } = window.config
const width = healthBarWidth / 2 - healthBarPad / 2
const height = 17
const radius = 8
context.save()
context.translate(object.renderX, object.renderY)
context.fillStyle = "#3d3f42"
context.roundRect(-width - healthBarPad, -height / 2, 2 * width + 2 * healthBarPad, height, radius)
context.fill()
context.fillStyle = menu.getModelValue("bars-color")
context.roundRect(-width, -height / 2 + healthBarPad, 2 * width * (object.health / object.maxHealth), height - 2 * healthBarPad, radius - 1)
context.fill()
context.restore()
})
})
})()