您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The best info Script!
- // ==UserScript==
- // @name Better Player Info 2
- // @namespace http://tampermonkey.net/
- // @version 2.05
- // @description The best info Script!
- // @author Dikinx(Diamondkingx)
- // @match https://zombs.io/*
- // @match http://zombs.io/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=vscode.dev
- // @grant none
- // ==/UserScript==
- 'use strict'
- // Helper Functions----->
- const log = console.log
- function element(selector) {
- return document.querySelector(selector)
- }
- // Simplifies creation of new elements
- function createElement(type, attributes = {}, properties = {}, creationOptions = {}) {
- const element = document.createElement(type, creationOptions)
- // Add all the attributes
- for (let attribute in attributes)
- element.setAttribute(attribute, attributes[attribute])
- // Add all the js properties
- for (let [property, value] of Object.entries(properties))
- element[property] = value
- element.appendTo = function (parent) {
- let parentElement
- if (typeof parent == "string")
- parentElement = element(parent)
- else if (parent instanceof HTMLElement)
- parentElement = parent
- else throw new TypeError("Unknown parent type.")
- if (!parentElement) throw new ReferenceError("Undefined Parent.")
- parentElement.append(this)
- return this
- }
- return element
- }
- // Elements created for the script to use
- function defineScriptElement(name, type, attributes = {}, properties = {}, creationOptions = {}) {
- // Create the element and define it in the scriptElements object
- return main.scriptElements[name] = createElement(type, attributes, { name, ...properties }, creationOptions)
- }
- // A function that only fires once after a transition ends on an element
- HTMLElement.prototype.onceontransitionend = function (callback) {
- if (typeof callback !== "function") throw new TypeError("'callback' must be a function.")
- const transitionEndHandler = () => {
- callback.bind(this)()
- this.removeEventListener("transitionend", transitionEndHandler)
- }
- this.addEventListener("transitionend", transitionEndHandler)
- }
- Math.lerp = function (a, b, c) {
- return a + (b - a) * c
- }
- Math.lerpAngles = function (a1, a2, c, returnUnitVec = false) {
- let x2 = Math.lerp(Math.cos(a1), Math.cos(a2), c),
- y2 = Math.lerp(Math.sin(a1), Math.sin(a2), c),
- mag
- if (returnUnitVec) {
- mag = Math.sqrt(x2 ** 2 + y2 ** 2)
- return { x: x2 / mag, y: y2 / mag, angle: Math.atan2(x2, y2) }
- }
- return Math.atan2(y2, x2)
- }
- // function toIndianNumberSystem(string = '0') {
- // if (string.length <= 3) return string;
- // const firstPartLength = (string.length - 3) % 2 === 0 ? 2 : 1
- // const firstPart = string.slice(0, firstPartLength)
- // const rest = string.slice(firstPartLength, -3)
- // const lastThree = string.slice(-3)
- // const formattedRest = rest.match(/.{2}/g)?.join(',') || ''
- // return firstPart + (formattedRest ? ',' + formattedRest : '') + ',' + lastThree
- // }
- function toInternationalNumberSystem(number = '0') {
- if (typeof number == "string") return number
- if (number.length <= 3) return number
- let rest = number.slice(0, number.length % 3)
- let groups = number.slice(rest.length).match(/.{3}/g) || []
- return (rest ? rest + ',' : '') + groups.join(',')
- }
- function toLargeUnits(number = 0) {
- if (typeof number == "string") return number
- if (number < 1_000) return number.toString()
- const units = ['k', 'mil', 'bil', 'tri']
- const unitIndex = Math.floor(Math.log10(number) / 3)
- if (unitIndex >= units.length) return number.toLocaleString()
- const scaledNumber = number / Math.pow(10, unitIndex * 3)
- return `${scaledNumber.toFixed(2) / 1}${units[unitIndex - 1]}`
- }
- // Main css----->
- const css = `
- :root {
- --background-dark: rgb(0 0 0 / .6);
- --background-light: rgb(0 0 0 / .4);
- --background-verylight: rgb(0 0 0 / .2);
- --background-purple: rgb(132 115 212 / .9);
- --background-yellow: rgb(214 171 53 / .9);
- --background-green: rgb(118 189 47 / .9);
- --background-healthgreen: rgb(100 161 10);
- --background-orange: rgb(214 120 32 / .9);
- --background-red: rgb(203 87 91 / .9);
- --text-light: rgb(255 255 255 / .6);
- --text-verylight: #eee;
- }
- #mainMenuWrapper {
- display: grid;
- grid-template-rows: repeat(2, auto);
- grid-template-columns: repeat(3, auto);
- width: max-content;
- height: max-content;
- position: absolute;
- padding: .625rem;
- scale: 0;
- opacity: 0;
- inset: 0;
- z-index: 11;
- border-radius: .25rem;
- pointer-events: none;
- transition: opacity .35s ease-in-out, scale .55s ease-in-out;
- }
- #mainMenuWrapper.open {
- scale: 1;
- opacity: 1;
- }
- #mainMenuWrapper.moveTransition {
- transition: all .35s ease-in-out, opacity .35s ease-in-out, scale .55s ease-in-out;
- }
- #mainMenuWrapper.pinned {
- z-index: 16;
- }
- #mainMenuWrapper #mainMenuTagsContainer {
- display: grid;
- grid-template-columns: auto auto;
- align-items: center;
- justify-items: center;
- gap: .25rem;
- width: max-content;
- height: max-content;
- padding: inherit;
- position: relative;
- grid-area: 1 / 2;
- inset: 0;
- margin-bottom: .5rem;
- background: var(--background-verylight);
- border-radius: inherit;
- transition: all .35s ease-in-out;
- }
- #mainMenuWrapper :is(#mainMenuTagsContainer[data-tagscount="1"], #mainMenuTagsContainer:empty) {
- gap: 0;
- }
- #mainMenuWrapper #mainMenuTagsContainer:empty {
- padding: 0;
- margin-bottom: 0;
- background: transparent;
- transition-delay: 2s;
- }
- #mainMenuWrapper .tag {
- width: max-content;
- height: max-content;
- padding: 0rem 0rem;
- position: relative;
- opacity: 0;
- font-family: 'Hammersmith One', sans-serif;
- font-size: 0px;
- color: var(--text-verylight);
- background: var(--background-verylight);
- border-radius: inherit;
- transition: all .55s cubic-bezier(.65, .05, .19, 1.02), opacity .65s cubic-bezier(.65, .05, .19, 1.02);
- }
- #mainMenuWrapper .tag.neutral {
- color: color-mix(in srgb, var(--background-green) 10%, #eee);
- background: var(--background-green);
- }
- #mainMenuWrapper .tag.warning {
- color: color-mix(in srgb, var(--background-yellow) 10%, #eee);
- background: var(--background-yellow);
- }
- #mainMenuWrapper .tag.error {
- margin: 0;
- color: color-mix(in srgb, var(--background-red) 10%, #eee);
- background: var(--background-red);
- }
- #mainMenuWrapper .tag.active {
- padding: .2rem .4rem;
- opacity: 1;
- font-size: 18px;
- }
- #mainMenuWrapper #mainMenuFeatureContainer {
- display: flex;
- flex-direction: column;
- gap: .25rem;
- height: max-content;
- position: relative;
- grid-area: 2 / 1;
- margin-right: .5rem;
- border-radius: inherit;
- pointer-events: all;
- }
- #mainMenuWrapper #mainMenuFeatureContainer .featureButton {
- width: 2.25rem;
- height: 2.25rem;
- padding: .625rem;
- scale: 1;
- font-size: 17px;
- color: var(--background-light);
- background: var(--background-verylight);
- outline: none;
- border: none;
- border-radius: inherit;
- cursor: pointer;
- transition: background .45s ease-in-out, all .35s ease-in-out, scale .25s cubic-bezier(0, .16, .79, 1.66);
- }
- #mainMenuWrapper #mainMenuFeatureContainer button:hover {
- background: var(--background-light);
- }
- #mainMenuWrapper #mainMenuFeatureContainer button.light {
- scale: .922;
- font-size: 14px;
- color: var(--text-verylight);
- background: var(--background-light);
- }
- #mainMenuWrapper #otherMenuTogglesContainer {
- display: flex;
- flex-direction: column;
- align-self: flex-end;
- gap: 0rem;
- width: max-content;
- height: max-content;
- padding: 0;
- position: relative;
- grid-area: 2 / 3;
- inset: 0 0 0 -350%;
- margin-left: 0rem;
- opacity: 0;
- background: var(--background-verylight);
- border-radius: .25rem;
- pointer-events: none;
- transition: all .35s ease-out, left .4s cubic-bezier(.5, 0, .5, 0), padding .35s ease-in;
- }
- #mainMenuWrapper #otherMenuTogglesContainer.open {
- gap: .25rem;
- left: 0%;
- padding: .625rem;
- margin-left: .5rem;
- opacity: 1;
- pointer-events: all;
- transition: all .35s ease-in, left .4s cubic-bezier(.5, 1, .5, 1), padding .35s ease-out, pointer-events 1ms linear .5s;
- }
- #mainMenuWrapper #otherMenuButton {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- width: 2rem;
- height: 2rem;
- position: relative;
- left: 100%;
- translate: -100% 0;
- z-index: 1;
- font-family: 'Hammersmith One', sans-serif;
- font-size: 15px;
- color: var(--text-light);
- background: var(--background-verylight);
- border: none;
- border-radius: .25rem;
- outline: none;
- transition: color .15s ease-in-out, width .35s ease-in-out, background .35s ease-in-out, transform .35s ease-in-out, translate .45s ease-in-out, left .45s ease-in-out;
- cursor: pointer;
- }
- #mainMenuWrapper .toggleButton {
- width: 0rem;
- height: 0rem;
- padding: 0;
- position: relative;
- opacity: 1;
- font-size: 0;
- color: var(--text-light);
- background: var(--background-verylight);
- border: none;
- border-radius: inherit;
- outline: none;
- transition: all .35s ease-in-out, color .15s ease-in-out;
- cursor: pointer;
- }
- #mainMenuWrapper :is(#otherMenuButton, .toggleButton):is(:hover, .dark):not(.disabled) {
- color: var(--text-verylight);
- background: var(--background-light);
- }
- #mainMenuWrapper #otherMenuButton.rotated {
- transform: rotateY(180deg);
- }
- #mainMenuWrapper #otherMenuButton.moved {
- left: 0%;
- translate: 0%;
- }
- #mainMenuWrapper .toggleButton.dark {
- color: var(--text-verylight);
- background: var(--background-light);
- }
- #mainMenuWrapper .toggleButton.disabled {
- opacity: .65;
- cursor: not-allowed;
- }
- #mainMenuWrapper #otherMenuTogglesContainer.open > .toggleButton {
- width: 2.25rem;
- height: 2.25rem;
- font-size: 15px;
- }
- #mainMenu {
- display: flex;
- flex-direction: column;
- gap: 1rem;
- width: 22.5rem;
- height: 14rem;
- position: relative;
- grid-area: 2 / 2;
- inset: 0;
- background: var(--background-light);
- border-radius: .25rem;
- padding: .625rem;
- pointer-events: all;
- transition: width .35s ease-in-out, height .35s ease-in-out;
- }
- #mainMenu :is(span, p) {
- display: inline-block;
- height: max-content;
- line-height: 100%;
- }
- #mainMenu #mainMenuBody {
- width: 100%;
- height: 100%;
- position: relative;
- border-radius: inherit;
- }
- #mainMenuBody .contentHolder {
- width: 100%;
- height: 100%;
- position: absolute;
- translate: -100% 0;
- z-index: 0;
- opacity: 0;
- border-radius: inherit;
- transition: opacity .45s cubic-bezier(.03, .02, .21, .78), translate .55s cubic-bezier(0, 1, 1, 1);
- pointer-events: none;
- }
- #mainMenuBody .contentHolder.opaque {
- opacity: 1;
- transition: opacity .45s cubic-bezier(.03, .02, .78, .21), translate .55s cubic-bezier(0, 1, 1, 1)
- pointer-events: all;
- }
- #mainMenuBody .contentHolder.moved {
- translate: 0% 0%;
- pointer-events: all;
- }
- #mainMenuBody .contentHolder.moved.opaque {
- z-index: 100;
- }
- #mainMenu #header {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
- #mainMenu #entityName {
- display: block;
- margin: 0;
- color: #eee;
- font-size: 24px;
- }
- #mainMenu #entityUID {
- display: block;
- margin: 0;
- color: var(--text-light);
- font-size: 18px;
- letter-spacing: .1rem;
- }
- #mainMenu #entityHealthBarsContainer {
- display: flex;
- justify-self: end;
- align-items: center;
- justify-content: flex-end;
- gap: .25rem;
- width: 100%;
- height: 100%;
- grid-area: 1 / 2 / 3;
- }
- #mainMenu .entityHealth {
- width: 0rem;
- height: 2.125rem;
- padding: .25rem 0 .25rem 0;
- position: relative;
- opacity: 1;
- background: var(--background-verylight);
- border-radius: .25rem;
- transition: all .35s ease-in-out, opacity .35s ease-in;
- }
- #mainMenu .entityHealth::before {
- content: attr(data-name);
- display: block;
- width: max-content;
- height: max-content;
- position: absolute;
- inset: 50% .5rem;
- translate: 0% -50%;
- opacity: 0;
- font-family: 'Hammersmith One', sans-serif;
- color: var(--text-verylight);
- font-size: 12px;
- text-shadow: 0 0 1px rgb(0 0 0 / .8);
- transition: all .15s ease-in;
- }
- #mainMenu .entityHealth.visible {
- width: min(6.25rem, 100%);
- padding-inline: .25rem;
- opacity: 1;
- }
- #mainMenu .entityHealth.visible::before {
- opacity: 1;
- }
- #mainMenu .entityHealthBar {
- width: 0%;
- height: 100%;
- background: var(--background-healthgreen);
- border-radius: inherit;
- transition: width .35s ease-in-out;
- }
- #mainMenu #body {
- width: 100%;
- height: max-content;
- padding: .625rem;
- border-radius: inherit;
- background: var(--background-verylight);
- }
- #mainMenu #body.infoMenuBody {
- display: grid;
- grid-template-columns: 1fr 1fr;
- align-items: center;
- row-gap: .3rem;
- margin-top: 1rem;
- }
- #mainMenu .entityInfo {
- margin: 0;
- opacity: .33;
- font-family: 'Open Sans', sans-serif;
- font-size: 14px;
- color: var(--text-light);
- transition: all .35s ease-in-out;
- }
- #mainMenu .entityInfo.visible {
- opacity: 1;
- }
- #mainMenu .entityInfo strong {
- color: var(--text-verylight);
- }
- #mainMenu .entityInfo span {
- display: inline-block;
- }
- #mainMenu #body.spectateMenuBody {
- width: 100%;
- height: 400%;
- max-height: 100%;
- overflow: hidden;
- border-radius: inherit;
- }
- #mainMenu .spectateButton {
- width: 100%;
- height: 100%;
- padding-bottom: 0rem;
- border-radius: inherit;
- opacity: 0;
- transition: opacity .35s ease-in-out, height .45s ease-in-out, padding .45s ease-in-out;
- }
- #mainMenu .spectateButton button {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- justify-content: center;
- width: 100%;
- height: 100%;
- padding: .625rem;
- scale: 1;
- font-size: 2rem;
- font-family: 'Hammersmith One';
- color: var(--text-verylight);
- background: var(--background-verylight);
- border: none;
- border-radius: inherit;
- outline: none;
- overflow: hidden;
- pointer-events: inherit;
- cursor: pointer;
- transition: background .35s ease-in-out, scale .35s cubic-bezier(0, .16, .79, 1.66);
- }
- #mainMenu .spectateButton button:hover {
- background: var(--background-light);
- }
- #mainMenu .spectateButton button:active {
- scale: .95;
- }
- #mainMenu #body.spectateMenuBody .spectateButton.visible{
- opacity: 1
- }
- #mainMenu #body.spectateMenuBody[data-playercount="1"] .spectateButton.visible{
- height: 100%;
- }
- #mainMenu #body.spectateMenuBody[data-playercount="2"] .spectateButton.visible{
- height: 50%;
- }
- #mainMenu #body.spectateMenuBody[data-playercount="3"] .spectateButton.visible{
- height: 33.33%;
- }
- #mainMenu .spectateMenuBody .wrapper {
- display: grid;
- grid-template-columns: 1fr;
- grid-template-rows: 1fr auto auto;
- gap: .4rem 0rem;
- width: 100%;
- height: 90%;
- border-radius: inherit;
- transition: all .45s ease-in-out;
- }
- #mainMenu .spectateButton span.name {
- justify-self: flex-start;
- align-self: center;
- max-width: min(18ch, 100%);
- text-overflow: ellipsis;
- overflow: hidden;
- transition: all .35s ease-in-out;
- }
- #mainMenu .spectateButton[data-partyposition="0"] .tag.position {
- background: var(--background-purple);
- color: color-mix(in srgb, var(--background-purple) 10%, #eee);
- }
- #mainMenu .spectateButton[data-partyposition="1"] .tag.position {
- background: var(--background-yellow);
- color: color-mix(in srgb, var(--background-yellow) 10%, #eee)
- }
- #mainMenu .spectateButton[data-partyposition="2"] .tag.position {
- background: var(--background-green);
- color: color-mix(in srgb, var(--background-green) 10%, #eee)
- }
- #mainMenu .spectateButton[data-partyposition="3"] .tag.position {
- background: var(--background-orange);
- color: color-mix(in srgb, var(--background-orange) 10%, #eee)
- }
- #mainMenu :is(#body.spectateMenuBody[data-playercount="2"]) .wrapper {
- grid-template-columns: repeat(2, max-content);
- grid-template-rows: 1fr 1fr;
- gap: 0rem .2rem;
- height: 100%;
- }
- #mainMenu :is(#body.spectateMenuBody[data-playercount="2"], #body.spectateMenuBody[data-playercount="3"]) .wrapper span.name {
- justify-self: flex-start;
- grid-area: 1 / 4 / 1 / 1;
- font-size: 1.25rem;
- }
- #mainMenu :is(#body.spectateMenuBody[data-playercount="2"], #body.spectateMenuBody[data-playercount="3"]) .wrapper .tag {
- font-size: 12px;
- }
- #mainMenu #body.spectateMenuBody[data-playercount="3"] .wrapper {
- grid-template-columns: 1fr repeat(2, max-content);
- grid-template-rows: 1fr;
- gap: 0rem .2rem;
- height: 100%;
- }
- #mainMenu #body.spectateMenuBody[data-playercount="3"] .wrapper span.name{
- grid-area: 1 / 1;
- }
- #entityFollower {
- position: absolute;
- translate: -50% -50%;
- opacity: 0;
- font-size: 22px;
- transition: opacity .35s ease-in-out, font-size .35s ease-in-out;
- }
- #entityFollower.visible {
- opacity: 1;
- }
- #entityFollower i {
- display: block;
- width: max-content;
- height: max-content;
- position: absolute;
- inset: 50%;
- translate: -50% -50%;
- rotate: 45deg;
- color: var(--text-light);
- -webkit-text-stroke: .12em rgb(42 42 42 / .9);
- }
- `
- // Create the element and append it on script's initialization
- const style = createElement("style")
- style.append(document.createTextNode(css))
- //Main Constants----->
- // Main controller constant, and it's functions
- const main = {
- settings: {
- targetableEntityClass: ["PlayerEntity", "Prop", "Npc"],
- mouseCollisionCheckFPS: 24,
- menuUpdateFPS: 20,
- backgroundMenuUpdateFPS: 10,
- activationKey: "control",
- paused: false,
- },
- gameElements: {},
- scriptElements: {},
- cursor: {
- x: innerWidth / 2,
- y: innerHeight / 2
- },
- controls: {
- listenedKeys: ["control"],
- },
- menu: {
- mainMenuName: "infoMenu",
- navigationStack: [],
- features: {},
- pinned: false,
- },
- // This data is not in the game but on the wiki
- gameData: {
- towers: {
- SlowTrap: {
- slowAmount: [40, 45, 50, 55, 60, 65, 70, 70]
- },
- Harvester: {
- attackSpeed: [1500, 1400, 1300, 1200, 1100, 1000, 900, 800]
- },
- MeleeTower: {
- attackSpeed: [400, 333, 284, 250, 250, 250, 250, 250]
- },
- BombTower: {
- attackSpeed: [1000, 1000, 1000, 1000, 1000, 1000, 900, 900]
- },
- MagicTower: {
- attackSpeed: [800, 800, 704, 602, 500, 400, 300, 300]
- }
- },
- pets: {
- PetCARL: {
- speed: [15, 16, 17, 17.5, 17.5, 18.5, 18.5, 19],
- },
- PetMiner: {
- speed: [30, 32, 34, 35, 35, 37, 37, 38],
- resourceGain: [1, 1, 2, 2, 3, 3, 4, 4]
- },
- }
- },
- inGame: false,
- }
- // This proxy allows tracking of value additions or removals from the stack.
- const navigationStackProxyHandler = {
- get(stack, property) {
- const value = stack[property]
- // If the accessed property is a function, wrap it to track stack modifications
- if (typeof value == "function") {
- return function (...args) {
- const returnValue = value.apply(stack, args)
- // Invoke the appropriate callback when a value is added or removed
- if (property == "push") stack.onAddCallback?.()
- else if (property == "pop") stack.onRemoveCallback?.()
- return returnValue
- }
- }
- // Otherwise, return the accessed value
- return value
- }
- }
- main.menu.navigationStack = new Proxy(main.menu.navigationStack, navigationStackProxyHandler)
- // Define the function to set a new active menu
- main.menu.setActiveMenu = function (name, onActivatedArgs = [], forceIntoNavigationStack = false) {
- if (name == this.activeMenu)
- return
- // Push it to the navigationStack to enable navigation back if necessary
- if ((name !== this.mainMenuName && name !== this.navigationStack[this.navigationStack.length - 1]) || forceIntoNavigationStack)
- this.navigationStack.push(name)
- const prevMenuObject = this.activeMenuObject,
- foundMenu = this.activeMenuObject = Menu.getActiveMenuObject(name)
- if (!foundMenu)
- throw new SyntaxError(`Cannot find Menu ${name}.\nAvailable Menus: ${Menu.getAvailableMenuNames().join(', ')}`)
- // Activate the new menu and pass any arguments for when a menu is activated
- foundMenu.activate(...onActivatedArgs)
- foundMenu.toggleButton?.setState(1, 0)
- this.activeMenu = name
- if (!prevMenuObject)
- return
- prevMenuObject.hideAllTags()
- prevMenuObject.toggleButton?.setState(0, 0)
- }
- // Define the function to add a new fature button
- main.menu.defineFeature = function (name, icon, activationType, ...callbacks) {
- const buttonElement = createElement("button", { class: `featureButton ${activationType}` }, {
- active: false,
- innerHTML: `<i class="${icon}"></i>`,
- setState(light) {
- this.classList.toggle("light", light)
- },
- }).appendTo(main.scriptElements.mainMenuFeatureContainer)
- // Bind all calbacks to refer to the buttonElement
- callbacks = callbacks.map(callback => callback.bind(buttonElement))
- switch (activationType) {
- case "toggle": {
- buttonElement.onclick = function (event) {
- this.setState(this.active = !this.active)
- callbacks[0]?.(event)
- }
- // This is to prevent the player from attacking
- buttonElement.onmousedown = function (event) {
- event.stopImmediatePropagation()
- }
- }
- break
- case "hold": {
- buttonElement.onmousedown = function (event) {
- event.stopImmediatePropagation()
- this.setState(this.active = true)
- callbacks[0]?.(event)
- }
- addEventListener("mouseup", function (event) {
- buttonElement.setState(buttonElement.active = false)
- callbacks[1]?.(event)
- })
- }
- break
- case "click": {
- buttonElement.onmouseenter = function (event) {
- this.setState(this.active = true)
- callbacks[1]?.(event)
- }
- buttonElement.onmouseleave = function (event) {
- this.setState(this.active = false)
- callbacks[2]?.(event)
- }
- buttonElement.onclick = callbacks[0]
- // This is to prevent the player from attacking
- buttonElement.onmousedown = function (event) {
- event.stopImmediatePropagation()
- }
- }
- }
- main.menu.features[name] = {
- name,
- activationType,
- icon,
- element: buttonElement,
- callbacks,
- }
- }
- // Define the 'main' variable in global scope for accessibility from the console
- window.bpi2 = main
- // Classes----->
- // Define the menu class
- class Menu {
- // Store all defined menus
- static DefinedMenus = []
- static getActiveMenuObject(activeMenuName) {
- return this.DefinedMenus.find(menu => menu.name === activeMenuName)
- }
- static getAvailableMenuNames() {
- return Menu.DefinedMenus.map((menu) => {
- return menu.name
- })
- }
- static createContentHolder(template) {
- const contentHolder = createElement("div",
- {
- class: "contentHolder"
- },
- {
- innerHTML: template,
- setState(moved, opaque) {
- this.classList.toggle("moved", moved)
- this.classList.toggle("opaque", opaque)
- },
- }).appendTo(main.scriptElements.mainMenuBody)
- return contentHolder
- }
- constructor(name, template, hasToggleButton = true, toggleButtonIcon, toggleButtonIndex = 0, isState = false) {
- if (Menu.getAvailableMenuNames().includes(name)) throw new SyntaxError(`Duplicate Menu name, ${name}.`)
- this.name = name
- this.template = template
- this.hasToggleButton = hasToggleButton
- this.isState = isState
- this.active = false
- this.type = "empty"
- if (!isState) {
- this.activeState = "none"
- this.hasStates = false
- this.tags = []
- this.canUpdateTags = false
- }
- this.defineMainElements(template)
- if (hasToggleButton && !isState)
- this.defineToggleButton(toggleButtonIcon, toggleButtonIndex)
- Menu.DefinedMenus.push(this)
- }
- // Defines the main header, body and footer elements of a given menu
- defineMainElements(template) {
- this.contentHolder = Menu.createContentHolder(template)
- this.header = this.contentHolder.querySelector("#header")
- this.body = this.contentHolder.querySelector("#body")
- this.footer = this.contentHolder.querySelector("#footer")
- }
- // Defines the toggle button for the menu
- defineToggleButton(icon, index = 0) {
- const toggleButtonContainer = main.scriptElements.otherMenuTogglesContainer
- this.toggleButton = toggleButtonContainer.addToggleButton(this.name,
- () => {
- if (main.menu.activeMenu == this.name)
- return
- main.menu.setActiveMenu(this.toggleButton.dataset.menuname)
- //Wait a bit so the user is ready for the change
- this.contentHolder.onceontransitionend(() => toggleButtonContainer.setState(0))
- }, icon, index)
- }
- initStates() {
- this.definedStates = []
- this.stateObject = this.stateMenu = this.stateContentHolder = null
- this.defineState = function (name, menu, callback = () => { }) {
- if (!menu.isState) throw new TypeError("A stateMenu should be a state. Define the menu with isState true.")
- this.definedStates.push({ name, menu, callback })
- }
- this.showState = function (stateObject) {
- if (stateObject) this.contentHolder.setState(0, 0)
- else return this.hideAllStates()
- this.definedStates.forEach(state => {
- if (state.menu.active = state.name == stateObject.name)
- state.menu.contentHolder.setState(1, 1)
- else state.menu.contentHolder.setState(0, 0)
- })
- this.resizeMenuCallback?.()
- this.onStateChangeCallback?.()
- }
- this.hideAllStates = function () {
- this.definedStates.forEach(state => state.menu.contentHolder.setState(0, 0))
- this.contentHolder.setState(1, 1)
- this.resizeMenuCallback?.()
- this.onStateChangeCallback?.()
- }
- this.setState = function (value) {
- // Check if the state is already set
- if (value == this.activeState)
- return
- // Find the new state
- const foundState = this.definedStates.find(state => state.name == value)
- // If no state is found then return back to the empty state
- if (!foundState)
- return this.hideAllStates()
- // Update the info about current state
- this.stateObject = foundState
- this.stateMenu = this.stateObject.menu
- this.stateContentHolder = this.stateMenu.contentHolder
- // Update current state name only after everything is done properly
- this.activeState = value
- // Show the state
- return this.showState(foundState)
- }
- this.hasStates = true
- return this
- }
- activate() {
- if (this.active) return
- setTimeout(() => this.canUpdateTags = true, 1200);
- // If there is an active state, display its content; otherwise, display the default content.
- // We can't rely on the showState function because states might not be initialized.
- (this.activeState != "none" ? this.stateContentHolder : this.contentHolder).setState(1, 1)
- // Iterate through defined menus to update their states
- Menu.DefinedMenus.forEach(menu => {
- // Deactivate the other menus
- if (menu.name != this.name && menu.name != this.stateMenu?.name)
- menu.deactivate()
- })
- // Execute the callback function when the menu is activated, passing any arguments from setActiveMenu
- this.onActivatedCallback?.(...arguments)
- this.resizeMenuCallback?.()
- this.active = true
- }
- deactivate() {
- (this.hasStates && this.activeState != "none" ? this.stateContentHolder : this.contentHolder).setState(0, 0)
- this.active = this.canUpdateTags = false
- }
- defineTag(name, type, callback, removalFrequency = 0) {
- const tagElement = createElement("span",
- {
- class: `tag ${type}`,
- "data-name": name.replaceAll(" ", ""),
- "data-removalfrequency": removalFrequency
- },
- {
- textContent: name
- })
- callback = callback.bind(this)
- const tagObject = {
- name,
- type,
- callback,
- removalFrequency,
- element: tagElement,
- state: "closed",
- show() {
- // return if tag is already open
- if (this.state === "open") return
- const container = main.scriptElements.mainMenuTagsContainer
- let inserted = false
- // This code arranges tags according to their likelihood of being 'shown'.
- if (container.childElementCount) {
- for (let element of Array.from(container.children)) {
- if (this.removalFrequency <= element.dataset.removalfrequency || !element.classList.contains("active")) {
- element.insertAdjacentElement("beforebegin", this.element)
- inserted = true
- break
- }
- }
- }
- // 'inserted' will only be false when no tag is in the tagsContainer, hence we can just append this tag
- if (!inserted)
- container.append(this.element)
- // Update this attribute to get the attribute from css and change the styling
- container.dataset.tagscount = parseInt(container.dataset.tagscount) + 1
- // Use requestAnimationFrame for optimal DOM update timing, and to be stop any visual glitches
- requestAnimationFrame(() => this.element.classList.add("active"))
- this.state = "open"
- },
- hide() {
- // Return if tag is closed or the element doesn't exist
- if (this.state === "closed" || this.state === "closing" || !this.element)
- return
- const container = main.scriptElements.mainMenuTagsContainer
- // Remove the 'active' class to hide the tag
- this.element.classList.remove("active")
- // Update the tag count attribute in the container
- container.dataset.tagscount = parseInt(container.dataset.tagscount) - 1
- // Remove the element from DOM after transition ends
- this.element.onceontransitionend(() => {
- this.element.remove()
- this.state = "closed"
- })
- this.state = "closing"
- }
- }
- this.tags.push(tagObject)
- }
- removeTag(name) {
- // Find the tag with the specified display name
- const tag = this.tags.find(tag => tag.name == name)
- if (!tag) return
- tag.hide()
- // Filter out the tag from the tags array
- this.tags = this.tags.filter(tag => tag.name != name)
- }
- hideAllTags() {
- this.tags.forEach(tag => tag.hide())
- }
- setUpdateCallback(callback) {
- this.updateCallback = callback
- }
- setBackgroundUpdateCallback(callback) {
- this.backgroundUpdateCallback = callback
- }
- setOnActivatedCallback(callback) {
- this.onActivatedCallback = callback
- }
- setOnStateChangeCallback(callback) {
- this.onStateChangeCallback = callback
- }
- setResizeMenuCallback(callback) {
- this.resizeMenuCallback = callback
- }
- updateTags(forceUpdate) {
- // Exit early if tag updates are not allowed and no force update is requested
- if (!this.canUpdateTags && !forceUpdate) return
- // Iterate through all tags
- this.tags.forEach(tag => {
- // Execute the callback function of the tag
- if (tag.callback())
- return tag.show() // Show the tag if callback returns true
- // Hide the tag if callback returns false
- tag.hide()
- })
- }
- updateStates(forceUpdate) {
- // Exit early if there are no states defined and no force update is requested
- if (!this.hasStates && !forceUpdate) return
- // Iterate through defined states
- for (let state of this.definedStates) {
- // Execute the callback function of the state
- if (state.callback()) {
- this.setState(state.name)
- return this.stateMenu.update()
- }
- }
- // Hide all states if no state is set
- if (this.activeState != "none") {
- this.activeState = "none"
- this.hideAllStates()
- }
- }
- update() {
- this.updateTags()
- // Check if any state is updated
- if (this.updateStates())
- return
- // Execute the updateCallback function if no state is updated
- this.updateCallback?.()
- }
- }
- class InfoMenu extends Menu {
- static Template = `
- <section id="header">
- <h2 id="entityName" data-forattr="name">Entity</h2>
- <h3 id="entityUID" data-forattr="uid">UID: <span>9817265</span></h3>
- <div id="entityHealthBarsContainer"></div>
- </div>
- </section>
- <section id="body" class="infoMenuBody"></section>`
- constructor(name, updatedAttributes = [], hasToggleButton = true, toggleButtonIcon, isState = false) {
- super(name, InfoMenu.Template, hasToggleButton, toggleButtonIcon, 0, isState)
- this.type = "info"
- // Make sure the place or the order in which attributes are presented can be controlled
- this.updatedAttributes = updatedAttributes.sort((a, b) => { return (a.index || 0) < (b.index || 0) ? -1 : 1 })
- this.infoElements = {}
- this.healthBars = []
- this.header.healthBarsContainer = this.header.querySelector("#entityHealthBarsContainer")
- this.parseHeaderElements()
- this.parseUpdatedAttributes()
- }
- createInfoElement(name, referenceName, activationCondition = () => { return true }, value, isVisible = true, index = 0, type = "text") {
- const infoElement = createElement("p",
- {
- class: "entityInfo" + (isVisible ? " visible" : "")
- },
- {
- innerHTML: `<span>${name}:</span> <strong>${0}</strong>`,
- name,
- referenceName,
- activationCondition,
- value,
- type,
- isVisible,
- index,
- setValue(value) {
- let toSetValue = value ?? null
- // if (this.type == "number" && toSetValue != null) {
- // switch (this.displayNumberSystem) {
- // case 0:
- // toSetValue = toLargeUnits(toSetValue)
- // break;
- // case 1:
- // toSetValue = toInternationalNumberSystem(new String(Math.floor(toSetValue)))
- // break
- // }
- // }
- return this.querySelector("strong").textContent = toSetValue
- }
- }).appendTo(this.body)
- if (type == "number") {
- infoElement.displayNumberSystem = 0
- infoElement.addEventListener("mousedown", () => infoElement.displayNumberSystem = (infoElement.displayNumberSystem + 1) % 3)
- }
- this.infoElements[name] = infoElement
- }
- createHealthBar(name, currentValRef, maxValRef, activationCondition = () => { return true }, barColor = getComputedStyle(document.body).getPropertyValue("--background-healthgreen"), isVisible = true) {
- const healthBar = createElement("div",
- {
- class: `entityHealth` + (isVisible ? " visible" : ""),
- 'data-name': name,
- },
- {
- innerHTML: `<div class="entityHealthBar"></div>`,
- currentValRef,
- maxValRef,
- activationCondition,
- barColor,
- isVisible
- }).appendTo(this.header.healthBarsContainer)
- this.healthBars.push(healthBar)
- healthBar.bar = healthBar.querySelector("div")
- healthBar.bar.style.background = barColor
- }
- parseHeaderElements() {
- for (let child of this.header.children)
- this.infoElements[child.dataset.forattr] = child
- }
- parseUpdatedAttributes() {
- this.updatedAttributes.forEach(attribute => {
- if (attribute.isBar === true)
- return this.createHealthBar(attribute.name, attribute.currentValRef, attribute.maxValRef, attribute.activationCondition, attribute.barColor, attribute.isVisible)
- this.createInfoElement(attribute.name, attribute.referenceName, attribute.activationCondition, attribute.value, attribute.isVisible, attribute.index, attribute.type)
- })
- }
- updateInfo(entity) {
- let element
- for (let property in this.infoElements) {
- element = this.infoElements[property]
- // Set value if the element has a setValue function and call it with the corresponding entity property
- if (element.classList.contains("entityInfo")) {
- element.classList.toggle("visible", element.activationCondition(entity))
- if (element.value)
- element.setValue(typeof element.value == "function" ? element.value(entity) : element.value)
- else element.setValue(entity.targetTick[typeof element.referenceName == "function" ? element.referenceName() : element.referenceName])
- continue
- }
- switch (element.dataset.forattr) {
- // Set element's text content to entity's name or model if name is not available
- case "name":
- element.textContent = entity.targetTick.name || entity.targetTick.model
- break
- // Set the text content of the span element to the entity's UID
- case "uid":
- element.querySelector("span").textContent = entity.targetTick.uid
- break
- }
- }
- let percentage
- for (let healthBar of this.healthBars) {
- percentage = entity.targetTick[healthBar.currentValRef] / entity.targetTick[healthBar.maxValRef] * 100
- healthBar.classList.toggle("visible", healthBar.activationCondition(entity))
- healthBar.bar.style.width = `${percentage}%`
- }
- }
- updateCallback() {
- const activeEntity = main.menu.activeEntity
- if (activeEntity)
- this.updateInfo(activeEntity)
- }
- resizeMenuCallback() {
- // Hardcoded min size(.9) for now
- main.scriptElements.mainMenu.resize(.9 + (this.activeState != "none" && this.stateMenu.updatedAttributes ? this.stateMenu : this).updatedAttributes.length / 36)
- }
- }
- // Other Handler Functions----->
- function checkMouseCollisionWithEntity() {
- // Check if the activation key is pressed down
- if (main.controls[main.settings.activationKey + "Down"] !== true || (main.menu.activeMenu != main.menu.mainMenuName && main.scriptElements.mainMenuWrapper.open))
- return
- let entityObj, entityWidth, entityHeight, entityTick, posScreen, distX, distY, circleRadius, dist
- // Iterate through each entity in the world
- Object.entries(game.world.entities).forEach(entity => {
- entityObj = entity[1]
- // Check if the entity belongs to a targetable class
- if (!main.settings.targetableEntityClass.includes(entityObj.entityClass))
- return
- // Get the entity's width and height
- entityWidth = entityObj.currentModel.base.sprite.width
- entityHeight = entityObj.currentModel.base.sprite.height
- // Throw an error if width or height is missing
- if (!entityWidth || !entityHeight) throw new ReferenceError("Cannot check collision without width and height.")
- entityTick = entityObj.targetTick
- posScreen = game.renderer.worldToScreen(entityTick.position.x, entityTick.position.y)
- distX = main.cursor.x - posScreen.x
- distY = main.cursor.y - posScreen.y
- circleRadius = Math.max(entityHeight, entityWidth) / 3
- dist = Math.sqrt(distX ** 2 + distY ** 2)
- // Check if the mouse is within the circle radius of the entity
- if (dist > circleRadius)
- return
- // Set the active entity's UID and update the menu state
- main.menu.activeEntityUID = entityTick.uid
- main.scriptElements.mainMenuWrapper.setState(1)
- main.scriptElements.mainMenuWrapper.moveToEntity(entityObj)
- main.scriptElements.entityFollower.followEntity(entityObj.targetTick.uid)
- })
- }
- function updateActiveEntity() {
- // Check if the active entity still exists in the world
- if (!game.world.entities[main.menu.activeEntityUID]) {
- // If the active entity exists, update the last active entity
- if (main.menu.activeEntity)
- main.menu.lastActiveEntity = main.menu.activeEntity
- main.menu.activeEntity = undefined
- return
- }
- // Update the active entity with the current entity data
- // Had to do this because objects are passed by reference
- const entity = game.world.entities[main.menu.activeEntityUID]
- main.menu.activeEntity = { entity, targetTick: entity.targetTick }
- }
- // Function responsible for initializing the main script, triggered on DOMContentLoaded event----->
- function script() {
- // DOM Manipulation----->
- // Return if hud is undefined
- if (!document.querySelector(".hud"))
- return
- // Append style element to the head of the document
- document.head.append(style)
- // Append style element for font-awesome to the head of the document
- document.head.append(createElement("link", { rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" }))
- // Define and append main menu wrapper element
- defineScriptElement("mainMenuWrapper", "div", { id: "mainMenuWrapper" }, {
- open: false,
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- // Set the state of the main menu wrapper
- setState(open) {
- // After transitionEnds wait for a delay before updates start
- this.onceontransitionend(() => setTimeout(() => this.open = open, 150))
- this.classList.toggle("open", open)
- main.scriptElements.entityFollower.setState(open)
- },
- // Move the main menu wrapper to the specified coordinates
- moveTo(x = this.x, y = this.y, pivotX = 0, pivotY = 0, transition = false) {
- const boundingBox = {
- width: this.scrollWidth,
- height: this.scrollHeight
- }
- let xPercent, yPercent, xTranslate = 0, yTranslate = 0
- this.classList.toggle("moveTransition", transition)
- // Move the menu in relation to the pivot
- x -= boundingBox.width * pivotX
- y -= boundingBox.height * pivotY
- // Using min-max instead of ifs to add bounds for the menu
- this.x = x = Math.round(Math.max(0, Math.min(x, innerWidth)))
- this.y = y = Math.round(Math.max(0, Math.min(y, innerHeight)))
- if (x > innerWidth - boundingBox.width)
- xTranslate = xPercent = 100
- // ^ This stops a bug where the otherMenuTogglesContainer is out of the screen
- else xPercent = (x / innerWidth) * 100
- if (y > innerHeight - boundingBox.height)
- yTranslate = yPercent = 100
- else yPercent = (y / innerHeight) * 100
- this.style.left = `${xPercent}%`
- this.style.top = `${yPercent}%`
- this.style.translate = `${-xTranslate}% ${-yTranslate}%`
- this.width = boundingBox.width
- this.height = boundingBox.height
- },
- moveToEntity(entity) {
- const menu = main.scriptElements.mainMenu,
- posScreen = game.renderer.worldToScreen(entity.targetTick.position.x, entity.targetTick.position.y)
- this.moveTo(posScreen.x, posScreen.y, .5 + (1 - menu.scrollWidth / this.width) / 2, 1.22, true)
- }
- }).appendTo(element(".hud"))
- // Define and append main menu element
- defineScriptElement("mainMenu", "div", { id: "mainMenu" }, {
- resize(ratio) {
- this.style.width = `${22.5 * ratio}rem`
- this.style.height = `${14 * ratio}rem`
- }
- }).appendTo(main.scriptElements.mainMenuWrapper)
- // Define and append main menu tags container element
- defineScriptElement("mainMenuTagsContainer", "div", { id: "mainMenuTagsContainer", "data-tagscount": 0 }).appendTo(main.scriptElements.mainMenuWrapper)
- // Define and append main menu feature buttons container
- defineScriptElement("mainMenuFeatureContainer", "div", { id: "mainMenuFeatureContainer" }).appendTo(main.scriptElements.mainMenuWrapper)
- // Define the feature for pinning the menu
- main.menu.defineFeature("pin", "fa-solid fa-location-pin", "toggle", () => main.scriptElements.mainMenuWrapper.classList.toggle("pinned", main.menu.pinned = !main.menu.pinned))
- // Function to handle the movement of the menu
- function moveFeatureHandler() {
- const eleBoundingBox = main.menu.features.move.element.getBoundingClientRect(),
- wrapperBoundingBox = main.scriptElements.mainMenuWrapper.getBoundingClientRect()
- const pivotX = ((eleBoundingBox.x - wrapperBoundingBox.x) + eleBoundingBox.width / 2) / wrapperBoundingBox.width,
- pivotY = ((eleBoundingBox.y - wrapperBoundingBox.y) + eleBoundingBox.height / 2) / wrapperBoundingBox.height
- main.scriptElements.mainMenuWrapper.moveTo(main.cursor.x, main.cursor.y, pivotX, pivotY, false)
- }
- let oldWidth = innerWidth,
- oldHeight = innerHeight
- // Make the menu responsive when screen resizes
- addEventListener("resize", () => {
- const wrapper = main.scriptElements.mainMenuWrapper
- wrapper.moveTo(wrapper.x / oldWidth * innerWidth, wrapper.y / oldHeight * innerHeight, 0, 0)
- oldWidth = innerWidth
- oldHeight = innerHeight
- })
- // Define the feature for moving the menu
- main.menu.defineFeature("move", "fa-solid fa-up-down-left-right", "hold",
- () => addEventListener("mousemove", moveFeatureHandler),
- () => removeEventListener("mousemove", moveFeatureHandler)
- )
- // document.addEventListener("visibilitychange", () => {
- // if (main.scriptElements.mainMenuWrapper.open)
- // main.scriptElements.mainMenuWrapper.setState(0)
- // })
- main.menu.defineFeature("forceClose", "fa-solid fa-xmark", "click", () => main.scriptElements.mainMenuWrapper.setState(0))
- // Define and append main menu body element
- defineScriptElement("mainMenuBody", "section", { id: "mainMenuBody" }).appendTo(main.scriptElements.mainMenu)
- // Define and append other menu button element
- defineScriptElement("otherMenuButton", "button", { id: "otherMenuButton" }, {
- innerHTML: `<i class="fa-solid fa-caret-right"></i>`,
- // Set the state of the other menu button
- setState(moved, rotated, dark) {
- this.classList.toggle("moved", moved)
- this.classList.toggle("rotated", rotated)
- this.classList.toggle("dark", dark)
- },
- // Return to the previous menu
- return() {
- const navigationStack = main.menu.navigationStack
- navigationStack.pop()
- main.menu.setActiveMenu(navigationStack[navigationStack.length - 1] ?? main.menu.mainMenuName)
- }
- }).appendTo(main.scriptElements.mainMenu)
- // Set callback functions for when a menu is added or removed from the navigation stack
- main.menu.navigationStack.onAddCallback = function () {
- main.scriptElements.otherMenuButton.setState(1, 1, 0)
- }
- main.menu.navigationStack.onRemoveCallback = function () {
- if (!this.length)
- main.scriptElements.otherMenuButton.setState(0, 0, 0)
- }
- // Define and append other menu container element
- defineScriptElement("otherMenuTogglesContainer", "div", { id: "otherMenuTogglesContainer" }, {
- open: false,
- toggleButtons: [],
- // Set the state of the other menu container
- setState(open) {
- this.classList.toggle("open", this.open = open)
- },
- // Add a toggle button for a specific menu
- addToggleButton(forMenuName, onClickCallback, icon, index = 0) {
- const toggleButton = createElement("button",
- {
- class: "toggleButton",
- "data-menuname": forMenuName,
- "data-index": index
- },
- {
- disabled: false,
- textContent: forMenuName[0],
- innerHTML: `<i class="${icon}"></i>`,
- setState(dark, disabled) {
- this.classList.toggle("dark", dark)
- this.classList.toggle("disabled", this.disabled = disabled)
- },
- })
- // Add event listener to the toggle button
- toggleButton.addEventListener("mousedown", function (event) {
- event.stopImmediatePropagation()
- if (!this.disabled)
- onClickCallback()
- })
- // Allows control over what the order of buttons will be in the container
- switch (true) {
- case index == 0: this.append(toggleButton)
- break
- case index >= this.childElementCount: this.prepend(toggleButton)
- break
- default: for (let child of this.children) {
- if (index > child.dataset.index)
- return child.insertAdjacentElement("beforebegin", this)
- }
- }
- this.toggleButtons.push(toggleButton)
- return toggleButton
- }
- }).appendTo(main.scriptElements.mainMenuWrapper)
- defineScriptElement("entityFollower", "div", { id: "entityFollower" }, {
- innerHTML: `<i class="fa-solid fa-location-arrow"></i>`,
- _x: 0,
- _y: 0,
- _rotation: 0,
- velocity: { x: 0, y: 0 },
- acceleration: { x: 0, y: 0 },
- destination: { x: innerWidth / 2, y: innerHeight / 2, direction: 0, reached: false, outOfReach: false },
- friction: .05,
- speed: .4,
- idleSpeed: .1,
- normalSpeed: .4,
- delta: 0,
- lastFrameTime: 0,
- activeEntityUID: undefined,
- activeEntity: undefined,
- setState(visible) {
- this.classList.toggle("visible", visible)
- },
- followEntity(uid) {
- this.activeEntityUID = uid
- },
- setSize(size) {
- this.style.fontSize = `${size}px`
- },
- setDestination(x, y) {
- this.destination = { x, y }
- },
- update(time) {
- this.delta = (time - this.lastFrameTime) / (1000 / 60) // :)
- this.lastFrameTime = time
- this.activeEntity = game.world.entities[this.activeEntityUID]
- const subSteps = 8,
- divDT = this.delta / subSteps;
- // If there is an activeEntity, follow it by updating the destination and set the speed to normal
- if (this.activeEntity) {
- var targetTick = this.activeEntity.targetTick,
- posScreen = game.renderer.worldToScreen(targetTick.position.x, targetTick.position.y),
- entityWidth = this.activeEntity.currentModel.base.sprite.width,
- entityHeight = this.activeEntity.currentModel.base.sprite.height,
- radius = Math.sqrt(entityWidth ** 2 + entityHeight ** 2)
- this.speed = this.normalSpeed
- this.setDestination(posScreen.x, posScreen.y)
- }
- // Otherwise idle with slower speed while moving around to random destinations from the center of the screen <- removed
- else if (this.destination.reached || this.destination.outOfReach || main.menu.activeMenu != main.menu.mainMenuName)
- this.speed = this.idleSpeed
- this.setSize(Math.min(Math.max(22, (radius ?? 0) * .122), 36))
- for (let i = 0; i < subSteps; i++) {
- // Make the arrow bigger for bigger entities
- // Calculate the distance from the destination
- let distX = this.destination.x - this.x,
- distY = this.destination.y - this.y,
- dist = Math.sqrt(distX ** 2 + distY ** 2)
- // Calculate the direction of the destination
- this.destination.direction = Math.atan2(distY, distX)
- // Check if destination is reached
- this.destination.reached = dist < this.arrowSize
- // Compressed way of setting acceleration to 0 when the destination is reached
- // And to have a normal acceleartion when moving to a new destination
- this.acceleration.x = this.speed * Math.cos(this.destination.direction) * (!this.destination.reached)
- this.acceleration.y = this.speed * Math.sin(this.destination.direction) * (!this.destination.reached)
- this.x += this.velocity.x * divDT
- this.y += this.velocity.y * divDT
- // Smoothly rotate towards the destination
- this.rotation = Math.lerpAngles(this.rotation, Math.atan2(this.velocity.y, this.velocity.x), .22 * divDT)
- this.velocity.x *= 1 - this.friction
- this.velocity.y *= 1 - this.friction
- this.velocity.x += this.acceleration.x
- this.velocity.y += this.acceleration.y
- // Add bounds
- this.x = Math.max(Math.min(innerWidth - this.arrowSize, this.x), this.arrowSize)
- this.y = Math.max(Math.min(innerHeight - this.arrowSize, this.y), this.arrowSize)
- }
- // Define when a destination is out of reach
- this.destination.outOfReach = this.destination.x < 0 || this.destination.x > innerWidth || this.destination.y < 0 || this.destination.y > innerHeight
- },
- updateVisiblity() {
- if (this.activeEntity) this.setState(main.scriptElements.mainMenuWrapper.open)
- else if (this.destination.outOfReach || main.menu.activeMenu != main.menu.mainMenuName) this.setState(0)
- }
- }).appendTo(element(".hud"))
- Object.defineProperties(main.scriptElements.entityFollower, {
- x: {
- set(value) {
- this._x = value
- this.style.left = `${value}px`
- },
- get() {
- return this._x
- },
- },
- y: {
- set(value) {
- this._y = value
- this.style.top = `${value}px`
- },
- get() {
- return this._y
- },
- },
- rotation: {
- set(rad) {
- this._rotation = rad
- this.style.rotate = `${rad}rad`
- },
- get() {
- return this._rotation
- },
- },
- arrowSize: {
- get() {
- return this.querySelector("i").scrollWidth
- }
- }
- })
- // Event Listeners----->
- // Listen for mouse movement events and update the cursor position accordingly
- addEventListener("mousemove", (event) => {
- main.cursor = event
- })
- // Listen for keydown events and update controls accordingly
- addEventListener("keydown", (event) => {
- const key = event.key.toLocaleLowerCase()
- if (!main.controls.listenedKeys.includes(key)) return
- main.controls[`${key}Up`] = false
- main.controls[`${key}Down`] = true
- })
- // Listen for keyup events and update controls accordingly
- addEventListener("keyup", (event) => {
- const key = event.key.toLocaleLowerCase()
- if (!main.controls.listenedKeys.includes(key)) return
- main.controls[`${key}Up`] = true
- main.controls[`${key}Down`] = false
- })
- // Listen for mousedown events and close the main menu if it's open and the click is outside of it
- addEventListener("mousedown", (event) => {
- if (main.inGame && (main.scriptElements.mainMenuWrapper.contains(event.target) || !main.scriptElements.mainMenuWrapper.open || main.menu.pinned)) {
- event.preventDefault()
- event.stopImmediatePropagation()
- return
- }
- main.scriptElements.mainMenuWrapper.setState(0)
- })
- // Listen for mousedown events on the other menu button and toggle the other menu container's state
- main.scriptElements.otherMenuButton.addEventListener("mousedown", function (event) {
- event.stopImmediatePropagation()
- // If navigation stack is not empty, execute return function, else toggle the other menu container's state
- if (main.menu.navigationStack.length) return this.return()
- main.scriptElements.otherMenuTogglesContainer.setState(!main.scriptElements.otherMenuTogglesContainer.open)
- // Toggle button state based on the other menu container's state
- if (main.scriptElements.otherMenuTogglesContainer.open) this.setState(0, 1, 1)
- else this.setState(0, 0, 0)
- })
- var lastMenuUpdateTime = 0,
- lastMouseCollisionCheckTime = 0,
- lastBackgroundMenuUpdateTime = 0;
- // Self-invoking function to update ASAP
- (function update(time) {
- requestAnimationFrame(update)
- // Check if the player is in the game world
- if (!(main.inGame = window.game != undefined && game.network?.connected != undefined && game.world?.inWorld != undefined && game.ui?.playerTick != undefined) || main.settings.paused)
- return
- // Set the active menu to the main menu if no menu is currently active
- if (!main.menu.activeMenu)
- main.menu.setActiveMenu(main.menu.mainMenuName)
- // Perform mouse collision check if enough time has elapsed
- if (time - lastMouseCollisionCheckTime >= 1000 / main.settings.mouseCollisionCheckFPS) {
- checkMouseCollisionWithEntity()
- lastMouseCollisionCheckTime = time
- }
- main.scriptElements.entityFollower.updateVisiblity()
- if (!main.scriptElements.mainMenuWrapper.open)
- return
- // Update the active entity and active menu if enough time has elapsed
- if (time - lastMenuUpdateTime >= 1000 / main.settings.menuUpdateFPS) {
- updateActiveEntity()
- main.menu.activeMenuObject?.update()
- lastMenuUpdateTime = time
- }
- // Update background elements if enough time has elapsed
- if (time - lastBackgroundMenuUpdateTime >= 1000 / main.settings.backgroundMenuUpdateFPS) {
- // Call the background update callback for all menus and toggle buttons
- [...Menu.DefinedMenus, ...main.scriptElements.otherMenuTogglesContainer.toggleButtons].forEach(element => element.backgroundUpdateCallback?.())
- lastBackgroundMenuUpdateTime = time
- }
- main.scriptElements.entityFollower.update(time)
- })(0)
- // Defining Menus----->
- // Main Menu--->
- const mainInfoMenu = new InfoMenu("infoMenu", [
- { name: "Model", referenceName: "model" },
- { name: "Wood", referenceName: "wood", type: "number" },
- { name: "Token", referenceName: "token", type: "number" },
- { name: "Stone", referenceName: "stone", type: "number" },
- {
- name: "Wave", value: (player) => {
- return player.targetTick.wave ?? "Not Found"
- },
- type: "number"
- },
- { name: "Gold", referenceName: "gold", type: "number" },
- { name: "Score", referenceName: "score", type: "number" },
- { name: "Health", referenceName: "health" },
- { name: "MaxHealth", referenceName: "maxHealth" },
- {
- name: "ShieldHealth",
- referenceName: "zombieShieldHealth",
- activationCondition: (entity) => {
- return entity.targetTick.zombieShieldMaxHealth != 0
- },
- index: 1,
- type: "number"
- },
- {
- name: "MaxShieldHealth",
- referenceName: "zombieShieldMaxHealth",
- activationCondition: (entity) => {
- return entity.targetTick.zombieShieldMaxHealth != 0
- },
- index: 2,
- type: "number"
- },
- {
- isBar: true,
- name: "HP",
- currentValRef: "health",
- maxValRef: "maxHealth",
- },
- {
- isBar: true,
- name: "SH",
- currentValRef: "zombieShieldHealth",
- maxValRef: "zombieShieldMaxHealth",
- isVisible: false,
- activationCondition: (entity) => {
- return entity.targetTick.zombieShieldMaxHealth != 0
- },
- barColor: "#3da1d9"
- },
- ], false).initStates()
- mainInfoMenu.defineState(
- "ResourceProp",
- new InfoMenu(
- `infoMenuStateResource`,
- [
- { name: "Model", referenceName: "model" },
- { name: "CollisionRadius", referenceName: "collisionRadius" },
- ],
- false,
- "",
- true
- ),
- () => {
- return (main.menu.activeEntity || main.menu.lastActiveEntity)?.targetTick.model.match(/(tree)|(stone)|(neutralcamp)/gi)
- }
- )
- mainInfoMenu.defineState(
- "TowerProp",
- new InfoMenu(
- `infoMenuStateTower`,
- [
- { name: "Model", referenceName: "model" },
- { name: "Tier", referenceName: "tier", },
- { name: "Health", referenceName: "health", type: "number" },
- { name: "MaxHealth", referenceName: "maxHealth", type: "number" },
- // A lot of error handling
- {
- name: "Damage",
- activationCondition: (tower) => {
- return game.ui.buildingSchema[tower.targetTick.model]?.damageTiers != undefined
- },
- value: (tower) => {
- return game.ui.buildingSchema[tower.targetTick.model]?.damageTiers?.[tower.targetTick.tier - 1] ?? "Not Found"
- },
- type: "number"
- },
- {
- name: "AttackSpeed",
- activationCondition: (tower) => {
- return main.gameData.towers[tower.targetTick.model]?.attackSpeed != undefined
- },
- value: (tower) => {
- return main.gameData.towers[tower.targetTick.model]?.attackSpeed?.[tower.targetTick.tier - 1] ?? "Not Found"
- }
- },
- {
- name: "Range",
- activationCondition: (tower) => {
- return game.ui.buildingSchema[tower.targetTick.model]?.rangeTiers != undefined
- },
- value: (tower) => {
- return game.ui.buildingSchema[tower.targetTick.model]?.rangeTiers?.[tower.targetTick.tier - 1] ?? "Not Found"
- },
- },
- {
- name: "Gold/Sec",
- activationCondition: (tower) => {
- return tower.targetTick.model.match(/goldmine/gi)
- },
- value: (tower) => {
- return game.ui.buildingSchema[tower.targetTick.model]?.gpsTiers?.[tower.targetTick.tier - 1] ?? "Not Found"
- },
- type: "number"
- },
- {
- name: "HarvestAmount",
- activationCondition: (tower) => {
- return tower.targetTick.model.match(/harvester/gi)
- },
- value: (tower) => {
- return game.ui.buildingSchema[tower.targetTick.model]?.harvestTiers?.[tower.targetTick.tier - 1] ?? "Not Found"
- }
- },
- {
- name: "HarvestCapacity",
- activationCondition: (tower) => {
- return tower.targetTick.model.match(/harvester/gi)
- },
- value: (tower) => {
- return game.ui.buildingSchema[tower.targetTick.model]?.harvestCapacityTiers?.[tower.targetTick.tier - 1] ?? "Not Found"
- },
- type: "number"
- },
- {
- name: "SlowAmount",
- activationCondition: (tower) => {
- return tower.targetTick.model.match(/slowtrap/gi)
- },
- value: (tower) => {
- return main.gameData.towers[tower.targetTick.model]?.slowAmount?.[tower.targetTick.tier - 1] ?? "Not Found"
- }
- },
- {
- isBar: true,
- name: "HP",
- currentValRef: "health",
- maxValRef: "maxHealth",
- },
- ],
- false,
- "",
- true
- ),
- () => {
- return Object.entries(game.ui.buildingSchema).find(building => building[0] == (main.menu.activeEntity || main.menu.lastActiveEntity)?.targetTick.model)
- }
- )
- mainInfoMenu.defineState(
- "Npc",
- new InfoMenu(
- `infoMenuStateNPC`,
- [
- { name: "Model", referenceName: "model" },
- {
- name: "Damage",
- activationCondition: (npc) => {
- return npc.targetTick.damage
- },
- value: (npc) => {
- return npc.targetTick.damage ?? "Not Found"
- },
- type: "number"
- },
- { name: "Yaw", referenceName: "yaw" },
- { name: "Health", referenceName: "health", type: "number" },
- { name: "MaxHealth", referenceName: "maxHealth", type: "number" },
- {
- isBar: true,
- name: "HP",
- currentValRef: "health",
- maxValRef: "maxHealth",
- }
- ],
- false,
- "",
- true
- ),
- () => {
- return (main.menu.activeEntity || main.menu.lastActiveEntity)?.targetTick.entityClass == "Npc"
- }
- )
- mainInfoMenu.defineState(
- "PetProp",
- new InfoMenu("infoMenuStatePet", [
- { name: "Model", referenceName: "model" },
- {
- name: "Name",
- value: (pet) => {
- return game.ui.itemSchema[pet.targetTick.model]?.name ?? "Not Found"
- }
- },
- { name: "Experience", referenceName: "experience", type: "number" },
- { name: "Tier", referenceName: "tier" },
- {
- name: "Damage",
- activationCondition: (pet) => {
- return game.ui.itemSchema[pet.targetTick.model]?.damageTiers != undefined
- },
- value: (pet) => {
- return game.ui.itemSchema[pet.targetTick.model]?.damageTiers?.[pet.targetTick.tier - 1] ?? "Not Found"
- },
- type: "number"
- },
- {
- name: "MovementSpeed",
- value: (pet) => {
- return main.gameData.pets[pet.targetTick.model]?.speed?.[pet.targetTick.tier - 1] ?? "Not Found"
- }
- },
- {
- name: "AttackSpeed",
- value: (pet) => {
- return (1000 / game.ui.itemSchema[pet.targetTick.model]?.attackSpeedTiers?.[pet.targetTick.tier - 1]) ?? "Not Found"
- }
- },
- {
- name: "ResourceGain",
- activationCondition: (pet) => {
- return pet.targetTick.model.match(/petminer/gi)
- },
- value: (pet) => {
- return main.gameData.pets[pet.targetTick.model]?.resourceGain?.[pet.targetTick.tier - 1] ?? "Not Found"
- },
- isVisible: false,
- },
- { name: "Health", referenceName: "health", type: "number" },
- { name: "MaxHealth", referenceName: "maxHealth", type: "number" },
- {
- isBar: true,
- name: "HP",
- currentValRef: "health",
- maxValRef: "maxHealth",
- },
- ],
- false,
- "",
- true),
- () => {
- return (main.menu.activeEntity || main.menu.lastActiveEntity)?.targetTick.model.match(/pet/gi)
- })
- mainInfoMenu.defineTag(
- "Entity Not Found",
- "error",
- () => {
- return (main.menu.activeMenu == main.menu.mainMenuName && !main.menu.activeEntity)
- },
- 1)
- // Party Spectate Menu--->
- // The most annoying menu
- const partySpectateMenu = new Menu("partySpectateMenu", `<div id="body" class="spectateMenuBody"></div>`, true, "fas fa-users", 1)
- partySpectateMenu.spectateButtons = []
- partySpectateMenu.createSpectateButton = function () {
- const spectateButton = createElement("div",
- {
- class: "spectateButton",
- "data-partyposition": "",
- "data-playeruid": ""
- },
- {
- innerHTML: `
- <button>
- <div class="wrapper">
- <span class="name">Undef</span>
- <section class="tag active uid">UID: <span></span></section>
- <section class="tag active position"></section>
- </div>
- </button>
- `
- })
- spectateButton.nameTag = spectateButton.querySelector("span.name")
- spectateButton.positionTag = spectateButton.querySelector("section.position")
- spectateButton.uidTag = spectateButton.querySelector("section.uid span")
- spectateButton.addEventListener("mousedown", function (event) {
- event.stopImmediatePropagation()
- this.onceontransitionend(() => {
- spectateInfoMenu.spectateEntityUID = parseInt(this.dataset.playeruid)
- main.menu.setActiveMenu(spectateInfoMenu.name)
- })
- })
- this.spectateButtons.push(spectateButton)
- this.body.append(spectateButton)
- }
- // I don't know how to do this in css :(
- partySpectateMenu.styleButtons = function (gap) {
- const visibleElements = Array.from(partySpectateMenu.body.children).filter(element => element.classList.contains("visible"))
- this.body.dataset.playercount = visibleElements.length
- if (visibleElements.length == 1) return
- gap = `${gap / 2}px`
- let eleCurrent
- for (let index = 0; index < visibleElements.length; index++) {
- eleCurrent = visibleElements[index]
- if (visibleElements[index - 1]) eleCurrent.style.paddingTop = gap
- if (visibleElements[index + 1]) eleCurrent.style.paddingBottom = gap
- }
- }
- partySpectateMenu.updateCallback = function () {
- if (game.ui.playerPartyMembers.length <= 1)
- main.scriptElements.otherMenuButton.return()
- }
- partySpectateMenu.backgroundUpdateCallback = function () {
- // Filter out the player and add index to each party member
- const partyMembers = game.ui.playerPartyMembers.map((member, index) => {
- if (member.playerUid == game.ui.playerTick.uid) return undefined
- return { member, index }
- }).filter(element => element)
- this.spectateButtons.forEach((button, index) => {
- button.classList.toggle("visible", false)
- button.dataset.playeruid = button.dataset.partyposition = ""
- if (!partyMembers[index]) return
- const nameTag = button.nameTag,
- { member, index: memberIndex } = partyMembers[index]
- nameTag.textContent = member.displayName
- switch (button.dataset.partyposition = memberIndex) {
- case 0: button.positionTag.textContent = "Leader"
- break
- case 1: button.positionTag.textContent = "Co-Leader"
- break
- case 2: case 3: button.positionTag.textContent = "Member"
- break
- }
- button.uidTag.textContent = button.dataset.playeruid = member.playerUid
- button.classList.toggle("visible")
- })
- this.styleButtons(8)
- }
- partySpectateMenu.toggleButton.backgroundUpdateCallback = function () {
- this.setState(0, game.ui.playerPartyMembers.length <= 1)
- }
- for (let i = 0; i < 4; i++)
- partySpectateMenu.createSpectateButton()
- // Spectate Info Menu--->
- const spectateInfoMenu = new InfoMenu("spectateInfoMenu",
- [
- ...mainInfoMenu.updatedAttributes,
- {
- name: "CanSell", value: (player) => {
- return new Boolean(game.ui.getPlayerPartyMembers()?.find(member => member.playerUid == player.targetTick.uid)?.canSell)
- },
- index: 0,
- },
- ], false)
- spectateInfoMenu.spectateEntityUID = undefined
- spectateInfoMenu.updateCallback = function () {
- this.spectateEntity = game.world.entities[this.spectateEntityUID]
- if (this.spectateEntity)
- this.updateInfo(this.spectateEntity)
- }
- spectateInfoMenu.defineTag("Spectating", "neutral", function () {
- return true
- }, 0)
- spectateInfoMenu.defineTag("Entity Not Found", "error", function () {
- return !this.spectateEntity
- }, 1)
- // Pet Spectate Menu--->
- const petSpectateMenu = new InfoMenu("PetSpectateMenu", mainInfoMenu.definedStates.find(state => state.name == "PetProp").menu.updatedAttributes, true, "fa-solid fa-paw")
- petSpectateMenu.updateCallback = function () {
- const petTick = game.ui.getPlayerPetTick()
- if (petTick)
- this.updateInfo({ targetTick: petTick })
- }
- petSpectateMenu.toggleButton.backgroundUpdateCallback = function () {
- this.setState(0, main.menu.activeEntityUID != game.ui.playerTick.uid || !game.ui.getPlayerPetTick())
- }
- petSpectateMenu.defineTag("Pet Died", "warning", () => {
- return game.ui.getPlayerPetTick()?.health <= 0
- })
- }
- // If the document is already loaded, call the scriptInit function immediately
- if (document.readyState != "loading") script()
- else document.addEventListener("DOMContentLoaded", script)
- // If the document is still loading, add an event listener for the DOMContentLoaded event
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址