// ==UserScript== // @name SB-AUI // @namespace http://tampermonkey.net/ // @version 1.2.1 // @description Advanced UI for Starblast with extra features // @author Halcyon // @license All rights reserved, this code may not be reproduced or used in any way without the express written consent of the author. // @match https://starblast.io/ // @icon https://www.google.com/s2/favicons?sz=64&domain=starblast.io // @grant none // @downloadURL none // ==/UserScript== /** * CHANGELOG * 1.0.0 - Initial creation. Core of AUI * 1.1.0 - Added team evaluation (PBS, PPBS, DPS) * 1.2.0 - Optimizations, code cleanup and transition to a component-based system * 1.2.1 - More optimizations and code cleanup. Added hardElementRefresh */ 'use strict'; const API_LINK = "https://starblast.dankdmitron.dev/api/simstatus.json"; const CURRENT_RUNNING_VERSION = "1.2.1" /********* STYLING ************ */ const applyCSS = (styles, element) => { if (!element) { return; } for (let key of Object.keys(styles)) { try { element.style[key] = styles[key] } catch (ex) {console.error(`Object.prototype.applyStyles: Cannot apply style '${key}' to ${element}`)} } return; } const applyBaseStyles = (element, includeFont = true) => { element.style.boxShadow = "black 0px 0px 0px"; element.style.textShadow = "black 0px 0px 0px"; if (includeFont) { element.style.fontFamily = `"DM Sans", sans-serif`; } element.style.fontWeight = "400"; element.style.background = "#0b0b0b"; element.style.border = "1px solid #1a1a1a" element.style.color = "#FFF"; element.style.borderRadius = "10px"; element.classList.add("hover-class"); } //Remove unneeded UI elements (spotify and socials columns) let removalQueries = ['[data-translate-base="music"]','[data-translate-base="community"]']; for (let query of removalQueries) { for (let el of document.querySelectorAll(query)) { el.style.display = "none" } } //Setting the AUI logo try { document.querySelector("#logo > img").src = "https://i.ibb.co/t25sFmR/SBAUI.png"; } catch (ex) { try { setTimeout(() => { document.querySelector("#logo > img").src = "https://i.ibb.co/t25sFmR/SBAUI.png"; }, 500) } catch (ex) { setTimeout(() => { document.querySelector("#logo > img").src = "https://i.ibb.co/t25sFmR/SBAUI.png"; }, 1000) } } //Importing DM Sans and Abel try { var styleElement = document.createElement('style'); var importRule = ` @import url('https://fonts.googleapis.com/css2?family=Abel&family=DM+Sans:wght@400;500;700&display=swap'); `; styleElement.textContent = importRule; document.head.appendChild(styleElement); document.addEventListener("DOMContentLoaded", function() { var elementsToStyle = document.querySelectorAll("body"); elementsToStyle.forEach(function(element) { element.style.fontFamily = '"DM Sans", sans-serif'; }); }); } catch (ex) {} //All buttons get base styles for (let el of document.querySelectorAll('button')) { applyBaseStyles(el) } //Scrollbar code document.documentElement.style.scrollbarWidth = 'thin'; document.documentElement.style.msOverflowStyle = 'none'; var style = document.createElement('style'); var css = ` /* Webkit and Blink */ ::-webkit-scrollbar { width: 0.2em; } ::-webkit-scrollbar-thumb { background-color: #1a1a1a; border: none; border-radius: 0.1em; } ::-webkit-scrollbar-track { background-color: transparent; border: none; } /* Firefox */ scrollbar-width: thin; scrollbar-color: transparent transparent; `; style.appendChild(document.createTextNode(css)); document.head.appendChild(style); document.querySelector("body").style.backgroundColor = "#0b0b0b"; //Overlay styles applyCSS({ backgroundColor: "#1a1a1a", background: "repeating-linear-gradient(45deg, #1a1a1a 0, #131313 1px, #0b0b0b 0, #0b0b0b 50%)", backgroundSize: "10px 10px", maxWidth: "calc(100% - 60px)", maxHeight: "calc(100% - 60px)", margin: "auto auto", boxSizing: "content-box", boxShadow: "black 0px 0px 0px", border: "6px solid #131313", outline: "54px solid #0b0b0b", }, document.querySelector("#overlay")); //Body styles document.querySelector("body").style.height = "100dvh"; document.querySelector("body").style.width = "100vw"; //Play button styles applyCSS({ fontFamily: `"DM Sans", sans-serif`, letterSpacing: "4px", fontSize: "2.2rem", fontWeight: "600" }, document.querySelector("#play")) //Styles applyCSS({ background: `transparent`, textShadow: `black 0px 0px 0px`, fontFamily: `'Abel', sans-serif`, fontSize: `1rem`, letterSpacing: `0px`, marginTop: `5px`, marginLeft: `auto`, marginRight: `auto`, width: `80%`, borderTop: `1px solid #1a1a1a`, color: `gray` },document.querySelector("#game_modes")) //Changelog styles document.querySelector(".changelog-new").style.fontFamily = `"DM Sans", sans-serif`; //Tools like "modding" styles (4 buttons) document.querySelector('.followtools').style.left = '0'; document.querySelector('.followtools').style.width = 'max-content'; document.querySelector('.followtools').style.zIndex = '500'; //Changelog to top document.querySelector('.bottom-left').style.top = '0'; document.querySelector('.bottom-left').style.height = 'max-content'; //Name input styles applyCSS({ background: '#0b0b0b', border: '1px solid #1a1a1a', fontFamily: 'DM Sans', boxShadow: 'black 0px 0px 0px', borderRadius: '10px', },document.querySelector('.inputwrapper')) //Buttons for switching modes styles const leftRight = [document.querySelector('#prevMode'),document.querySelector('#nextMode')]; for (let el of leftRight) { el.style.color = '#FFFFFF'; el.style.textShadow = 'black 0px 0px 0px'; } //Elements to apply baseStyles to const baseStyleQueries = ['.changelog-new', '#moddingspace', "#donate", "#rankings", "#training"]; for (let query of baseStyleQueries) { applyBaseStyles(document.querySelector(query)); } //Styles for button elements const ml = ['#moddingspace', "#donate", "#rankings", "#training"] for (let query of ml) { let item = document.querySelector(query); item.style.paddingBottom = "0.5rem"; let icon = document.querySelector(`${query} > i`); icon.style.margin = "0.5rem auto 0.5rem auto"; icon.style.paddingBottom = '0.5rem'; icon.style.width = '80%'; icon.style.borderBottom = '1px solid #1a1a1a'; let span = document.querySelector(`${query} > span`); span.style.color = "#FFF"; span.style.letterSpacing = '1px'; span.style.textShadow = 'black 0px 0px 0px'; span.style.fontWeight = '500'; } for (let el of document.querySelectorAll('.modal')) { applyBaseStyles(el); } for (let el of document.querySelectorAll('.social i')) { applyBaseStyles(el, false); } /***********=/STYLING********** */ //Create SL INTEGRATION var J = document.createElement("div"); J.id = "SL_INTEGRATION"; document.querySelector('#overlay').appendChild(J); const SL_INTEGRATION = document.querySelector('#SL_INTEGRATION'); applyCSS({ position: 'absolute', height: '100%', width: '25%', top: '0', right: '0', display: 'flex', flexDirection: 'column' }, SL_INTEGRATION) const templateStatusData = () => ({name: "",id: "",team_1: {hue: null,gems: 0,level: 0,potentialOutput: 0,PBS: 0,PPBS: 0,players: []},team_2: {hue: null,gems: 0,level: 0,potentialOutput: 0,PBS: 0,PPBS: 0,players: []},team_3: {hue: null,gems: 0,level: 0,potentialOutput: 0,PBS: 0,PPBS: 0,players: []}}) //All variables used in the components should be declared here window["COMPONENT_STATE_VALUES"] = { options: { activePanel: "listing", activeRegion: "europe", modes: { team: true, survival: false, deathmatch: false, modding: false, invasion: false } }, filteredSystems: [], statusReportActive: false, statusReportData: { name: "", id: "", team_1: { hue: null, gems: 0, level: 0, potentialOutput: 0, PBS: 0, PPBS: 0, players: [] }, team_2: { hue: null, gems: 0, level: 0, potentialOutput: 0, PBS: 0, PPBS: 0, players: [] }, team_3: { hue: null, gems: 0, level: 0, potentialOutput: 0, PBS: 0, PPBS: 0, players: [] } } } //Component class for easier maintaining. NOTE: All components must have only 1 parent element and components should be named using PascalCase class Component { /** * AUI HTML component. Make sure there is a wrapping parent element. * @param {String} ID - ID of the element * @param {Function} HTML - Function that returns a template string representing innerHTML. Note that any conditions put on the wrapper element itself will never reflect upon using .refreshElement(), to reflect those changes use .hardRefreshElement() */ constructor(ID, HTML) { this.ID = ID this.HTML = HTML } evaluate() { if (typeof this.HTML === 'function') { return this.HTML(); } else {throw new Error(`Component class error: Second argument in Component instantiation is not a function ('${typeof this.HTML}'). ${typeof this.HTML === 'string' && "Hint: Put '() =>' before your template literal"}`)} } getElement() { const tempContainer = document.createElement("span"); tempContainer.innerHTML = this.evaluate(); tempContainer.children[0].setAttribute("id", `${this.ID}`) if (tempContainer.children.length > 1) { throw new Error(`Component class error: Components must have a parent element (Component ID: ${this.ID})`) } return tempContainer.innerHTML; } refreshElement() { //console.log(`Component refreshed: ${this.ID}`) try { let tempElement = document.createElement("span"); tempElement.innerHTML = this.evaluate(); document.querySelector(`#${this.ID}`).innerHTML = tempElement.children[0].innerHTML; } catch (ex) {console.error(`Couldn't refresh element with the ID of '${this.ID}': ` + ex)} } hardRefreshElement() { try { let tempElement = document.createElement("span"); tempElement.innerHTML = this.evaluate(); tempElement.children[0].setAttribute("id", `${this.ID}`); document.querySelector(`#${this.ID}`).outerHTML = tempElement.innerHTML; } catch (ex) {console.error(`Couldn't hardRefresh element with the ID of '${this.ID}': ` + ex)} } } let API_TIMER = setInterval(async () => { if (COMPONENT_STATE_VALUES.options.activePanel !== 'listing') { return; } let raw = await(await fetch(API_LINK)).json(); let allSystems = []; for (let item of raw) { if (item.modding) { if (!COMPONENT_STATE_VALUES.options.modes.modding) { continue; } } if (item.location.toLowerCase() !== COMPONENT_STATE_VALUES.options.activeRegion) { continue; } for (let system of item.systems) { if (COMPONENT_STATE_VALUES.options.modes[system.mode]) { allSystems.push({ ...system, IP_ADDR: item.address }); } } } COMPONENT_STATE_VALUES.filteredSystems = allSystems.sort((a, b) => a.time - b.time); Listing.refreshElement(); }, 3200) let STATUS_TIMER = null; window.statusReport = async (query) => { if (STATUS_TIMER) {return}; COMPONENT_STATE_VALUES.statusReportActive = true; StatusReportModal.hardRefreshElement(); STATUS_TIMER = setInterval(async () => { let raw = await (await fetch(`https://starblast.dankdmitron.dev/api/status/${query}`)).json(); COMPONENT_STATE_VALUES.statusReportData = templateStatusData(); COMPONENT_STATE_VALUES.statusReportData.name = raw.name; COMPONENT_STATE_VALUES.statusReportData.id = query.split('@')[0]; for (let key of Object.keys(raw.players)) { let player = raw.players[key]; COMPONENT_STATE_VALUES.statusReportData[`team_${player.friendly + 1}`].players.push({ name: player.player_name, ecp: !!player.custom, score: player.score, type: player.type, PBS: calculatePlayerScore(player.type, !!player.custom) }) COMPONENT_STATE_VALUES.statusReportData[`team_${player.friendly + 1}`].hue = player.hue; } for (let team of raw.mode.teams) { for (let num of ["team_1", "team_2", "team_3"]) { if (team.hue === COMPONENT_STATE_VALUES.statusReportData[num].hue) { COMPONENT_STATE_VALUES.statusReportData[num].gems = team.crystals COMPONENT_STATE_VALUES.statusReportData[num].level = team.level break } } } for (let team of ["team_1", "team_2", "team_3"]) { let sPBS = 0, sPPBS = 0, potentialOutput = 0; for (let i = 0; i < COMPONENT_STATE_VALUES.statusReportData[team].players.length; i++) { sPBS += Number(COMPONENT_STATE_VALUES.statusReportData[team].players[i].PBS.currentScore); sPPBS += Number(COMPONENT_STATE_VALUES.statusReportData[team].players[i].PBS.potentialScore); potentialOutput += COMPONENT_STATE_VALUES.statusReportData[team].players[i].PBS.energyOutput; } COMPONENT_STATE_VALUES.statusReportData[team].PBS = sPBS.toFixed(2); COMPONENT_STATE_VALUES.statusReportData[team].PPBS = sPPBS.toFixed(2); COMPONENT_STATE_VALUES.statusReportData[team].potentialOutput = potentialOutput; COMPONENT_STATE_VALUES.statusReportData[team].players = COMPONENT_STATE_VALUES.statusReportData[team].players.sort((a, b) => a.score - b.score).reverse(); } StatusReportModal.refreshElement(); }, 2500) } //These functions are attached to the window so listeners have access to them (Tampermonkey quirk) window.switchActivePanel = (panel) => { COMPONENT_STATE_VALUES.options.activePanel = panel; ListingOrSettings.refreshElement(); Settings.hardRefreshElement(); Listing.hardRefreshElement(); } window.switchActiveRegion = (region) => { COMPONENT_STATE_VALUES.options.activeRegion = region; Settings.refreshElement(); } window.toggleMode = (mode) => { COMPONENT_STATE_VALUES.options.modes[mode] = !COMPONENT_STATE_VALUES.options.modes[mode]; console.log(COMPONENT_STATE_VALUES.options) Settings.refreshElement(); } window.closeStatusReport = () => { COMPONENT_STATE_VALUES.statusReportActive = false; clearInterval(STATUS_TIMER); STATUS_TIMER = null; StatusReportModal.hardRefreshElement(); } document.querySelector('#play').addEventListener('click', () => { clearInterval(API_TIMER); clearInterval(STATUS_TIMER); SL_INTEGRATION.style.display = 'none'; }); //WARNING: refreshSL is a SLOW function. Use it only when absolutely neccessary. const refreshSL = () => { SL_INTEGRATION.innerHTML = ` ${StatusReportModal.getElement()} ${TitleAndCredits.getElement()} ${ListingOrSettings.getElement()} ${Listing.getElement()} ${Settings.getElement()} ` } //COMPONENTS GO BELOW let ListingOrSettings = new Component("SL_OPTIONS_WRAPPER", () => `
LISTING
SETTINGS
`) let TitleAndCredits = new Component("TitleAndCredits", () => `
Starblast AUI v${CURRENT_RUNNING_VERSION}
API (Serverlist+): dankdmitron
Design and integration: Halcyon
`) let Settings = new Component("SLSettings", () => `
REGION:
Europe
${ COMPONENT_STATE_VALUES.options.activeRegion == "europe" ? `` : `` }
America
${ COMPONENT_STATE_VALUES.options.activeRegion == "america" ? `` : `` }
Asia
${ COMPONENT_STATE_VALUES.options.activeRegion == "asia" ? `` : `` }
MODE:
Team Mode
${ COMPONENT_STATE_VALUES.options.modes.team ? `` : `` }
Survival
${ COMPONENT_STATE_VALUES.options.modes.survival ? `` : `` }
Deathmatch
${ COMPONENT_STATE_VALUES.options.modes.deathmatch ? `` : `` }
Modded
${ COMPONENT_STATE_VALUES.options.modes.modding ? `` : `` }
Invasion
${ COMPONENT_STATE_VALUES.options.modes.invasion ? `` : `` }
`) let Listing = new Component("ServerListing", () => `
${ COMPONENT_STATE_VALUES.filteredSystems.length === 0 ? "" : COMPONENT_STATE_VALUES.filteredSystems.map(system => { return `
${system.name}
${system.mode === 'modding' ? capitalize(system.mod_id) : capitalize(system.mode)}
${~~(system.time / 60)} min
${system.players} players
` }).join('') }
`) let StatusReportModal = new Component("StatusReportModal", () => `
${COMPONENT_STATE_VALUES.statusReportData.name}
JOIN
${ (() => { let arr = []; for (let CUR_TEAM of ["team_1", "team_2", "team_3"]) { arr.push(`
${hueToColorName(COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].hue)}
Player count
${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].players.length}
Gems
${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].gems}
Level
${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].level}
Playerbase strength score
${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].PBS}
Potential playerbase strength score
${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].PPBS}
Maximum damage output (DPS)
${COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].potentialOutput}
ECP | NAME
SCORE | SHIP
${ COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].players.length === 0 ? "" : COMPONENT_STATE_VALUES.statusReportData[CUR_TEAM].players.map(player => { return `
${player.name}
${player.score} 
` }).join('') }
${CUR_TEAM !== "team_3" ? `
` : ""}`) } return arr.join('') })() }
`) //FUNCTIONS NOT RELEVANT TO MANIPULATING THE DOM GO BELOW const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1); const hueToColorName = (hue) => { const colorMap = [ { hueRange: [0, 15], colorName: 'Red' }, { hueRange: [15, 45], colorName: 'Orange' }, { hueRange: [45, 75], colorName: 'Yellow' }, { hueRange: [75, 150], colorName: 'Green' }, { hueRange: [150, 195], colorName: 'Cyan' }, { hueRange: [195, 285], colorName: 'Blue' }, { hueRange: [285, 330], colorName: 'Magenta' }, { hueRange: [330, 360], colorName: 'Red' } ]; const matchedColor = colorMap.find(entry => hue >= entry.hueRange[0] && hue < entry.hueRange[1]); return matchedColor ? matchedColor.colorName : 'Undefined'; } const POTENTIAL = { // ECP NON-ECP odyssey: [2.5, 1.5], x3: [1.7, 1 ], bastion: [1.4, 0.4], aries: [1 , 0.3], barracuda: [1.75, 0.3], } const buildItem = (ecp, nonecp, eregen, potential) => ({ecp, nonecp, eregen, potential}); const SHIP_TABLE = { // The numbers on this table need massive improvement /** * ECP - Points when player is ecp * NONECP - Obvious * POTENTIAL - Highest points the player can achieve with current path (e.g 2.5 for an ecp playing furystar) * EREGEN - Obvious */ // ECP NON-ECP E-REGEN POTENTIAL // Tier 7 "701": buildItem(2.5 , 1.5, 150, POTENTIAL.odyssey), "702": buildItem(1.7 , 1 , 50 , POTENTIAL.x3), "703": buildItem(1.4 , 0.4, 100, POTENTIAL.bastion), "704": buildItem(1 , 0.3, 175, POTENTIAL.aries), // Tier 6 "601": buildItem(1.2 , 0.5, 60 , POTENTIAL.odyssey), "602": buildItem(1.2 , 0.45, 50 , POTENTIAL.odyssey), "603": buildItem(0.9 , 0.25, 40 , POTENTIAL.x3), "604": buildItem(0.5 , 0.25, 48 , POTENTIAL.x3), "605": buildItem(0.9 , 0.25, 45 , POTENTIAL.x3), "606": buildItem(0.9 , 0.2 , 45 , POTENTIAL.x3), "607": buildItem(1.75, 0.3 , 0 , POTENTIAL.barracuda), "608": buildItem(0.5 , 0.2 , 40 , POTENTIAL.bastion), // Tier 5 "501": buildItem(1.05, 0.45, 60 , POTENTIAL.odyssey), "502": buildItem(0.75, 0.3, 40 , POTENTIAL.odyssey), "503": buildItem(0.2 , 0.1, 50 , POTENTIAL.x3), "504": buildItem(0.3 , 0.15, 45 , POTENTIAL.x3), "505": buildItem(0.1 , 0.05, 29 , POTENTIAL.x3), "506": buildItem(0.9 , 0.5, 50 , POTENTIAL.barracuda), "507": buildItem(0.15, 0.1, 35 , POTENTIAL.barracuda), // Tier 4 "401": buildItem(0.3 , 0.05 , 35 , POTENTIAL.odyssey), "402": buildItem(0.55, 0.25 , 50 , POTENTIAL.odyssey), "403": buildItem(0.4 , 0.2 , 55 , POTENTIAL.x3), "404": buildItem(0.3 , 0.15 , 40 , POTENTIAL.x3), "405": buildItem(0.3 , 0.1 , 30 , POTENTIAL.x3), "406": buildItem(0.05, 0 , 25 , POTENTIAL.barracuda), // Tier 3 "301": buildItem(0.2 , 0.07 , 30 , POTENTIAL.odyssey), "302": buildItem(0.17, 0.05 , 35 , POTENTIAL.x3), "303": buildItem(0.05, 0 , 16 , POTENTIAL.x3), "304": buildItem(0.15, 0.05 , 25 , POTENTIAL.barracuda), // Tier 2 "201": buildItem(0.05, 0 , 25 , POTENTIAL.odyssey), "202": buildItem(0.02, 0 , 20 , POTENTIAL.x3), // Fly "101": buildItem(0 , 0 , 10 , [0,0]), } const calculatePlayerScore = (type, ecp) => { return { currentScore: (Number(String(type).split('')[0]) / 15) + SHIP_TABLE[String(type)][ecp ? "ecp" : "nonecp"], potentialScore: (7 / 15) + SHIP_TABLE[String(type)]["potential"][+!ecp], // +!ecp is: First ecp is converted into a boolean and then inverted using !, then turned into a number using +, resulting in index 0 for ecp=true energyOutput: SHIP_TABLE[String(type)]["eregen"] } } const SHIP_LINKS = [ "https://i.ibb.co/6gjB0Y9/504.png", "https://i.ibb.co/h1BWddj/505.png", "https://i.ibb.co/ZG2wQtk/506.png", "https://i.ibb.co/ZxY43kc/507.png", "https://i.ibb.co/f8zzwcS/601.png", "https://i.ibb.co/hXgqvHQ/602.png", "https://i.ibb.co/HxNmSPY/603.png", "https://i.ibb.co/DVZrPT7/604.png", "https://i.ibb.co/w6jZfmK/605.png", "https://i.ibb.co/p4qBj2k/606.png", "https://i.ibb.co/4fjJcBC/607.png", "https://i.ibb.co/wYMGzCs/608.png", "https://i.ibb.co/ZNmcHfC/701.png", "https://i.ibb.co/JWZFqVv/702.png", "https://i.ibb.co/X2w682R/703.png", "https://i.ibb.co/RQrfMGW/704.png", "https://i.ibb.co/s3YVpVW/101.png", "https://i.ibb.co/w7GFPR5/201.png", "https://i.ibb.co/4JsJz8G/202.png", "https://i.ibb.co/Pz0xp1s/301.png", "https://i.ibb.co/M7PWNz7/302.png", "https://i.ibb.co/4ZKStWk/303.png", "https://i.ibb.co/df72XT8/304.png", "https://i.ibb.co/VM2kJgD/401.png", "https://i.ibb.co/8g6qgBw/402.png", "https://i.ibb.co/HnqK41P/403.png", "https://i.ibb.co/s2grnKB/404.png", "https://i.ibb.co/cvj9FWz/405.png", "https://i.ibb.co/64fsKPt/406.png", "https://i.ibb.co/27fLBPx/501.png", "https://i.ibb.co/3SfYGZX/502.png", "https://i.ibb.co/9pJt735/503.png" ] //This runs the SL integration. Do not touch refreshSL();