Finished refactoring the overlay to be Object Oriented

Also fixed bug with font not loading
This commit is contained in:
SwingTheVine 2025-07-27 02:37:25 -04:00
parent 08eae5c094
commit c1559340c7
10 changed files with 431 additions and 285 deletions

View file

@ -1 +1 @@
#bm-overlay{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000}div#bm-overlay{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-bar-drag{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-bar-drag.dragging{cursor:grabbing}#bm-contain-header{margin-bottom:.5em}#bm-overlay img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle}#bm-overlay h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-contain-automation input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-contain-automation label{margin-right:.5ch}.bm-help{border:white 1px solid;height:1.25em;width:1.25em;margin-top:2px;text-align:center;line-height:1.25em;padding:0!important}#bm-button-coords{vertical-align:middle}#bm-button-coords svg{width:50%;margin:0 auto;fill:#111}#bm-contain-coords input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-contain-coords input[type=number]::-webkit-outer-spin-button,#bm-contain-coords input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-contain-buttons{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-input-file)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-output-status{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-contain-userinfo,#bm-contain-automation,#bm-contain-coords,#bm-contain-buttons,div:has(>#bm-input-file),#bm-output-status{margin-top:.5em}#bm-overlay button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-overlay button:hover,#bm-overlay button:focus-visible{background-color:#1061e5}#bm-overlay button:active,#bm-overlay button:disabled{background-color:#2e97ff}#bm-overlay button:disabled{text-decoration:line-through}
#bm-overlay{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000}div#bm-overlay{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-bar-drag{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-bar-drag.dragging{cursor:grabbing}#bm-contain-header{margin-bottom:.5em}#bm-overlay img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle}#bm-overlay h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-contain-automation input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-contain-automation label{margin-right:.5ch}.bm-help{border:white 1px solid;height:1.25em;width:1.25em;margin-top:2px;text-align:center;line-height:1.25em;padding:0!important}#bm-button-coords{vertical-align:middle}#bm-button-coords svg{width:50%;margin:0 auto;fill:#111}#bm-contain-coords input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-contain-coords input[type=number]::-webkit-outer-spin-button,#bm-contain-coords input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-contain-buttons{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-input-file-template)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-output-status{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-contain-userinfo,#bm-contain-automation,#bm-contain-coords,#bm-contain-buttons,div:has(>#bm-input-file-template),#bm-output-status{margin-top:.5em}#bm-overlay button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-overlay button:hover,#bm-overlay button:focus-visible{background-color:#1061e5}#bm-overlay button:active,#bm-overlay button:disabled{background-color:#2e97ff}#bm-overlay button:disabled{text-decoration:line-through}

File diff suppressed because one or more lines are too long

View file

@ -35,7 +35,7 @@
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/LICENSE.txt" target="_blank"><img alt="Software License: MPL-2.0" src="https://img.shields.io/badge/Software_License-MPL--2.0-brightgreen?style=flat"></a>
<a href="https://discord.gg/tpeBPy46hf" target="_blank"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="" target="_blank"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-10hrs_0mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-114-black?style=flat"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-148-black?style=flat"></a>
<a href="" target="_blank"><img alt="Total Lines of Code" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=code"></a>
<a href="" target="_blank"><img alt="Total Comments" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=comments"></a>
<a href="" target="_blank"><img alt="Build" src="https://github.com/SwingTheVine/Wplace-BlueMarble/actions/workflows/build.yml/badge.svg"></a>

4
package-lock.json generated
View file

