// ==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