Skip to content

Instantly share code, notes, and snippets.

@defensem3ch
Last active May 25, 2025 12:47
Show Gist options
  • Save defensem3ch/314fd6c9ae91e7309e5f11aa380965c3 to your computer and use it in GitHub Desktop.
Save defensem3ch/314fd6c9ae91e7309e5f11aa380965c3 to your computer and use it in GitHub Desktop.
BotB userscript
// ==UserScript==
// @name botb custom + donload grabber
// @author DEFENSE MECHANISM
// @version 0.4
// @namespace http://battleofthebits.com/
// @description Alternate layout and css for battleofthebits
// @updateURL https://github.com/defensem3ch/userscripts/raw/main/botb%20custom.user.js
// @downloadURL https://github.com/defensem3ch/userscripts/raw/main/botb%20custom.user.js
// @homepage https://defensemech.com
// @match https://battleofthebits.com/*
// @exclude https://battleofthebits.com/disk/*
// @exclude https://battleofthebits.com/battle/Tally/*
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM_setClipboard
// @grant GM_registerMenuCommand
// ==/UserScript==
//
// some background ideas:
// https://opengameart.com/sites/default/files/styles/medium/public/seamless%20space_0.PNG
// https://content.mycutegraphics.com/backgrounds/hearts/tiny-cute-hearts-on-black-background.gif
// https://battleofthebits.com/disk/debris/botb_bg.png
// https://vgmusic.com/images/background.jpg
//
// todo: maybe grab the showHide stuff and set an option to default to hidden
//
(async function() {
'use strict';
const profilePage = "https://battleofthebits.com/barracks/Profile/DefenseMechanism/"; // ^O^
// user settings
// okay this function sort of turned into a big deal
const defaultSettings = {
localdateColor: "inherit",
fiteColor: "inherit",
logoColor: "inherit",
localdateOn: "on",
botbdateOn: "on",
altlayoutOn: "on",
compactOn: "on",
burfsOff: "off",
bnadOff: "off",
entryactOff: "off",
battleactOff: "off",
actlogOff: "off",
recentbrOff: "off",
bgImage: "https://battleofthebits.com/disk/debris/botb_bg.png"
};
async function init() {
const uv = await getUserValues();
console.log("Settings loaded:", uv);
applyStyles(uv);
tryInsertSettingsPanel(uv);
applyCollapsibles();
}
async function getUserValues() {
const keys = Object.keys(defaultSettings);
const result = {};
for (let key of keys) {
result[key] = await GM.getValue(key, defaultSettings[key]);
}
return result;
}
function applyStyles(uv){
const matches = document.querySelectorAll("span.countdown"); // you can tell I did this first
var now = Date.now();
// let patreon = document.querySelector("[href='https://www.patreon.com/battleofthebits']");
let avatarLink = document.querySelector("#homeMenu > div > div > div.botbrAvatar > a");
// let bandcamp = document.querySelector("[href='http://battleofthebits.bandcamp.com']");
const sideSeperators = document.querySelectorAll("#SIDE_BOX > div.hSeperator");
const otherSideSeperators = document.querySelectorAll("#homeMenu > div > div > div.hMiniSeperator");
// patreon.id = 'patreonLink';
// bandcamp.id = 'bandcampLink';
let butts = document.querySelectorAll("div.menu_butt");
var frights = document.querySelectorAll("div.fright.alignR");
var iframeGuys = document.querySelectorAll("iframe");
var footGuys = document.querySelectorAll(".footerMenu");
var ultimoFoot = footGuys[footGuys.length - 1];
// make Recent Activity into the same kind of span as the other collapsible things
var recentAL = document.evaluate("//span[contains(text(), 'Recent Activity..')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (recentAL != null) {
recentAL.classList.add("tb2");
recentAL.innerText = "Activity Log"; // had to rename this to stop it from breaking
}
var teeTwos = document.querySelectorAll("div > div.inner.clearfix > span.t2, div > div.inner.clearfix > span.tb2"); // will this burn me if I leave the main page?
if (uv['localdateOn'] == "on") {
var jdate = "inline";
} else {
jdate = "none";
}
if (uv['botbdateOn'] == "on") {
var bdate = "inline";
} else {
bdate = "none";
}
// you better make that into a function before you do any more ^
// apparently I need this too oops
// https://nimishprabhu.com/convert-rgb-to-hex-and-hex-to-rgb-javascript-online-demo.html
function rgb2hex(r, g, b) {
try {
var rHex = parseInt(r).toString(16).padStart(2, '0');
var gHex = parseInt(g).toString(16).padStart(2, '0');
var bHex = parseInt(b).toString(16).padStart(2, '0');
} catch (e) {
return false;
}
if (rHex.length > 2 || gHex.length > 2 || bHex.length > 2) return false;
return '#' + rHex + gHex + bHex;
}
// Color lightenin' function
// https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors (nice)
function lightenDarkenColor(hex, amt) {
let col = hex[0] === '#' ? hex.slice(1) : hex;
let num = parseInt(col, 16);
let r = Math.min(255, Math.max(0, (num >> 16) + amt));
let g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + amt));
let b = Math.min(255, Math.max(0, (num & 0x0000FF) + amt));
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
// using the above
var defCol = window.getComputedStyle(document.body).getPropertyValue("color");
var defaultCol = defCol.slice(4, 17);
var defaultColA = defaultCol.trim().split(/\s*,\s*/);
defaultCol = rgb2hex(defaultColA[0], defaultColA[1], defaultColA[2]); // this is also one of the logo colors
var lighterCol = lightenDarkenColor(defaultCol, 30);
var darkerCol = lightenDarkenColor(defaultCol, -30);
// jeez this should be in the function I guess, oh well too late now
var lCol = window.getComputedStyle(document.querySelector("a")).getPropertyValue("color");
var defaultL = lCol.slice(4, 17);
var defaultLA = defaultL.trim().split(/\s*,\s*/);
var defaultLink = rgb2hex(defaultLA[0], defaultLA[1], defaultLA[2]); // I think this is a logo color too
if (uv['logoColor'] == "inherit") {
uv['logoColor'] = defaultCol;
}
if (uv['fiteColor'] == "inherit") {
uv['fiteColor'] = defaultLink;
}
var lightL = lightenDarkenColor(uv['logoColor'], 30);
var darkL = lightenDarkenColor(uv['logoColor'], -30);
var styles = `
@keyframes localmorph {
0% { color: #fc6ab5; }
25% { color: #fc6ae3; }
50% { color: #f36afc; }
75% { color: #fc6ae3; }
100% { color: #fc6ab5; }
}
@keyframes userglow {
0% { color: ${defaultCol}; }
25% { color: ${lighterCol};}
50% { color: ${defaultCol}; }
75% { color: ${darkerCol}; }
100% { color: ${defaultCol}; }
}
@keyframes logomorph {
0% { color: ${lightL}; }
25% { color: ${uv['logoColor']}; }
50% { color: ${darkL}; }
75% { color: ${uv['logoColor']}; }
100% { color: ${lightL}; }
}
.hSeperator.compact {
height: 15px;
}
.hMiniSeperator.compact {
height: 3px;
}
#localbeghast {
color: ${uv['localdateColor']};
font-weight: bold;
margin-top: 1rem;
}
#localsettings {
background-color: rgba(16,16,16,0.7);
color: #dbdbff;
text-align: center;
line-height: 120%;
}
#localsettings ul {
margin: auto;
width: 520px;
text-align: left;
}
.savemsg {
display: inline;
}
#localsettings hr {
margin: 3px auto;
}
#localsettings h2 {
margin: auto;
font-size: 2em;
line-height: 2.1em;
font-weight: normal;
}
.localdate {
color: ${uv['localdateColor']};
display: ${jdate};
font-weight: normal;
}
span.countdown {
display: ${bdate} !important;
}
body, #pageWrap {
background-image: url("${uv['bgImage']}");
}
.localblurb {
font-size: 1rem;
line-height: 1.3rem;
}
.logo, .logo2 {
animation: logomorph 4s linear infinite;
}
.compact .logo, .compact .logo2 {
height: 38px;
font-size: 49px;
}
.robo {
font-weight: bold;
animation: localmorph 5s infinite;
}
#patreonLink, #bandcampLink {
display: none;
}
a[href="${profilePage}"] {
animation: colorRotate 6s linear 0s infinite;
}
@keyframes colorRotate {
from {
color: #6666ff;
}
10% {
color: #0099ff;
}
50% {
color: #00ff00;
}
75% {
color: #ff3399;
}
100% {
color: #6666ff;
}
}
.toggler:hover {
animation: userglow 1s linear infinite;
cursor: pointer;
}
#heartofthesun {
margin: 0px auto;
width: 560px;
text-align: center;
}
#localsave, #localclear {
font-size: 120%;
padding: .3em;
margin: auto 2em;
width: 120px;
height: 60px;
}
.savemsg, .delmsg {
color: #ff4444;
font-weight: bold;
}
#fight {
color: ${uv['fiteColor']};
}
#footer.compact #footerMSG .footerMenu a {
padding: 1px 7px 0 0;
}
.colortest {
width: 20px;
height: 20px;
margin-left: 1rem;
display: inline-block;
vertical-align: top;
}
#MENU + .compact {
display:none;
}
#battleActBot.compact a + div.hMiniSeperator.compact {
display: none;
}
`
var styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
// replace "LOGOUT" with door emoji
let logout = document.querySelector("div.tb0 > a:nth-child(1)");
logout.innerHTML = "🚪";
console.log('should have replaced logout');
let majors = document.querySelector("div.ajaxContent");
console.log(majors);
let majorLinks = majors.querySelectorAll("a");
// Iterate through the links and modify the href
majorLinks.forEach(link => {
// Replace part of the href
link.href = link.href.replace("/Entries/", "/InfoRules/");
// Log the updated href to verify
console.log("Updated href:", link.href);
});
document.head.appendChild(styleSheet);
if (uv['compactOn'] == "on") {
var divs = document.querySelectorAll("div");
for (let d = 0; d < divs.length; d++) {
divs[d].classList.add('compact');
}
}
if (uv['altlayoutOn'] == "on") {
// hide the youtube song of the day
// and merch link
let youtubes = document.querySelector(".ajaxContent > div:nth-child(11)");
let merch = document.querySelector("#botb-gear-banner-container");
let sep1 = document.querySelector(".ajaxContent > div:nth-child(10)");
let sep2 = document.querySelector("div.hSeperator:nth-child(14)");
if (youtubes) {youtubes.style.display = "none";}
if (merch) {merch.style.display = "none";}
if (sep1) {sep1.style.display = "none";}
if (sep2) {sep2.style.display = "none";}
// reorganize the topbar
// this seems to have made some visual issues with the dropdown menus?
butts[0].innerHTML = butts[4].innerHTML; // Arena
var placeholder1 = butts[2].innerHTML; // not pictured: forum
var placeholder2 = butts[3].innerHTML; //
butts[2].innerHTML = butts[5].innerHTML; // Rax
butts[3].style.display = 'none' // butts[6].innerHTML; // Chats
butts[4].innerHTML = placeholder1; // Browser
butts[5].innerHTML = placeholder2; // Lyceum
butts[6].innerHTML = '<a class="tab boxLink hButt" id="fight" title="BEGAST" href="https://battleofthebits.com/arena/XHB_BEGHAST/">BEGAST!! </a>'; // BEGAST!
butts[7].innerHTML = butts[8].innerHTML; // Radio
butts[8].innerHTML = '<a class="tab boxLink hButt" title="Random Node Goto-er" href="/index/RNG">?</a>'; // random page
butts[8].insertAdjacentHTML('afterend', '<div class="menu_butt"><a class="tab boxLink hButt" title="Entry of the Day" href="' + youtubes + '">!</a></div>'); // Entry of the day
// reorganize the sidebar, but try to check to see if it even exists first
if (document.getElementById("homeMenu")) {
var sidebarGuys = document.querySelectorAll("#homeMenu > div > div > a");
avatarLink.href = "https://battleofthebits.com/barracks/EditProfile";
for (let h = 0; h < sidebarGuys.length; h++) {
if (sidebarGuys[h].title == 'Begast XHB' || sidebarGuys[h].title == 'EditProfile') {
sidebarGuys[h].style.display = 'none';
var aichplusone = h + 1;
otherSideSeperators[aichplusone].style.display = 'none';
}
}
}
// uhhhhh let's move this to the bottom
// ultimoFoot.innerHTML += '<a href="http://battleofthebits.bandcamp.com/">Bandcamp</a><a href="https://www.patreon.com/battleofthebits">Patreon</a>';
// hide some seperators... watch it, this might get ugly on subpages
sideSeperators[2].style.display = 'none';
sideSeperators[3].style.display = 'none';
// trying to save some vertical space by moving those small "hosted by" messages
var currentHosts = document.querySelectorAll("a.clearfix > div > span.tiny");
for (let cH = 0; cH < currentHosts.length; cH++) {
var desty = currentHosts[cH].parentNode.parentNode.querySelector('div[style="padding-left:72px;"]');
if (desty) {
desty.insertAdjacentHTML("beforeend", '<br><span class="t0">' + currentHosts[cH].innerHTML + '</span>');
currentHosts[cH].style.display = "none";
var lastbr = currentHosts[cH].parentNode.querySelector('.localdate + br');
if (lastbr != null) {
lastbr.style.display = "none";
}
}
}
} // end of the altlayout if ... copy-paste ruined the indent
// https://stackoverflow.com/questions/1573053/javascript-function-to-convert-color-names-to-hex-codes#1573141
function getHexColor(colorStr) {
var a = document.createElement('div');
a.style.color = colorStr;
var colors = window.getComputedStyle(document.body.appendChild(a)).color.match(/\d+/g).map(function(a) {
return parseInt(a, 10);
});
document.body.removeChild(a);
return (colors.length >= 3) ? '#' + (((1 << 24) + (colors[0] << 16) + (colors[1] << 8) + colors[2]).toString(16).substr(1)) : false;
}
function hexListener(fieldid) {
var target = document.getElementById(fieldid);
function replaceWithHex(target) {
if (target.value) {
console.log(target.value);
let out = getHexColor(target.value);
if (out != defaultCol) {
target.value = out;
}
}
}
target.addEventListener("blur", function() {
replaceWithHex(target);
var colorbox = target.parentNode.querySelector(".colortest")
colorbox.style.background = target.value;
});
}
} // this is the end of applyStyles
function tryInsertSettingsPanel(uv) {
if (
window.location.href === 'https://battleofthebits.com/barracks/Settings/' &&
!document.getElementById("localsettings")
) {
const settingsTarget = document.querySelector("#homeMenu > div.ajaxContent.grid_8 > div > div.inner.clearfix");
if (!settingsTarget) {
console.warn("Settings target not found");
return;
}
const settingsHTML = `
<hr>
<div id="localsettings">
<h2>localsettings</h2>
<p>Hiiii~ so these are <span class="robo tb1">secret extra settings</span> for my BotB userscript.</p>
<p>For colors, you can input either an HTML color name (chartreuse) or a hex code (#7fff00).</p>
<p>Consider <a href="https://www.paypal.com/donate?hosted_button_id=XNGKRVMEPMN36">donating</a> if you found this script useful or amusing ^O^</p>
<hr>
<h2>Background image</h2>
<p>Remember, this is only visible for you! Some ideas:</p>
<ul>
<li>https://battleofthebits.com/disk/debris/botb_bg.png</li>
<li>https://opengameart.com/sites/default/files/styles/medium/public/seamless%20space_0.PNG</li>
<li>https://content.mycutegraphics.com/backgrounds/hearts/tiny-cute-hearts-on-black-background.gif</li>
<li>https://vgmusic.com/images/background.jpg</li>
</ul>
<p><input type="url" id="bgImage" name="bgImage" size="40" placeholder="${uv['bgImage']}"></p>
<h2>Layout options</h2>
<p>Show normal botb countdowns? <input type="checkbox" id="botbdateOn" name="botbdateOn" placeholder="${uv['botbdateOn']} value="${uv['botbdateOn']}"></p>
<p>Show calculated date/times? <input type="checkbox" id="localdateOn" name="localdateOn" placeholder="${uv['localdateOn']} value="${uv['localdateOn']}"></p>
<p>Use alternate layout? <input type="checkbox" id="altlayoutOn" name="altlayout" placeholder="${uv['altlayoutOn']} value="${uv['altlayoutOn']}"></p>
<p>Use compact mode (slightly smaller)? <input type="checkbox" id="compactOn" name="compactOn" placeholder="${uv['compactOn']} value="${uv['compactOn']}"></p>
<h2>Colors</h2>
<p>Date/Time color: <input size="11" placeholder="${uv['localdateColor']}" id="localdateColor" name="localdateColor"><span class="colortest"></span></p>
<p>FITE!! color: <input size="11" placeholder="${uv['fiteColor']}" id="fiteColor" name="fiteColor"><span class="colortest"></span></p>
<p>BotB logo color: <input size="11" placeholder="${uv['logoColor']}" id="logoColor" name="logoColor"><span class="colortest"></span></p>
<hr>
<div id="heartofthesun"><h2>Controls</span></h2>
<p><button id="localsave">Save changes</button> <button id="localclear">Delete data</button></p>
<br>
</div>
</div>`;
settingsTarget.insertAdjacentHTML("beforeend", settingsHTML);
for (const key in uv) {
const el = document.getElementById(key);
if (el) {
if (el.type === "checkbox") el.checked = uv[key] === "on";
else el.value = uv[key];
}
}
document.getElementById("localsave")?.addEventListener("click", localsave);
document.getElementById("localclear")?.addEventListener("click", localclear);
}
}
// start of special settings editor code
async function localsave() {
const inputs = document.querySelectorAll("#localsettings input");
for (let input of inputs) {
const id = input.id;
if (!id) continue;
if (input.type === "checkbox") {
await GM.setValue(id, input.checked ? "on" : "off");
} else if (input.value && input.value !== input.placeholder) {
await GM.setValue(id, input.value);
}
}
alert("Settings saved!");
}
async function localclear() {
const keys = await GM.listValues();
for (let key of keys) {
await GM.deleteValue(key);
}
alert("All custom settings cleared.");
}
// start of page cleanup, declaring things
const matches = document.querySelectorAll("span.countdown"); // you can tell I did this first
var now = Date.now();
let avatarLink = document.querySelector("#homeMenu > div > div > div.botbrAvatar > a");
const sideSeperators = document.querySelectorAll("#SIDE_BOX > div.hSeperator");
const otherSideSeperators = document.querySelectorAll("#homeMenu > div > div > div.hMiniSeperator");
let butts = document.querySelectorAll("div.menu_butt");
var iframeGuys = document.querySelectorAll("iframe");
// make Recent Activity into the same kind of span as the other collapsible things
var recentAL = document.evaluate("//span[contains(text(), 'Recent Activity..')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (recentAL != null) {
recentAL.classList.add("tb2");
recentAL.innerText = "Activity Log"; // had to rename this to stop it from breaking
}
// get the collapsible t2s
function applyCollapsibles() {
const sections = [
{ label: "BotBr Burfdays", id: "burf" },
{ label: "Bulletins, News & Development", id: "bnad" },
{ label: "Recent Battles", id: "recentBR" },
{ label: "Entry Activity", id: "entryActBot" },
{ label: "Battle Activity", id: "battleActBot" },
{ label: "Activity Log", id: "recentAL" },
{ label: " Call to Arms", id: "callTA" }
];
document.querySelectorAll("span.t2, span.tb2").forEach(span => {
const section = sections.find(s => s.label === span.innerText);
if (section) {
span.classList.add("toggler");
span.addEventListener("click", () => {
const div = document.getElementById(section.id);
if (div) {
div.style.display = div.style.display === "none" ? "block" : "none";
}
});
const container = span.parentNode;
const newDiv = document.createElement("div");
newDiv.id = section.id;
const moveItems = container.querySelectorAll("a.inner.boxLink, div.hMiniSeperator, div.t0:not(.padL):not(.fright)");
moveItems.forEach(item => newDiv.appendChild(item));
span.insertAdjacentElement("afterend", newDiv);
}
});
}
// start of general countdown code
for (let i = 0; i < matches.length; i++) {
let botbSeconds = matches[i].getAttribute("data-countdown");
let target = now + (botbSeconds * 1000);
let outputDate = new Date(target);
let humanDate = outputDate.toLocaleString("en-us", {
timeStyle: "medium",
dateStyle: "full"
});
if (target != now) {
matches[i].insertAdjacentHTML('afterend', '<br><span class="localdate">' + humanDate + '</span>');
}
}
// start of special beghast code
if (document.title == 'BEGAST teh ONE HOUR BATTLE!!' || document.title == 'BEGHAST teh X HOUR BATTLE!!') {
var xpath2 = "//span[text()='(n00bs still get latist points)']";
var preSetup = document.evaluate(xpath2, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
preSetup.insertAdjacentHTML('afterend', '<div id="localbeghast"></div>');
function recalculate() {
var now = Date.now();
var dt = document.getElementsByName("start_day")[0].value * 1000 * 3600 * 24;
var hour = document.getElementsByName("start_hour")[0].value * 1000 * 3600;
var min = document.getElementsByName("start_min")[0].value * 1000 * 60;
var target = now + dt + hour + min;
let outputDate = new Date(target);
let humanDate = outputDate.toLocaleString("en-US", {
timeStyle: "medium",
dateStyle: "full"
});
document.getElementById("localbeghast").innerHTML = "Start time: " + humanDate;
}
setInterval(recalculate, 1000);
}
// start of localica profile page code (useful for debugging and also hiii!)
if (window.location.href == 'https://battleofthebits.com/barracks/Profile/DefenseMechanism/') {
var localProfile = document.querySelector("#profileMenu > div.ajaxContent > div > div > blockquote");
var localBlurb = `
<hr>
<p class="localblurb">If you can see this, it means you installed some version (v0.4) of the userscript above and it
got all the way to the bottom of the page without reporting an error! This doesn't necessarily mean it ran successfully,
just that it works from your computer's perspective :o</span><br>
<p class="localblurb">This script offers a few customization options in the <a href="https://battleofthebits.com/barracks/Settings/">Settings</a> page.</p>
`
localProfile.innerHTML += localBlurb; // byeee
}
// refresh page on "Refresh..."
const waitForElement = (selector, timeout = 10000) => {
return new Promise((resolve, reject) => {
const start = Date.now();
const interval = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(interval);
resolve(el);
} else if (Date.now() - start > timeout) {
clearInterval(interval);
reject(new Error('Timeout: Element not found'));
}
}, 100);
});
};
waitForElement('span.countdown').then(countdownEl => {
const observer = new MutationObserver(() => {
const text = countdownEl.textContent.trim();
if (text === 'Refresh...') {
observer.disconnect();
location.reload();
}
});
observer.observe(countdownEl, {
characterData: true,
childList: true,
subtree: true
});
}).catch(err => {
console.warn(err.message);
});
if (document.readyState === "complete" || document.readyState === "interactive") {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
})();
function getDonloads() {
let donloads = [];
let temp;
let temp0 = document.querySelectorAll("div.margin0 > div > a.boxLink");
// console.log(temp0)
if (temp0.length > 0) {
temp = temp0;
}
for (let i = 0; i < temp.length; i++) {
if (temp[i].href.indexOf("EntryDonload") >= 0) {
donloads.push(temp[i].href);
}
// console.log(temp[i].href);
}
donloads.sort();
if (donloads.length < 1) {
alert("didn't get no donloads :(");
} else {
GM_setClipboard(donloads.join('\n'));
alert("got downloads! :D");
}
}
GM_registerMenuCommand("get donloads!", getDonloads);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment