QOLbox

Persist hitbox.io game and jukebox volume, with exponential audio taper, wheel controls, and jukebox mute.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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();
  }
})();