Building Health Bars

Shows the health of buildings.

// ==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()
        })
    })
})()