// ==UserScript== // @name SB-AUI // @namespace http://tampermonkey.net/ // @version 1.3.3 // @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://i.ibb.co/1QgnHfK/aui.png // @grant none // @downloadURL https://update.greasyfork.icu/scripts/472581/SB-AUI.user.js // @updateURL https://update.greasyfork.icu/scripts/472581/SB-AUI.meta.js // ==/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 * 1.2.2 - Tested component relationships (they work), added loading animations and some debugging * 1.2.3 - GreasyFork is begging me to update the version number * 1.2.4 - (Mateo) - Added user search * 1.2.5 - XSS prevention (username sanitization) and user search results update * 1.3.0 - Added prop components - Useful for building elements from a template. Uses custom syntax - ||variable||. Lists transition to prop components still WIP * 1.3.1 - Fixed bug regarding number 0 in prop components * 1.3.2 - Fixed returnCaret bug * 1.3.3 - Lousy fix of modded statusReport */ 'use strict'; const API_LINK = "https://starblast.dankdmitron.dev/api/simstatus.json"; const CURRENT_RUNNING_VERSION = "1.3.3" /********* 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; } .noglow-placeholder::placeholder { text-shadow: black 0px 0px 0px; } /* 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"] = { listingLoading: true, options: { activePanel: "listing", activeRegion: "europe", modes: { team: true, survival: false, deathmatch: false, modding: false, invasion: false } }, userSearch: { active: false, loading: false, input: "", results: {}, systemsQueried: 0, }, filteredSystems: [], statusReportActive: false, statusReportLoading: false, isUpdateAvailable: false, statusReportData: { name: "", id: "", mode: "", 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"}`)} } /** * Evaluates element with props object. The ID is not included in build elements and they cannot be refreshed * @param {Object} props - Props object is used to "build" elements. Its useful for displaying sets of data. A prop from the props object should be used inside the string between |||| tags like so: ||propName|| * @returns {innerHTML} */ buildElement(props = {}) { let processedHTML = this.evaluate(); processedHTML = processedHTML.replace(/\|\|([^|]+)\|\|/g, (match, variableName) => { if (!props.hasOwnProperty(variableName)) { console.error(`Component class error: ${variableName} not defined in props object`); return match } return props[variableName] ?? match; }); return processedHTML } /** * Evaluates the HTML * @returns {innerHTML} */ 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; } /** * Re-evaluated the HTML excluding the parent element */ 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)} } /** * Re-evaluates the HTML including the parent element */ 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; } if (COMPONENT_STATE_VALUES.userSearch.input) { return; } let raw = await(await fetch(API_LINK)).json(); COMPONENT_STATE_VALUES.listingLoading = false; 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); if (isFocused(document.querySelector('#user-search'))) { Listing.refreshElement(); returnCaret(); } else { Listing.refreshElement(); } }, 3200) const returnCaret = () => { //The three lines below are necessary because of refresh resetting the caret on input document.querySelector('#user-search').focus() document.querySelector('#user-search').value = ""; document.querySelector('#user-search').value = COMPONENT_STATE_VALUES.userSearch.input } const isFocused = (element) => document.activeElement === element; let STATUS_TIMER = null; window.statusReport = async (query) => { if (STATUS_TIMER) {return}; COMPONENT_STATE_VALUES.statusReportActive = true; COMPONENT_STATE_VALUES.statusReportLoading = true; StatusReportModal.hardRefreshElement(); STATUS_TIMER = setInterval(async () => { let raw = await (await fetch(`https://starblast.dankdmitron.dev/api/status/${query}`)).json(); if (COMPONENT_STATE_VALUES.statusReportLoading) { COMPONENT_STATE_VALUES.statusReportLoading = false; } COMPONENT_STATE_VALUES.statusReportData = templateStatusData(); COMPONENT_STATE_VALUES.statusReportData.name = raw.name; COMPONENT_STATE_VALUES.statusReportData.id = query.split('@')[0]; COMPONENT_STATE_VALUES.statusReportData.mode = raw.mode.id; 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; } try { 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 } } } } catch (ex) {console.log(ex)} 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(); } let USER_QUERY_TIMER = null window.handleSearch = () => { COMPONENT_STATE_VALUES.userSearch.input = document.querySelector("#user-search").value; clearTimeout(USER_QUERY_TIMER); if (!COMPONENT_STATE_VALUES.userSearch.input) { Listing.refreshElement(); return COMPONENT_STATE_VALUES.userSearch.loading = false; } if (!COMPONENT_STATE_VALUES.userSearch.loading) { COMPONENT_STATE_VALUES.userSearch.loading = true; Listing.refreshElement(); returnCaret(); } USER_QUERY_TIMER = setTimeout(async () => { //https://starblast.dankdmitron.dev/api/status/${query}` COMPONENT_STATE_VALUES.userSearch.systemsQueried = 0; let playersList = [], mostSimilar = []; for (let system of COMPONENT_STATE_VALUES.filteredSystems) { try { let query = `${system.id}@${system.IP_ADDR}` let raw = await (await fetch(`https://starblast.dankdmitron.dev/api/status/${query}`)).json() for (let key of Object.keys(raw.players)) { let player = raw.players[key] playersList.push({name: player.player_name, query: query}); } if (COMPONENT_STATE_VALUES.userSearch.systemsQueried < COMPONENT_STATE_VALUES.filteredSystems.length) { COMPONENT_STATE_VALUES.userSearch.systemsQueried++; } } catch (ex) {console.log(ex)} } for (let player of playersList) { let similarity = calculateSimilarity(player.name).toFixed(2) if (similarity > 25) { mostSimilar.push({ name: player.name, similarity: similarity, query: player.query }) } } COMPONENT_STATE_VALUES.userSearch.results = mostSimilar.sort((a, b) => a.similarity - b.similarity).reverse(); COMPONENT_STATE_VALUES.userSearch.loading = false; Listing.refreshElement(); returnCaret(); }, 300) } 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", () => `