@ -7,7 +7,7 @@
"devDependencies": {
"esbuild": "^0.25.0"
},
"version": "0.43.6"
"version": "0.43.40"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
@ -467,5 +467,5 @@
}
}
},
"version": "0.43.6"
"version": "0.43.40"
}

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.43.6",
"version": "0.43.40",
"type": "module",
"scripts": {
"build": "node build/build.js",

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.43.6
// @version 0.43.40
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0

View file

@ -40,6 +40,15 @@ export class ApiHandler {
switch (endpointText) {
case 'me': // Request to retrieve user data
// If the game can not retrieve the userdata...
if (data.jsonData?.status && data.jsonData?.status?.toString()[0] != '2') {
// The server is probably down (NOT a 2xx status)
overlay.handleDisplayError(`The game is down!\nCould not fetch userdata.`);
return; // Kills itself before attempting to display null userdata
}
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(data.jsonData?.level) * Math.pow(30, 0.65), (1/0.65)) - data.jsonData?.pixelsPainted); // Calculates pixels to the next level
overlay.updateInnerHTML('bm-user-name', `Username: <b>${data.jsonData?.name}</b>`); // Updates the text content of the username field

View file

@ -70,7 +70,10 @@ var stylesheetLink = document.createElement('link');
stylesheetLink.href = 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap';
stylesheetLink.rel = 'preload';
stylesheetLink.as = 'style';
stylesheetLink.onload = "this.onload=null;this.rel='stylesheet'";
stylesheetLink.onload = function () {
this.onload = null;
this.rel = 'stylesheet';
};
document.head.appendChild(stylesheetLink);
const observers = new Observers(); // Constructs a new Observers object
@ -81,12 +84,63 @@ const apiHandler = new ApiHandler(coordsHandler); // Constructs a new ApiHandler
overlay.setApiHandler(apiHandler); // Sets the API handler
// Deploys the overlay to the page
// Parent/child relationships in the DOM structure below are indicated by indentation
overlay.addDiv({'id': 'bm-overlay', 'style': 'top: 10px; right: 75px;'})
.addDiv({'id': 'bm-contain-header'})
.addDiv({'id': 'bm-bar-drag'}).buildElement()
.addImg({'alt': 'Blue Marble Icon', 'src': 'https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/src/assets/Favicon.png'}).buildElement()
.addHeader(1, {'textContent': name}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'id': 'bm-contain-userinfo'})
.addP({'id': 'bm-user-name', 'textContent': 'Username:'}).buildElement()
.addP({'id': 'bm-user-droplets', 'textContent': 'Droplets:'}).buildElement()
.addP({'id': 'bm-user-nextlevel', 'textContent': 'Next level in...'}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'id': 'bm-contain-automation'})
.addCheckbox({'id': 'bm-input-stealth', 'textContent': 'Stealth', 'checked': true}).buildElement()
.addButtonHelp({'title': 'Waits for the website to make requests, instead of sending requests.'}).buildElement()
.addBr().buildElement()
.addCheckbox({'id': 'bm-input-possessed', 'textContent': 'Possessed', 'checked': true}).buildElement()
.addButtonHelp({'title': 'Controls the website as if it were possessed.'}).buildElement()
.addBr().buildElement()
.addDiv({'id': 'bm-contain-coords'})
.addButton({'id': 'bm-button-coords', 'className': 'bm-help', 'style': 'margin-top: 0;', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 6"><circle cx="2" cy="2" r="2"></circle><path d="M2 6 L3.7 3 L0.3 3 Z"></path><circle cx="2" cy="2" r="0.7" fill="white"></circle></svg></svg>'},
(instance, button) => {
button.onclick = () => {
const coords = instance.apiHandler?.coordsTilePixel; // Retrieves the coords from the API handler
if (!coords?.[0]) {
instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?');
return;
}
instance.updateInnerHTML('bm-input-tx', coords?.[0] || '');
instance.updateInnerHTML('bm-input-ty', coords?.[1] || '');
instance.updateInnerHTML('bm-input-px', coords?.[2] || '');
instance.updateInnerHTML('bm-input-py', coords?.[3] || '');
}
}
).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-ty', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-px', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-py', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.buildElement()
.addInputFile({'id': 'bm-input-file-template', 'textContent': 'Upload Template'}).buildElement()
.addDiv({'id': 'bm-contain-buttons'})
.addButton({'id': 'bm-button-enable', 'textContent': 'Enable'}).buildElement()
.addButton({'id': 'bm-button-disable', 'textContent': 'Disable'}).buildElement()
.buildElement()
.addTextarea({'id': overlay.outputStatusId, 'placeholder': `Status: Sleeping...\nVersion: ${version}`, 'readOnly': true}).buildElement()
.buildElement()
.buildOverlay(document.body);
overlay.handleDrag('#bm-overlay', '#bm-bar-drag'); // Creates dragging capability on the drag bar for dragging the overlay
apiHandler.spontaneousResponseListener(overlay); // Reads spontaneous fetch responces
console.log(`${name} (${version}) userscript has loaded!`);

View file

@ -123,7 +123,7 @@ div#bm-overlay {
}
/* The template file upload button */
div:has(> #bm-input-file) > button {
div:has(> #bm-input-file-template) > button {
width: 100%;
white-space: nowrap;
overflow: hidden;
@ -144,7 +144,7 @@ div:has(> #bm-input-file) > button {
#bm-contain-automation,
#bm-contain-coords,
#bm-contain-buttons,
div:has(> #bm-input-file),
div:has(> #bm-input-file-template),
#bm-output-status {
margin-top: 0.5em;
}

View file

@ -1,6 +1,21 @@
/** The overlay for the Blue Marble script.
/** The overlay builder for the Blue Marble script.
* @description This class handles the overlay UI for the Blue Marble script.
* @since 0.0.2
* @example
* const overlay = new Overlay();
* overlay.addDiv('overlay')
* .addHeader(1, {'textContent': 'Your Overlay'}).buildElement()
* .addP({'textContent': 'This is your overlay. It is versatile.'}).buildElement()
* .addHr().buildElement()
* .buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <div id="overlay">
* <h1>Your Overlay</h1>
* <p>This is your overlay. It is versatile.</p>
* </div>
* </body>
*/
export class Overlay {
@ -30,13 +45,13 @@ export class Overlay {
setApiHandler(apiHandler) {this.apiHandler = apiHandler;}
/** Creates an element.
* For internal use of the {@link Overlay} class
* For **internal use** of the {@link Overlay} class.
* @param {string} tag - The tag name as a string.
* @param {Object.<string, any>} [properties={}] - The DOM properties of the element.
* @returns {HTMLElement} HTML Element
* @since 0.43.2
*/
createElement(tag, properties = {}, additionalProperties={}) {
#createElement(tag, properties = {}, additionalProperties={}) {
const element = document.createElement(tag); // Creates the element
@ -71,11 +86,11 @@ export class Overlay {
* @example
* overlay
* .addDiv()
* .addHeader1().buildElement() // Breaks out of the <h1>
* .addParagraph().buildElement() // Breaks out of the <p>
* .addHeader(1).buildElement() // Breaks out of the <h1>
* .addP().buildElement() // Breaks out of the <p>
* .buildElement() // Breaks out of the <div>
* .addHr() // Since there are no more elements, calling buildElement() is optional
* .buildOverlay();
* .buildOverlay(document.body);
*/
buildElement() {
if (this.parentStack.length > 0) {
@ -91,33 +106,67 @@ export class Overlay {
* @example
* overlay
* .addDiv()
* .addParagraph().buildElement()
* .addP().buildElement()
* .buildElement()
* .buildOverlay(document.body); // Adds DOM structure to document body
* // <div><p></p></div>
*/
buildOverlay(parent) {
parent.appendChild(this.overlay);
// Resets the class-bound variables of this class instance back to default so overlay can be build again later
this.overlay = null;
this.currentParent = null;
this.parentStack = [];
}
/** Adds a `<div>` to the overlay.
* This `<div>` element will have properties shared between all `<div>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<div>` that are NOT shared between all overlay `<div>` elements. These should be camelCase.
* @param {function(HTMLElement):void} [callback=()=>{}] - Additional modification to the div.
* @param {function(Overlay, HTMLDivElement):void} [callback=()=>{}] - Additional JS modification to the `<div>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.2
* @example
* // Assume all <div> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addDiv({'id': 'foo'}).buildOverlay();
* // <div id="foo" class="bar"></div>
* overlay.addDiv({'id': 'foo'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <div id="foo" class="bar"></div>
* </body>
*/
addDiv(additionalProperties = {}, callback = () => {}) {
const properties = {}; // Shared <div> DOM properties
const div = this.createElement('div', properties, additionalProperties); // Creates the div element
callback(div); // Runs (optionally) passed in script
const div = this.#createElement('div', properties, additionalProperties); // Creates the <div> element
callback(this, div); // Runs any script passed in through the callback
return this;
}
/** Adds a `<p>` to the overlay.
* This `<p>` element will have properties shared between all `<p>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<p>` that are NOT shared between all overlay `<p>` elements. These should be camelCase.
* @param {function(Overlay, HTMLParagraphElement):void} [callback=()=>{}] - Additional JS modification to the `<p>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.2
* @example
* // Assume all <p> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addP({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <p id="foo" class="bar">Foobar.</p>
* </body>
*/
addP(additionalProperties = {}, callback = () => {}) {
const properties = {}; // Shared <p> DOM properties
const p = this.#createElement('p', properties, additionalProperties); // Creates the <p> element
callback(this, p); // Runs any script passed in through the callback
return this;
}
@ -125,157 +174,306 @@ export class Overlay {
* This `<img>` element will have properties shared between all `<img>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<img>` that are NOT shared between all overlay `<img>` elements. These should be camelCase.
* @param {function(HTMLElement):void} [callback=()=>{}] - Additional modification to the img.
* @param {function(Overlay, HTMLImageElement):void} [callback=()=>{}] - Additional JS modification to the `<img>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.2
* @example
* // Assume all <img> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addimg({'id': 'foo'}).buildOverlay();
* // <img id="foo" class="bar">
* overlay.addimg({'id': 'foo', 'src': './img.png'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <img id="foo" src="./img.png" class="bar">
* </body>
*/
addImg(additionalProperties = {}, callback = () => {}) {
const properties = {}; // Shared <div> DOM properties
const properties = {}; // Shared <img> DOM properties
const img = this.createElement('img', properties, additionalProperties); // Creates the div element
callback(img); // Runs (optionally) passed in script
const img = this.#createElement('img', properties, additionalProperties); // Creates the <img> element
callback(this, img); // Runs any script passed in through the callback
return this;
}
/** Creates and deploys the overlay element
* @since 0.0.2
/** Adds a header to the overlay.
* This header element will have properties shared between all header elements in the overlay.
* You can override the shared properties by using a callback.
* @param {number} level - The header level. Must be between 1 and 6 (inclusive)
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the header that are NOT shared between all overlay header elements. These should be camelCase.
* @param {function(Overlay, HTMLHeadingElement):void} [callback=()=>{}] - Additional JS modification to the header.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.7
* @example
* // Assume all header elements have a shared class (e.g. {'className': 'bar'})
* overlay.addHeader(6, {'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <h6 id="foo" class="bar">Foobar.</h6>
* </body>
*/
create() {
addHeader(level, additionalProperties = {}, callback = () => {}) {
const overlay = document.createElement('div'); // Creates a new <div> element for the overlay
overlay.id = 'bm-overlay';
overlay.style.top = '10px'; // Position from top of viewport
overlay.style.right = '75px'; // Position from right of viewport
const properties = {}; // Shared header DOM properties
const containerOverlayHeader = document.createElement('div');
containerOverlayHeader.id = 'bm-contain-header';
const header = this.#createElement('h' + level, properties, additionalProperties); // Creates the header element
callback(this, header); // Runs any script passed in through the callback
return this;
}
const barDrag = document.createElement('div'); // Drag bar for the overlay
barDrag.id = 'bm-bar-drag';
containerOverlayHeader.appendChild(barDrag); // Adds the drag bar to the overlay header container
/** Adds a `<hr>` to the overlay.
* This `<hr>` element will have properties shared between all `<hr>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<hr>` that are NOT shared between all overlay `<hr>` elements. These should be camelCase.
* @param {function(Overlay, HTMLHRElement):void} [callback=()=>{}] - Additional JS modification to the `<hr>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.7
* @example
* // Assume all <hr> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addhr({'id': 'foo'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <hr id="foo" class="bar">
* </body>
*/
addHr(additionalProperties = {}, callback = () => {}) {
const barHeaderImage = document.createElement('img'); // Image in header
barHeaderImage.src = 'https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/src/assets/Favicon.png';
barHeaderImage.alt = 'Blue Marble Icon';
containerOverlayHeader.appendChild(barHeaderImage); // Adds the header image to the overlay header container
const properties = {}; // Shared <hr> DOM properties
const barHeader = document.createElement('h1'); // Header bar for the overlay
barHeader.textContent = this.name;
containerOverlayHeader.appendChild(barHeader); // Adds the header to the overlay header container
const hr = this.#createElement('hr', properties, additionalProperties); // Creates the <hr> element
callback(this, hr); // Runs any script passed in through the callback
return this;
}
const containerUserInfo = document.createElement('div'); // User info container
containerUserInfo.id = 'bm-contain-userinfo';
/** Adds a `<br>` to the overlay.
* This `<br>` element will have properties shared between all `<br>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<br>` that are NOT shared between all overlay `<br>` elements. These should be camelCase.
* @param {function(Overlay, HTMLBRElement):void} [callback=()=>{}] - Additional JS modification to the `<br>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.11
* @example
* // Assume all <br> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addbr({'id': 'foo'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <br id="foo" class="bar">
* </body>
*/
addBr(additionalProperties = {}, callback = () => {}) {
const userName = document.createElement('p'); // User name field
userName.id = 'bm-user-name';
userName.textContent = 'Username:';
containerUserInfo.appendChild(userName); // Adds the username field to the user info container
const properties = {}; // Shared <br> DOM properties
const userDroplets = document.createElement('p'); // User droplet field
userDroplets.id = 'bm-user-droplets';
userDroplets.textContent = 'Droplets:';
containerUserInfo.appendChild(userDroplets); // Adds the droplet field to the user info container
const br = this.#createElement('br', properties, additionalProperties); // Creates the <br> element
callback(this, br); // Runs any script passed in through the callback
return this;
}
const userNextLevel = document.createElement('p'); // Amount to next level
userNextLevel.id = 'bm-user-nextlevel';
userNextLevel.textContent = 'Next level in...';
containerUserInfo.appendChild(userNextLevel); // Adds the "amount to next level" field to the user info container
/** Adds a checkbox to the overlay.
* This checkbox element will have properties shared between all checkbox elements in the overlay.
* You can override the shared properties by using a callback. Note: the checkbox element is inside a label element.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the checkbox that are NOT shared between all overlay checkbox elements. These should be camelCase.
* @param {function(Overlay, HTMLLabelElement, HTMLInputElement):void} [callback=()=>{}] - Additional JS modification to the checkbox.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.10
* @example
* // Assume all checkbox elements have a shared class (e.g. {'className': 'bar'})
* overlay.addCheckbox({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <label>
* <input type="checkbox" id="foo" class="bar">
* "Foobar."
* </label>
* </body>
*/
addCheckbox(additionalProperties = {}, callback = () => {}) {
const containerAutomation = document.createElement('div'); // Automated stuff container
containerAutomation.id = 'bm-contain-automation';
const properties = {'type': 'checkbox'}; // Shared checkbox DOM properties
// Stealth Mode checkbox
containerAutomation.appendChild(this.createInputCheckbox(
'Stealth',
'bm-input-stealth',
true
));
// Adds the help icon for stealth mode
containerAutomation.appendChild(this.createButtonQuestion(
'bm-help-stealth',
'Help: Waits for the website to make requests, instead of sending requests.',
this.outputStatusId
));
const label = this.#createElement('label', {'textContent': additionalProperties['textContent'] ?? ''}); // Creates the label element
delete additionalProperties['textContent']; // Deletes 'textContent' DOM property before adding the properties to the checkbox
const checkbox = this.#createElement('input', properties, additionalProperties); // Creates the checkbox element
label.insertBefore(checkbox, label.firstChild); // Makes the checkbox the first child of the label (before the text content)
this.buildElement(); // Signifies that we are done adding children to the checkbox
callback(this, label, checkbox); // Runs any script passed in through the callback
return this;
}
/** Adds a `<button>` to the overlay.
* This `<button>` element will have properties shared between all `<button>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<button>` that are NOT shared between all overlay `<button>` elements. These should be camelCase.
* @param {function(Overlay, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the `<button>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.12
* @example
* // Assume all <button> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addButton({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <button id="foo" class="bar">Foobar.</button>
* </body>
*/
addButton(additionalProperties = {}, callback = () => {}) {
containerAutomation.appendChild(document.createElement('br')); // Line break
const properties = {}; // Shared <button> DOM properties
// Possessed Mode checkbox
containerAutomation.appendChild(this.createInputCheckbox(
'Possessed',
'bm-input-possessed',
true
));
const button = this.#createElement('button', properties, additionalProperties); // Creates the <button> element
callback(this, button); // Runs any script passed in through the callback
return this;
}
// Adds the help icon for possessed mode
containerAutomation.appendChild(this.createButtonQuestion(
'bm-help-possessed',
'Help: Controls the website as if it were possessed.',
this.outputStatusId
));
/** Adds a help button to the overlay. It will have a "?" icon unless overridden in callback.
* On click, the button will attempt to output the title to the output element (ID defined in Overlay constructor).
* This `<button>` element will have properties shared between all `<button>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<button>` that are NOT shared between all overlay `<button>` elements. These should be camelCase.
* @param {function(Overlay, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the `<button>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.12
* @example
* // Assume all help button elements have a shared class (e.g. {'className': 'bar'})
* overlay.addButtonHelp({'id': 'foo', 'title': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <button id="foo" class="bar" title="Help: Foobar.">?</button>
* </body>
* @example
* // Assume all help button elements have a shared class (e.g. {'className': 'bar'})
* overlay.addButtonHelp({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <button id="foo" class="bar" title="Help: Foobar.">?</button>
* </body>
*/
addButtonHelp(additionalProperties = {}, callback = () => {}) {
containerAutomation.appendChild(document.createElement('br')); // Line break
const tooltip = additionalProperties['title'] ?? additionalProperties['textContent'] ?? 'Help: No info'; // Retrieves the tooltip
const containerAutomationCoords = document.createElement('div'); // Coords container
containerAutomationCoords.id = 'bm-contain-coords';
// Makes sure the tooltip is stored in the title property
delete additionalProperties['textContent'];
additionalProperties['title'] = `Help: ${tooltip}`;
const buttonCoords = document.createElement('button');
buttonCoords.id = 'bm-button-coords';
buttonCoords.className = 'bm-help';
buttonCoords.style = 'margin-top: 0';
buttonCoords.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 6"><circle cx="2" cy="2" r="2"></circle><path d="M2 6 L3.7 3 L0.3 3 Z"></path><circle cx="2" cy="2" r="0.7" fill="white"></circle></svg></svg>';
buttonCoords.onclick = () => {
const coords = this.apiHandler?.coordsTilePixel; // Retrieves the coords from the API handler
if (!coords?.[0]) {
this.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?');
return;
// Shared help button DOM properties
const properties = {
'textContent': '?',
'className': 'bm-help',
'onclick': () => {
this.updateInnerHTML(this.outputStatusId, tooltip);
}
this.updateInnerHTML('bm-input-tx', coords?.[0] || '0000');
this.updateInnerHTML('bm-input-ty', coords?.[1] || '0000');
this.updateInnerHTML('bm-input-px', coords?.[2] || '000');
this.updateInnerHTML('bm-input-py', coords?.[3] || '000');
}
containerAutomationCoords.appendChild(buttonCoords); // Adds the coordinate button to the automation container
};
// Tile (x,y) and Pixel (x,y) input fields
containerAutomationCoords.appendChild(this.createInputNumber('bm-input-tx', 'Tl X', '', '0', '2047', '1'));
containerAutomationCoords.appendChild(this.createInputNumber('bm-input-ty', 'Tl Y', '', '0', '2047', '1'));
containerAutomationCoords.appendChild(this.createInputNumber('bm-input-px', 'Px X', '', '0', '999', '1'));
containerAutomationCoords.appendChild(this.createInputNumber('bm-input-py', 'Px Y', '', '0', '999', '1'));
const help = this.#createElement('button', properties, additionalProperties); // Creates the <button> element
callback(this, help); // Runs any script passed in through the callback
return this;
}
containerAutomation.appendChild(containerAutomationCoords); // Adds coord container to automation container
/** Adds a `<input>` to the overlay.
* This `<input>` element will have properties shared between all `<input>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<input>` that are NOT shared between all overlay `<input>` elements. These should be camelCase.
* @param {function(Overlay, HTMLInputElement):void} [callback=()=>{}] - Additional JS modification to the `<input>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.13
* @example
* // Assume all <input> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addInput({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <input id="foo" class="bar">Foobar.</input>
* </body>
*/
addInput(additionalProperties = {}, callback = () => {}) {
containerAutomation.appendChild(this.createInputFile('bm-input-file')); // Adds the file upload element to the automation container
const properties = {}; // Shared <input> DOM properties
const containerAutomationButtons = document.createElement('div'); // Button array for template
containerAutomationButtons.id = 'bm-contain-buttons';
const input = this.#createElement('input', properties, additionalProperties); // Creates the <input> element
callback(this, input); // Runs any script passed in through the callback
return this;
}
// Button array for template
containerAutomationButtons.appendChild(this.createButton('bm-button-enable', 'Enable'));
containerAutomationButtons.appendChild(this.createButton('bm-button-disable', 'Disable'));
/** Adds a file input to the overlay. This includes a container and a button.
* This input element will have properties shared between all file input elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the file input that are NOT shared between all overlay file input elements. These should be camelCase.
* @param {function(Overlay, HTMLDivElement, HTMLInputElement, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the file input.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.17
* @example
* // Assume all file input elements have a shared class (e.g. {'className': 'bar'})
* overlay.addInputFile({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <div>
* <input type="file" id="foo" class="bar" style="display: none"></input>
* <button>Foobar.</button>
* </div>
* </body>
*/
addInputFile(additionalProperties = {}, callback = () => {}) {
const properties = {'type': 'file', 'style': 'display: none;'}; // Shared file input DOM properties
const text = additionalProperties['textContent'] ?? ''; // Retrieves the text content
containerAutomation.appendChild(containerAutomationButtons); // Adds button container to automation container
delete additionalProperties['textContent']; // Deletes the text content before applying the additional properties to the file input
const outputStatus = document.createElement('textarea'); // Outputs script status
outputStatus.id = this.outputStatusId;
outputStatus.readOnly = true; // Read-only input field
outputStatus.placeholder = `Status: Sleeping...\nVersion: ${this.version}`; // Default text value
containerAutomation.appendChild(outputStatus);
const container = this.#createElement('div'); // Container for file input
const input = this.#createElement('input', properties, additionalProperties); // Creates the file input
this.buildElement(); // Signifies that we are done adding children to the file input
const button = this.#createElement('button', {'textContent': text});
this.buildElement(); // Signifies that we are done adding children to the button
this.buildElement(); // Signifies that we are done adding children to the container
// Construction of the overlay element
overlay.appendChild(containerOverlayHeader); // Adds the overlay header container to the overlay
overlay.appendChild(document.createElement('hr')); // Adds a horizontal line to the overlay
overlay.appendChild(containerUserInfo); // Adds the user info container to the overlay
overlay.appendChild(document.createElement('hr')); // Adds a horizontal line to the overlay
overlay.appendChild(containerAutomation); // Adds the automation stuff container to the overlay
document.body.appendChild(overlay); // Adds the overlay to the body of the webpage
button.addEventListener('click', () => {
input.click(); // Clicks the file input
});
this.handleDrag(overlay, barDrag); // Starts handling the drag functionality
// Changes the button text content (and trims the file name)
input.addEventListener('change', () => {
button.style.maxWidth = `${button.offsetWidth}px`;
if (input.files.length > 0) {
button.textContent = input.files[0].name;
} else {
button.textContent = text;
}
});
callback(this, container, input, button); // Runs any script passed in through the callback
return this;
}
/** Adds a `<textarea>` to the overlay.
* This `<textarea>` element will have properties shared between all `<textarea>` elements in the overlay.
* You can override the shared properties by using a callback.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `<textarea>` that are NOT shared between all overlay `<textarea>` elements. These should be camelCase.
* @param {function(Overlay, HTMLTextAreaElement):void} [callback=()=>{}] - Additional JS modification to the `<textarea>`.
* @returns {Overlay} Overlay class instance (this)
* @since 0.43.13
* @example
* // Assume all <textarea> elements have a shared class (e.g. {'className': 'bar'})
* overlay.addTextarea({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
* // Output:
* // (Assume <body> already exists in the webpage)
* <body>
* <textarea id="foo" class="bar">Foobar.</textarea>
* </body>
*/
addTextarea(additionalProperties = {}, callback = () => {}) {
const properties = {}; // Shared <textarea> DOM properties
const textarea = this.#createElement('textarea', properties, additionalProperties); // Creates the <textarea> element
callback(this, textarea); // Runs any script passed in through the callback
return this;
}
/** Updates the inner HTML of the element.
@ -288,7 +486,8 @@ export class Overlay {
*/
updateInnerHTML(id, html, doSafe=false) {
const element = document.getElementById(id); // Retrieve the element
const element = document.getElementById(id.replace(/^#/, '')); // Retrieve the element from the 'id' (removed the '#')
if (!element) {return;} // Kills itself if the element does not exist
// Input elements don't have innerHTML, so we modify the value attribute instead
@ -304,168 +503,51 @@ export class Overlay {
}
}
/** Creates a help icon.
* When clicked, it will populate the text content of the outputId element with the tooltip.
* On hover, it will generate a tooltip.
* @param {string} id - ID of the help icon
* @param {string} tooltip - Flavor message
* @param {string} outputId - ID of the element to populate the text content with
* @returns {HTMLButtonElement} HTML Button Element
* @since 0.25.5
*/
createButtonQuestion(id, tooltip, outputId) {
const buttonQuestion = this.createButton(id, '?'); // Creates a button
buttonQuestion.className = 'bm-help';
buttonQuestion.title = tooltip; // Tooltip on hover
buttonQuestion.onclick = () => {
this.updateInnerHTML(outputId, tooltip); // Update output element text with tooltip on click
}
return buttonQuestion;
}
/** Creates a button
* @param {string} id - The ID of the button
* @param {string} text - The text content of the button
* @param {boolean} [isEnabled] - (Optional) Is the button enabled? Default is true
* @returns {HTMLButtonElement} HTML Button Element
* @since 0.33.1
*/
createButton(id, text, isEnabled=true) {
const button = document.createElement('button');
button.id = id;
button.textContent = text;
button.disabled = !isEnabled;
return button;
}
/** Creates a text input field
* @param {string} inputId - The ID for the text input
* @param {string} [placeholder] - (Optional) The placeholder text of the input
* @param {string} [text] - (Optional) The text content of the input
* @param {string} [maxLength] - (Optional) The maximum character amount of the input
* @param {boolean} [isReadOnly] - (Optional) Should the input be readOnly? False by default
* @returns {HTMLInputElement} HTML Input Element
* @since 0.30.3
*/
createInputText(inputId, placeholder='', text='', maxLength='', isReadOnly=false) {
const input = document.createElement('input');
input.id = inputId;
input.type = 'text';
input.placeholder = placeholder;
input.value = text;
input.readOnly = isReadOnly;
input.maxLength = maxLength;
return input;
}
/** Creates a number input field
* @param {string} inputId - The ID for the number input
* @param {string} [placeholder] - (Optional) The placeholder text of the input
* @param {string} [text] - (Optional) The text content of the input
* @param {string} [numMin] - (Optional) The minimum possible number the user can input
* @param {string} [numMax] - (Optional) The maximum possible number the user can input
* @param {string} [numStep] - (Optional) The increment that numbers are considered valid inputs. E.g. a step of "1" will only allow integers
* @param {boolean} [isReadOnly] - (Optional) Should the input be readOnly? False by default
* @returns {HTMLInputElement} HTML Input Element
* @since 0.41.9
*/
createInputNumber(inputId, placeholder='', text='', numMin='', numMax='', numStep='', isReadOnly=false) {
const input = this.createInputText(inputId, placeholder, text, '', isReadOnly); // A shortcut :P
input.type = 'number';
input.min = numMin;
input.max = numMax;
input.step = numStep;
return input;
}
/** Creates the checkbox input
* @param {string} labelText - The text for the label
* @param {string} checkboxId - The ID for the checkbox input
* @param {boolean} [checkboxDefault] - (Optional) The default status of the checkbox (true = checked). False by default.
* @returns {HTMLLabelElement} HTML Label Element with Input child
* @since 0.27.1
*/
createInputCheckbox(labelText, checkboxId, checkboxDefault=false) {
const label = document.createElement('label');
label.textContent = labelText;
const input = document.createElement('input');
input.type = 'checkbox';
input.id = checkboxId;
input.checked = checkboxDefault;
label.prepend(input); // Adds the input as the first child of the label (before the text)
return label;
}
/** Creates a custom file upload input.
* Only the button is displayed, but interacting with it will simulate the file input
* @param {string} inputId - The file input element ID
* @returns {HTMLDivElement} HTML Div Element with Button and Input children
* @since 0.36.1
*/
createInputFile(inputId) {
const container = document.createElement('div');
const input = document.createElement('input');
input.id = inputId;
input.type = 'file';
input.style = 'display: none'; // Hides the file input
const button = document.createElement('button');
button.textContent = 'Upload File';
button.addEventListener('click', () => {
input.click(); // Clicks the file input
});
// Changes the button text content (and trims the file name)
input.addEventListener('change', () => {
button.style.maxWidth = `${button.offsetWidth}px`;
if (input.files.length > 0) {
button.textContent = input.files[0].name;
} else {
button.textContent = 'Upload File';
}
});
container.appendChild(input);
container.appendChild(button);
return container;
}
/** Handles dragging of the overlay.
* @param {HTMLElement} overlay - The overlay element to be moved.
* @param {HTMLElement} barDrag - The element that acts as the drag handle.
* @param {string} moveMe - The ID of the element to be moved
* @param {string} iMoveThings - The ID of the element to be moved
* @since 0.8.2
*/
handleDrag(overlay, barDrag) {
handleDrag(moveMe, iMoveThings) {
let isDragging = false;
let offsetX, offsetY = 0;
// What to do when the mouse is pressed down on the barDrag
barDrag.addEventListener('mousedown', function(event) {
// Retrieves the elements (allows either '#id' or 'id' to be passed in)
moveMe = document.querySelector(moveMe?.[0] == '#' ? moveMe : '#' + moveMe);
iMoveThings = document.querySelector(iMoveThings?.[0] == '#' ? iMoveThings : '#' + iMoveThings);
// What to do when one of the two elements are not found
if (!moveMe || !iMoveThings) {
this.handleDisplayError(`Can not drag! ${!moveMe ? 'moveMe' : ''} ${!moveMe && !iMoveThings ? 'and ' : ''}${!iMoveThings ? 'iMoveThings ' : ''}was not found!`);
return; // Kills itself
}
// What to do when the mouse is pressed down on the element that moves things
iMoveThings.addEventListener('mousedown', function(event) {
isDragging = true;
offsetX = event.clientX - overlay.getBoundingClientRect().left;
offsetY = event.clientY - overlay.getBoundingClientRect().top;
offsetX = event.clientX - moveMe.getBoundingClientRect().left;
offsetY = event.clientY - moveMe.getBoundingClientRect().top;
document.body.style.userSelect = 'none'; // Prevents text selection while dragging
barDrag.classList.add('dragging'); // Adds a class to indicate a dragging state
iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
});
// What to do when the touch starts on the barDrag
barDrag.addEventListener('touchstart', function(event) {
// What to do when the touch starts on the element that moves things
iMoveThings.addEventListener('touchstart', function(event) {
isDragging = true;
const touch = event?.touches?.[0];
if (!touch) {return;}
offsetX = touch.clientX - overlay.getBoundingClientRect().left; // Distance between the left edge of the overlay, and the cursor
offsetY = touch.clientY - overlay.getBoundingClientRect().top; // Distance between the top edge of the overlay, and the cursor
offsetX = touch.clientX - moveMe.getBoundingClientRect().left; // Distance between the left edge of the overlay, and the cursor
offsetY = touch.clientY - moveMe.getBoundingClientRect().top; // Distance between the top edge of the overlay, and the cursor
document.body.style.userSelect = 'none'; // Prevents text selection while dragging
barDrag.classList.add('dragging'); // Adds a class to indicate a dragging state
iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
}, { passive: false }); // Prevents scrolling from being captured
// What to do when the mouse is moved while dragging
document.addEventListener('mousemove', function(event) {
if (isDragging) {
overlay.style.left = (event.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor
overlay.style.top = (event.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor
overlay.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen
moveMe.style.left = (event.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor
moveMe.style.top = (event.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor
moveMe.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen
}
});
@ -474,8 +556,8 @@ export class Overlay {
if (isDragging) {
const touch = event?.touches?.[0];
if (!touch) {return;}
overlay.style.left = (touch.clientX - offsetX) + 'px';
overlay.style.top = (touch.clientY - offsetY) + 'px';
moveMe.style.left = (touch.clientX - offsetX) + 'px';
moveMe.style.top = (touch.clientY - offsetY) + 'px';
event.preventDefault(); // prevent scrolling while dragging
}
}, { passive: false }); // Prevents scrolling from being captured
@ -484,21 +566,21 @@ export class Overlay {
document.addEventListener('mouseup', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
barDrag.classList.remove('dragging'); // Removes the dragging class
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
// What to do when the touch ends
document.addEventListener('touchend', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
barDrag.classList.remove('dragging'); // Removes the dragging class
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
// What to do when the touch is cancelled
document.addEventListener('touchcancel', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
barDrag.classList.remove('dragging'); // Removes the dragging class
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
}