Greasy Fork is available in English.
Persist hitbox.io game and jukebox volume, with exponential audio taper, wheel controls, and jukebox mute.
// ==UserScript==
// @name QOLbox
// @namespace Violentmonkey Scripts
// @author gpt-5.4-high (Codex CLI), steered by Aggressive Combo
// @version 1.0.0
// @description Persist hitbox.io game and jukebox volume, with exponential audio taper, wheel controls, and jukebox mute.
// @license ISC
// @match https://hitbox.io/game2.html*
// @match https://www.hitbox.io/game2.html*
// @run-at document-start
// @inject-into page
// @grant none
// ==/UserScript==
(function () {
'use strict';
const GAME_VOLUME_KEY = 'vm.hitbox.volumePercent';
const JUKEBOX_STATE_KEY = 'vm.hitbox.jukeboxState';
const STEP_PERCENT = 5;
const DEFAULT_GAME_PERCENT = 100;
const DEFAULT_JUKEBOX_PERCENT = 50;
const GAME_CURVE_EXPONENT = 2;
const JUKEBOX_CURVE_EXPONENT = 2;
const JUKEBOX_MIN_ANGLE = -40;
const JUKEBOX_MAX_ANGLE = 220;
const JUKEBOX_ARC_CENTER = 14;
const JUKEBOX_ARC_RADIUS = 12;
const JUKEBOX_WHEEL_STEP = 5;
const JUKEBOX_DRAG_SENSITIVITY = 1;
const TICK_MS = 400;
let gamePercent = loadGamePercent();
let currentGameMenuItem = null;
let currentJukeboxMenuItem = null;
let originalHowlVolume = null;
let settingGameVolumeInternally = false;
let jukeboxState = loadJukeboxState();
let activeKnobDrag = null;
let trackedPlayers = new Set();
let youTubeHookInstalled = false;
function clampPercent(value, fallback = 0) {
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) {
return fallback;
}
const steppedValue = Math.round(numericValue / STEP_PERCENT) * STEP_PERCENT;
return Math.min(100, Math.max(0, steppedValue));
}
function clampJukeboxPercent(value) {
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) {
return DEFAULT_JUKEBOX_PERCENT;
}
return Math.min(100, Math.max(0, Math.round(numericValue)));
}
function loadGamePercent() {
try {
return clampPercent(localStorage.getItem(GAME_VOLUME_KEY), DEFAULT_GAME_PERCENT);
} catch {
return DEFAULT_GAME_PERCENT;
}
}
function saveGamePercent() {
try {
localStorage.setItem(GAME_VOLUME_KEY, String(gamePercent));
} catch {
// Ignore storage failures.
}
}
function loadJukeboxState() {
const fallback = {
percent: null,
muted: false,
};
try {
const rawState = localStorage.getItem(JUKEBOX_STATE_KEY);
if (!rawState) {
return fallback;
}
const parsed = JSON.parse(rawState);
return {
percent:
parsed && parsed.percent !== null && parsed.percent !== undefined
? clampJukeboxPercent(parsed.percent)
: null,
muted: Boolean(parsed && parsed.muted),
};
} catch {
return fallback;
}
}
function saveJukeboxState() {
try {
localStorage.setItem(JUKEBOX_STATE_KEY, JSON.stringify(jukeboxState));
} catch {
// Ignore storage failures.
}
}
function percentToGameScalar(percent) {
return Math.pow(clampPercent(percent, DEFAULT_GAME_PERCENT) / 100, GAME_CURVE_EXPONENT);
}
function percentToJukeboxVolume(percent) {
const normalized = clampJukeboxPercent(percent) / 100;
return Math.round(Math.pow(normalized, JUKEBOX_CURVE_EXPONENT) * 100);
}
function percentToJukeboxAngle(percent) {
const normalized = clampJukeboxPercent(percent) / 100;
return JUKEBOX_MIN_ANGLE + (JUKEBOX_MAX_ANGLE - JUKEBOX_MIN_ANGLE) * normalized;
}
function angleToJukeboxPercent(angle) {
const numericAngle = Number(angle);
if (!Number.isFinite(numericAngle)) {
return DEFAULT_JUKEBOX_PERCENT;
}
const normalized =
(Math.min(JUKEBOX_MAX_ANGLE, Math.max(JUKEBOX_MIN_ANGLE, numericAngle)) - JUKEBOX_MIN_ANGLE) /
(JUKEBOX_MAX_ANGLE - JUKEBOX_MIN_ANGLE);
return clampJukeboxPercent(normalized * 100);
}
function polarToArcPoint(angle) {
const radians = ((angle + 180) * Math.PI) / 180;
return {
x: JUKEBOX_ARC_CENTER + JUKEBOX_ARC_RADIUS * Math.cos(radians),
y: JUKEBOX_ARC_CENTER + JUKEBOX_ARC_RADIUS * Math.sin(radians),
};
}
function findGameVolumeItem() {
const candidates = document.querySelectorAll('.items.left .item, .item');
for (const candidate of candidates) {
if (/^Volume:\s*\d+%$/.test(candidate.textContent.trim())) {
return candidate;
}
}
return null;
}
function updateGameVolumeText() {
if (!currentGameMenuItem || !currentGameMenuItem.isConnected) {
currentGameMenuItem = findGameVolumeItem();
}
if (!currentGameMenuItem) {
return;
}
currentGameMenuItem.textContent = `Volume: ${gamePercent}%`;
currentGameMenuItem.title = 'Scroll to adjust by 5%, left-click up, right-click down';
currentGameMenuItem.style.cursor = 'ns-resize';
currentGameMenuItem.style.userSelect = 'none';
}
function applyGameVolume() {
updateGameVolumeText();
if (!window.Howler || !Array.isArray(window.Howler._howls) || !originalHowlVolume) {
return;
}
settingGameVolumeInternally = true;
try {
for (const howl of window.Howler._howls) {
if (!howl || typeof howl !== 'object') {
continue;
}
if (typeof howl.__vmBaseVolume !== 'number') {
const initialVolume = Number(howl._volume);
howl.__vmBaseVolume = Number.isFinite(initialVolume) ? initialVolume : 1;
}
originalHowlVolume.call(howl, howl.__vmBaseVolume * percentToGameScalar(gamePercent));
}
} finally {
settingGameVolumeInternally = false;
}
}
function setGamePercent(nextPercent) {
gamePercent = clampPercent(nextPercent, DEFAULT_GAME_PERCENT);
saveGamePercent();
applyGameVolume();
}
function patchGameVolumeMenu() {
const item = findGameVolumeItem();
if (!item) {
return false;
}
currentGameMenuItem = item;
if (!item.dataset.vmHitboxGameVolumePatched) {
item.dataset.vmHitboxGameVolumePatched = 'true';
item.onclick = event => {
event.preventDefault();
event.stopPropagation();
setGamePercent(gamePercent + STEP_PERCENT);
};
item.oncontextmenu = event => {
event.preventDefault();
event.stopPropagation();
setGamePercent(gamePercent - STEP_PERCENT);
};
item.addEventListener(
'wheel',
event => {
event.preventDefault();
event.stopPropagation();
setGamePercent(gamePercent + (event.deltaY < 0 ? STEP_PERCENT : -STEP_PERCENT));
},
{ passive: false }
);
}
updateGameVolumeText();
return true;
}
function hookHowlPrototype() {
const HowlCtor = window.Howl;
if (!HowlCtor || !HowlCtor.prototype || typeof HowlCtor.prototype.volume !== 'function') {
return false;
}
if (HowlCtor.prototype.volume.__vmHitboxWrapped) {
return true;
}
originalHowlVolume = HowlCtor.prototype.volume;
function wrappedVolume(value, ...rest) {
if (arguments.length === 0) {
if (typeof this.__vmBaseVolume === 'number') {
return this.__vmBaseVolume;
}
return originalHowlVolume.call(this);
}
if (typeof value === 'number' && !settingGameVolumeInternally) {
this.__vmBaseVolume = value;
return originalHowlVolume.call(this, value * percentToGameScalar(gamePercent), ...rest);
}
return originalHowlVolume.call(this, value, ...rest);
}
wrappedVolume.__vmHitboxWrapped = true;
HowlCtor.prototype.volume = wrappedVolume;
applyGameVolume();
return true;
}
function findSettingsContainer() {
return document.querySelector('.items.left');
}
function findChangeControlsItem(container) {
if (!container) {
return null;
}
const items = container.querySelectorAll('.item');
for (const item of items) {
if (item.textContent.trim() === 'Change Controls') {
return item;
}
}
return null;
}
function getJukeboxMenuLabel() {
return jukeboxState.muted ? 'Unmute Jukebox' : 'Mute Jukebox';
}
function updateJukeboxMenuItem() {
if (!currentJukeboxMenuItem || !currentJukeboxMenuItem.isConnected) {
return;
}
currentJukeboxMenuItem.textContent = getJukeboxMenuLabel();
currentJukeboxMenuItem.title = 'Remember the lobby radio mute state';
}
function patchJukeboxMenu() {
const container = findSettingsContainer();
if (!container) {
return false;
}
let item = container.querySelector('.item[data-vm-hitbox-jukebox-menu="true"]');
if (!item) {
item = document.createElement('div');
item.className = 'item';
item.dataset.vmHitboxJukeboxMenu = 'true';
item.onclick = event => {
event.preventDefault();
event.stopPropagation();
toggleJukeboxMute();
};
const beforeItem = findChangeControlsItem(container);
if (beforeItem) {
container.insertBefore(item, beforeItem);
} else {
container.appendChild(item);
}
}
currentJukeboxMenuItem = item;
updateJukeboxMenuItem();
return true;
}
function findJukeboxKnob() {
return document.querySelector('.jukebox .knob.volumeContainer');
}
function ensureJukeboxPercent(knob) {
if (jukeboxState.percent !== null) {
return;
}
const bar = knob ? knob.querySelector('.barSVG') : null;
const transform = bar ? bar.style.transform || window.getComputedStyle(bar).transform : '';
const match = typeof transform === 'string' ? transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/i) : null;
jukeboxState.percent = angleToJukeboxPercent(match ? Number(match[1]) : DEFAULT_JUKEBOX_PERCENT);
saveJukeboxState();
}
function setKnobVisual(knob, percent) {
if (!knob) {
return;
}
const angle = percentToJukeboxAngle(percent);
const bar = knob.querySelector('.barSVG');
const arcPath = knob.querySelector('.arcSVG path');
if (bar) {
bar.style.transform = `rotate(${angle}deg)`;
}
if (arcPath) {
const startPoint = polarToArcPoint(JUKEBOX_MIN_ANGLE);
const endPoint = polarToArcPoint(angle);
const sweepDegrees = Math.max(0, angle - JUKEBOX_MIN_ANGLE);
const largeArcFlag = sweepDegrees > 180 ? 1 : 0;
arcPath.setAttribute(
'd',
`M ${startPoint.x} ${startPoint.y} A ${JUKEBOX_ARC_RADIUS} ${JUKEBOX_ARC_RADIUS} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y}`
);
}
}
function applyJukeboxStateToKnob(knob) {
if (!knob || activeKnobDrag) {
return;
}
ensureJukeboxPercent(knob);
setKnobVisual(knob, jukeboxState.muted ? 0 : jukeboxState.percent);
}
function trackPlayer(player) {
if (!player || typeof player.setVolume !== 'function') {
return;
}
trackedPlayers.add(player);
}
function discoverPlayers() {
const yt = window.YT;
if (!yt || typeof yt.get !== 'function') {
return;
}
const candidates = document.querySelectorAll('#ytContainer [id], #ytContainer iframe[id]');
for (const candidate of candidates) {
if (!candidate.id) {
continue;
}
try {
const player = yt.get(candidate.id);
if (player && typeof player.setVolume === 'function') {
trackPlayer(player);
}
} catch {
// Ignore unresolved ids.
}
}
}
function applyJukeboxStateToPlayer(player) {
if (!player || typeof player.setVolume !== 'function') {
trackedPlayers.delete(player);
return;
}
ensureJukeboxPercent(findJukeboxKnob());
try {
if (jukeboxState.muted) {
if (typeof player.unMute === 'function') {
player.unMute();
}
player.setVolume(0);
if (typeof player.mute === 'function') {
player.mute();
}
} else {
if (typeof player.unMute === 'function') {
player.unMute();
}
player.setVolume(percentToJukeboxVolume(jukeboxState.percent));
}
} catch {
trackedPlayers.delete(player);
}
}
function applyJukeboxState() {
const knob = findJukeboxKnob();
applyJukeboxStateToKnob(knob);
discoverPlayers();
for (const player of Array.from(trackedPlayers)) {
applyJukeboxStateToPlayer(player);
}
}
function hookYouTubePlayer() {
const yt = window.YT;
if (!yt || typeof yt.Player !== 'function') {
return false;
}
if (youTubeHookInstalled || yt.Player.__vmHitboxWrapped) {
youTubeHookInstalled = true;
discoverPlayers();
return true;
}
const OriginalPlayer = yt.Player;
function WrappedPlayer(...args) {
const instance = new OriginalPlayer(...args);
trackPlayer(instance);
window.setTimeout(() => {
applyJukeboxStateToPlayer(instance);
}, 0);
return instance;
}
Object.setPrototypeOf(WrappedPlayer, OriginalPlayer);
WrappedPlayer.prototype = OriginalPlayer.prototype;
WrappedPlayer.__vmHitboxWrapped = true;
yt.Player = WrappedPlayer;
youTubeHookInstalled = true;
discoverPlayers();
return true;
}
function setJukeboxPercent(nextPercent) {
jukeboxState.percent = clampJukeboxPercent(nextPercent);
jukeboxState.muted = false;
saveJukeboxState();
updateJukeboxMenuItem();
setKnobVisual(findJukeboxKnob(), jukeboxState.percent);
applyJukeboxState();
}
function toggleJukeboxMute() {
ensureJukeboxPercent(findJukeboxKnob());
jukeboxState.muted = !jukeboxState.muted;
saveJukeboxState();
updateJukeboxMenuItem();
applyJukeboxState();
}
function getKnobPercentFromPointer(event) {
if (!activeKnobDrag) {
return DEFAULT_JUKEBOX_PERCENT;
}
const deltaY = activeKnobDrag.startY - event.clientY;
const nextPercent = activeKnobDrag.startPercent + deltaY * JUKEBOX_DRAG_SENSITIVITY;
return clampJukeboxPercent(nextPercent);
}
function onKnobPointerMove(event) {
if (!activeKnobDrag) {
return;
}
event.preventDefault();
const percent = getKnobPercentFromPointer(event);
setJukeboxPercent(percent);
}
function endKnobDrag() {
if (!activeKnobDrag) {
return;
}
activeKnobDrag = null;
}
function patchGlobalKnobListeners() {
if (window.__vmHitboxJukeboxGlobalsPatched) {
return;
}
window.__vmHitboxJukeboxGlobalsPatched = true;
window.addEventListener('pointermove', onKnobPointerMove, true);
window.addEventListener('mousemove', onKnobPointerMove, true);
window.addEventListener('pointerup', endKnobDrag, true);
window.addEventListener('mouseup', endKnobDrag, true);
window.addEventListener('blur', endKnobDrag, true);
}
function patchJukeboxKnob() {
const knob = findJukeboxKnob();
if (!knob) {
return false;
}
patchGlobalKnobListeners();
ensureJukeboxPercent(knob);
applyJukeboxStateToKnob(knob);
if (!knob.dataset.vmHitboxJukeboxPatched) {
knob.dataset.vmHitboxJukeboxPatched = 'true';
knob.title = 'Scroll or drag to adjust the jukebox volume';
knob.style.touchAction = 'none';
const startDrag = event => {
event.preventDefault();
event.stopPropagation();
if (typeof knob.setPointerCapture === 'function' && event.pointerId !== undefined) {
try {
knob.setPointerCapture(event.pointerId);
} catch {
// Ignore pointer capture failures.
}
}
if (jukeboxState.muted) {
jukeboxState.muted = false;
saveJukeboxState();
updateJukeboxMenuItem();
applyJukeboxState();
}
activeKnobDrag = {
knob,
startY: event.clientY,
startPercent: jukeboxState.muted ? 0 : (jukeboxState.percent ?? DEFAULT_JUKEBOX_PERCENT),
};
onKnobPointerMove(event);
};
knob.addEventListener('pointerdown', startDrag, true);
knob.addEventListener(
'wheel',
event => {
event.preventDefault();
event.stopPropagation();
ensureJukeboxPercent(knob);
const currentPercent = jukeboxState.muted ? 0 : jukeboxState.percent;
setJukeboxPercent(currentPercent + (event.deltaY < 0 ? JUKEBOX_WHEEL_STEP : -JUKEBOX_WHEEL_STEP));
},
{ passive: false }
);
}
return true;
}
function bootstrap() {
hookHowlPrototype();
patchGameVolumeMenu();
hookYouTubePlayer();
patchJukeboxMenu();
patchJukeboxKnob();
applyJukeboxState();
}
window.setInterval(bootstrap, TICK_MS);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
} else {
bootstrap();
}
})();