// ==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
// @downloadURL https://update.greasyfork.icu/scripts/433138/Better%20Player%20Info%202.user.js
// @updateURL https://update.greasyfork.icu/scripts/433138/Better%20Player%20Info%202.meta.js
// ==/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: ``,
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 = `
Entity
UID: 9817265