Florr.io Rebinds

Customizable key rebinding interface for Florr.io

// ==UserScript==
// @name         Florr.io Rebinds
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Customizable key rebinding interface for Florr.io
// @author       VortexPrime
// @match        https://florr.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=florr.io
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  document.addEventListener('DOMContentLoaded', function() {
    const keybinds = {
      '0': null,
      '1': null,
      '2': null,
      '3': null,
      '4': null,
      '5': null,
      '6': null,
      '7': null,
      '8': null,
      '9': null,
      'R': null,
      'L': null,
      'K': null,
      'M': null
    };

    const bindableKeys = new Set(Object.keys(keybinds));
    const reverseKeybinds = {};
    const keyState = {};

    function updateReverseMapping() {
      Object.keys(reverseKeybinds).forEach(key => delete reverseKeybinds[key]);

      Object.keys(keybinds).forEach(target => {
        const bind = keybinds[target];
        if (bind) {
          reverseKeybinds[bind.toUpperCase()] = target;
        }
      });
    }

    updateReverseMapping();

    const keyDisplayMap = {
      'ESCAPE': 'ESC',
      'BACKSPACE': 'BSP',
      'DELETE': 'DEL',
      'INSERT': 'INS',
      'PAGEUP': 'PUP',
      'PAGEDOWN': 'PDN',
      'ARROWUP': 'UP',
      'ARROWDOWN': 'DWN',
      'ARROWLEFT': 'LFT',
      'ARROWRIGHT': 'RGT',
      'SPACE': 'SPC',
      'CONTROL': 'CTL',
      'SHIFT': 'SHF',
      'ENTER': 'ENT',
      'TAB': 'TAB'
    };

    const keyCodeMap = {
      '0': 'Digit0',
      '1': 'Digit1',
      '2': 'Digit2',
      '3': 'Digit3',
      '4': 'Digit4',
      '5': 'Digit5',
      '6': 'Digit6',
      '7': 'Digit7',
      '8': 'Digit8',
      '9': 'Digit9',
      'A': 'KeyA',
      'B': 'KeyB',
      'C': 'KeyC',
      'D': 'KeyD',
      'E': 'KeyE',
      'F': 'KeyF',
      'G': 'KeyG',
      'H': 'KeyH',
      'I': 'KeyI',
      'J': 'KeyJ',
      'K': 'KeyK',
      'L': 'KeyL',
      'M': 'KeyM',
      'N': 'KeyN',
      'O': 'KeyO',
      'P': 'KeyP',
      'Q': 'KeyQ',
      'R': 'KeyR',
      'S': 'KeyS',
      'T': 'KeyT',
      'U': 'KeyU',
      'V': 'KeyV',
      'W': 'KeyW',
      'X': 'KeyX',
      'Y': 'KeyY',
      'Z': 'KeyZ',
      'SPACE': 'Space',
      'SHIFT': 'ShiftLeft',
      'CONTROL': 'ControlLeft',
      'TAB': 'Tab'
    };

    const groupDescriptions = {
      loadout: 'Loadout Slots',
      utility: {
        'R': 'Swap all loadouts',
        'L': 'Load saved builds',
        'K': 'Load saved builds',
        'M': 'Toggle expanded minimap'
      }
    };

    const style = document.createElement('style');
    style.textContent = `
      .florrBinds-modal {
        position: absolute;
        top: 50px;
        left: 50px;
        width: 280px;
        background-color: #374151;
        border-radius: 8px;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
        border: 1px solid #4B5563;
        font-family: Arial, sans-serif;
        color: #E5E7EB;
        z-index: 10000;
        overflow: hidden;
        user-select: none;
      }

      .florrBinds-modal.hidden {
        display: none;
      }

      .florrBinds-header {
        background-color: #1F2937;
        padding: 10px 16px;
        border-bottom: 1px solid #4B5563;
        cursor: move;
        display: flex;
        justify-content: space-between;
      }

      .florrBinds-header-left, .florrBinds-header-right {
        display: flex;
        flex-direction: column;
        justify-content: center;
      }

      .florrBinds-header-right {
        align-items: flex-end;
        text-align: right;
      }

      .florrBinds-title {
        color: #FBBF24;
        font-weight: bold;
        font-size: 18px;
        margin: 0;
        padding: 0;
        line-height: 1.2;
      }

      .florrBinds-subtitle {
        color: #9CA3AF;
        font-size: 12px;
        margin: 0;
      }

      .florrBinds-author {
        color: #9CA3AF;
        font-size: 11px;
        margin-top: 2px;
      }

      .florrBinds-author a {
        color: #93C5FD;
        text-decoration: none;
      }

      .florrBinds-author a:hover {
        text-decoration: underline;
      }

      .florrBinds-drag-hint {
        color: #6B7280;
        font-size: 11px;
        margin-bottom: 2px;
      }

      .florrBinds-content {
        padding: 8px;
        overflow: hidden;
      }

      .florrBinds-modal.collapsed .florrBinds-content,
      .florrBinds-modal.collapsed .florrBinds-footer {
        display: none;
      }

      .florrBinds-group {
        margin-bottom: 8px;
      }

      .florrBinds-group-title {
        color: #9CA3AF;
        font-size: 14px;
        margin-bottom: 4px;
        padding-bottom: 4px;
        border-bottom: 1px solid #4B5563;
      }

      .florrBinds-loadout-grid {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        gap: 8px;
      }

      .florrBinds-key-item {
        display: flex;
        flex-direction: column;
        align-items: center;
        margin-bottom: 6px;
      }

      .florrBinds-key-label {
        color: #D1D5DB;
        font-size: 14px;
        margin-bottom: 4px;
      }

      .florrBinds-key-value {
        background-color: #1F2937;
        color: #FFF;
        padding: 4px 12px;
        border-radius: 4px;
        border: 1px solid #4B5563;
        font-family: monospace;
        font-size: 14px;
        cursor: pointer;
        text-align: center;
        width: 100%;
        box-sizing: border-box;
        transition: transform 0.2s ease;
      }

      .florrBinds-key-value:hover {
        transform: translateY(-2px);
      }

      .florrBinds-key-value.unbound {
        background-color: #111827;
        color: #9CA3AF;
        border-color: #4B5563;
      }

      .florrBinds-utility {
        display: none;
      }

      .florrBinds-utility.show {
        display: block;
      }

      .florrBinds-utility-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 6px;
      }

      .florrBinds-utility-info {
        display: flex;
        align-items: center;
        min-width: 0;
        flex: 1;
      }

      .florrBinds-utility-key {
        color: #D1D5DB;
        font-size: 14px;
        margin-right: 8px;
        flex-shrink: 0;
      }

      .florrBinds-utility-desc {
        color: #9CA3AF;
        font-size: 12px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .florrBinds-key-value-container {
        flex-shrink: 0;
        width: 60px;
        text-align: right;
      }

      .florrBinds-utility .florrBinds-key-value {
        width: 100%;
        box-sizing: border-box;
        text-align: center;
      }

      .florrBinds-toggle {
        width: 100%;
        text-align: center;
        color: #60A5FA;
        background: none;
        border: none;
        padding: 4px;
        margin-bottom: 6px;
        font-size: 14px;
        cursor: pointer;
      }

      .florrBinds-toggle:hover {
        color: #93C5FD;
      }

      .florrBinds-footer {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 8px;
        border-top: 1px solid #4B5563;
      }

      .florrBinds-reset {
        background-color: #2563EB;
        color: white;
        border: none;
        border-radius: 4px;
        padding: 4px 8px;
        font-size: 12px;
        cursor: pointer;
      }

      .florrBinds-reset:hover {
        background-color: #3B82F6;
      }

      .florrBinds-hint {
        color: #9CA3AF;
        font-size: 12px;
      }

      .florrBinds-resize {
        position: absolute;
        bottom: 0;
        right: 0;
        width: 12px;
        height: 12px;
        cursor: ew-resize;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .florrBinds-resize-icon {
        width: 0;
        height: 0;
        border-style: solid;
        border-width: 0 0 8px 8px;
        border-color: transparent transparent rgba(200, 200, 200, 0.4) transparent;
      }

      .florrBinds-status {
        position: absolute;
        bottom: 8px;
        left: 8px;
        padding: 4px 8px;
        background-color: rgba(0, 0, 0, 0.5);
        color: white;
        border-radius: 4px;
        font-size: 12px;
        pointer-events: none;
        opacity: 0;
        transition: opacity 0.3s ease;
      }

      .florrBinds-status.show {
        opacity: 1;
      }
    `;
    document.head.appendChild(style);

    const modal = document.createElement('div');
    modal.className = 'florrBinds-modal';

    const header = document.createElement('div');
    header.className = 'florrBinds-header';

    const headerLeft = document.createElement('div');
    headerLeft.className = 'florrBinds-header-left';

    const title = document.createElement('h3');
    title.className = 'florrBinds-title';
    title.textContent = 'Florr.io Binds';

    const subtitle = document.createElement('p');
    subtitle.className = 'florrBinds-subtitle';
    subtitle.textContent = 'Rebind specific keys!';

    headerLeft.appendChild(title);
    headerLeft.appendChild(subtitle);

    const headerRight = document.createElement('div');
    headerRight.className = 'florrBinds-header-right';

    const dragHint = document.createElement('div');
    dragHint.className = 'florrBinds-drag-hint';
    dragHint.textContent = '(Drag to move)';

    const author = document.createElement('div');
    author.className = 'florrBinds-author';
    author.innerHTML = 'by <a href="https://ashish.top" target="_blank">VortexPrime</a>';

    headerRight.appendChild(dragHint);
    headerRight.appendChild(author);

    header.appendChild(headerLeft);
    header.appendChild(headerRight);
    modal.appendChild(header);

    const content = document.createElement('div');
    content.className = 'florrBinds-content';

    const loadoutGroup = document.createElement('div');
    loadoutGroup.className = 'florrBinds-group';

    const loadoutTitle = document.createElement('h4');
    loadoutTitle.className = 'florrBinds-group-title';
    loadoutTitle.textContent = groupDescriptions.loadout;
    loadoutGroup.appendChild(loadoutTitle);

    const loadoutGrid = document.createElement('div');
    loadoutGrid.className = 'florrBinds-loadout-grid';

    for (let i = 0; i <= 9; i++) {
      const key = i.toString();
      const keyItem = document.createElement('div');
      keyItem.className = 'florrBinds-key-item';

      const keyLabel = document.createElement('span');
      keyLabel.className = 'florrBinds-key-label';
      keyLabel.textContent = key;

      const keyValue = document.createElement('div');
      keyValue.className = `florrBinds-key-value ${keybinds[key] ? '' : 'unbound'}`;
      keyValue.textContent = keybinds[key] ? formatKeyDisplay(keybinds[key]) : '-';
      keyValue.dataset.key = key;

      keyItem.appendChild(keyLabel);
      keyItem.appendChild(keyValue);
      loadoutGrid.appendChild(keyItem);
    }

    loadoutGroup.appendChild(loadoutGrid);
    content.appendChild(loadoutGroup);

    const utilityGroup = document.createElement('div');
    utilityGroup.className = 'florrBinds-group florrBinds-utility';

    const utilityTitle = document.createElement('h4');
    utilityTitle.className = 'florrBinds-group-title';
    utilityTitle.textContent = 'Utility Keys';
    utilityGroup.appendChild(utilityTitle);

    const utilityKeys = ['R', 'L', 'K', 'M'];
    for (const key of utilityKeys) {
      const utilityItem = document.createElement('div');
      utilityItem.className = 'florrBinds-utility-item';

      const utilityInfo = document.createElement('div');
      utilityInfo.className = 'florrBinds-utility-info';

      const utilityKey = document.createElement('span');
      utilityKey.className = 'florrBinds-utility-key';
      utilityKey.textContent = key;

      const utilityDesc = document.createElement('span');
      utilityDesc.className = 'florrBinds-utility-desc';
      utilityDesc.textContent = groupDescriptions.utility[key];

      utilityInfo.appendChild(utilityKey);
      utilityInfo.appendChild(utilityDesc);

      const keyValueContainer = document.createElement('div');
      keyValueContainer.className = 'florrBinds-key-value-container';

      const keyValue = document.createElement('div');
      keyValue.className = `florrBinds-key-value ${keybinds[key] ? '' : 'unbound'}`;
      keyValue.textContent = keybinds[key] ? formatKeyDisplay(keybinds[key]) : '-';
      keyValue.dataset.key = key;

      keyValueContainer.appendChild(keyValue);

      utilityItem.appendChild(utilityInfo);
      utilityItem.appendChild(keyValueContainer);
      utilityGroup.appendChild(utilityItem);
    }

    content.appendChild(utilityGroup);

    const toggleBtn = document.createElement('button');
    toggleBtn.className = 'florrBinds-toggle';
    toggleBtn.textContent = 'Show More »';
    content.appendChild(toggleBtn);

    const footer = document.createElement('div');
    footer.className = 'florrBinds-footer';

    const resetBtn = document.createElement('button');
    resetBtn.className = 'florrBinds-reset';
    resetBtn.textContent = 'Reset All';

    const hint = document.createElement('div');
    hint.className = 'florrBinds-hint';
    hint.textContent = 'Toggle using Right Shift';

    footer.appendChild(resetBtn);
    footer.appendChild(hint);

    modal.appendChild(content);
    modal.appendChild(footer);

    const resizeHandle = document.createElement('div');
    resizeHandle.className = 'florrBinds-resize';

    const resizeIcon = document.createElement('div');
    resizeIcon.className = 'florrBinds-resize-icon';

    resizeHandle.appendChild(resizeIcon);
    modal.appendChild(resizeHandle);

    const statusNotification = document.createElement('div');
    statusNotification.className = 'florrBinds-status';
    document.body.appendChild(statusNotification);

    document.body.appendChild(modal);

    let isDragging = false;
    let dragOffsetX = 0;
    let dragOffsetY = 0;

    let isResizing = false;
    let originalWidth = 280;
    let originalMouseX = 0;

    let isRebinding = false;
    let rebindElement = null;

    function formatKeyDisplay(key) {
      if (!key) return '-';
      const upperKey = key.toUpperCase();
      return keyDisplayMap[upperKey] || (upperKey.length > 3 ? upperKey.substring(0, 3) : upperKey);
    }

    function showStatus(message, duration = 2000) {
      statusNotification.textContent = message;
      statusNotification.classList.add('show');
      setTimeout(() => {
        statusNotification.classList.remove('show');
      }, duration);
    }

    function simulateKeyEvent(type, key, code) {
      const event = new KeyboardEvent(type, {
        key: key,
        code: code,
        keyCode: key.charCodeAt(0),
        which: key.charCodeAt(0),
        bubbles: true,
        cancelable: true
      });

      document.dispatchEvent(event);
    }

    function getCodeForKey(key) {
      if (key >= '0' && key <= '9') {
        return `Digit${key}`;
      }
      return keyCodeMap[key.toUpperCase()] || `Key${key.toUpperCase()}`;
    }

    toggleBtn.addEventListener('click', () => {
      const utilitySection = document.querySelector('.florrBinds-utility');
      utilitySection.classList.toggle('show');
      toggleBtn.textContent = utilitySection.classList.contains('show') ? '« Show Less' : 'Show More »';
    });

    header.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      modal.classList.toggle('collapsed');
      return false;
    });

    header.addEventListener('mousedown', (e) => {
      if (e.button === 0) {
        isDragging = true;
        dragOffsetX = e.clientX - modal.offsetLeft;
        dragOffsetY = e.clientY - modal.offsetTop;
        e.preventDefault();
      }
    });

    resizeHandle.addEventListener('mousedown', (e) => {
      isResizing = true;
      originalWidth = modal.offsetWidth;
      originalMouseX = e.clientX;
      e.preventDefault();
      e.stopPropagation();
    });

    document.addEventListener('mousemove', (e) => {
      if (isDragging) {
        modal.style.left = (e.clientX - dragOffsetX) + 'px';
        modal.style.top = (e.clientY - dragOffsetY) + 'px';
      } else if (isResizing) {
        const width = originalWidth + (e.clientX - originalMouseX);

        if (width >= 240) {
          modal.style.width = width + 'px';
        }
      }
    });

    document.addEventListener('mouseup', () => {
      isDragging = false;
      isResizing = false;
    });

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Shift' && e.location === 2) {
        modal.classList.toggle('hidden');
        e.preventDefault();
        e.stopPropagation();
        return;
      }

      if (isRebinding) return;

      const pressedKey = e.key.toUpperCase();

      if (reverseKeybinds[pressedKey]) {
        const targetKey = reverseKeybinds[pressedKey];

        if (!keyState[pressedKey]) {
          keyState[pressedKey] = true;

          const targetCode = getCodeForKey(targetKey);
          simulateKeyEvent('keydown', targetKey, targetCode);
        }
      }
    }, true);

    document.addEventListener('keyup', (e) => {
      if (isRebinding) return;

      const releasedKey = e.key.toUpperCase();

      if (reverseKeybinds[releasedKey]) {
        const targetKey = reverseKeybinds[releasedKey];

        if (keyState[releasedKey]) {
          keyState[releasedKey] = false;

          const targetCode = getCodeForKey(targetKey);
          simulateKeyEvent('keyup', targetKey, targetCode);
        }
      }
    }, true);

    const keyElements = document.querySelectorAll('.florrBinds-key-value');
    keyElements.forEach(element => {
      element.addEventListener('click', () => {
        if (isRebinding) {
          rebindElement.style.boxShadow = '';
          isRebinding = false;
        }

        keyElements.forEach(el => el.style.boxShadow = '');
        element.style.boxShadow = '0 0 0 2px #60A5FA';

        isRebinding = true;
        rebindElement = element;

        showStatus(`Press any key to bind to ${element.dataset.key}. ESC/BACKSPACE to unbind.`);

        const captureKey = (e) => {
          e.preventDefault();
          e.stopPropagation();

          const key = e.key.toUpperCase();
          const targetKey = element.dataset.key;

          let conflictKey = null;
          for (const k in keybinds) {
            if (keybinds[k] && keybinds[k].toUpperCase() === key) {
              conflictKey = k;
              break;
            }
          }

          if (conflictKey && conflictKey !== targetKey) {
            showStatus(`Warning: "${key}" was already bound to ${conflictKey}. Binding swapped.`, 3000);

            const conflictElement = document.querySelector(`.florrBinds-key-value[data-key="${conflictKey}"]`);
            if (conflictElement) {
              conflictElement.textContent = '-';
              conflictElement.classList.add('unbound');
              keybinds[conflictKey] = null;
            }
          }

          if (key === 'ESCAPE' || key === 'BACKSPACE') {
            element.textContent = '-';
            element.classList.add('unbound');
            keybinds[targetKey] = null;
            showStatus(`Removed binding for ${targetKey}`);
          } else {
            element.textContent = formatKeyDisplay(key);
            element.classList.remove('unbound');
            keybinds[targetKey] = key;
            showStatus(`Bound ${targetKey} to ${key}`);
          }

          updateReverseMapping();

          element.style.boxShadow = '';
          isRebinding = false;
          rebindElement = null;

          document.removeEventListener('keydown', captureKey);
        };

        document.addEventListener('keydown', captureKey);
      });
    });

    resetBtn.addEventListener('click', () => {
      for (const key in keybinds) {
        keybinds[key] = null;

        const element = document.querySelector(`.florrBinds-key-value[data-key="${key}"]`);
        if (element) {
          element.textContent = '-';
          element.classList.add('unbound');
        }
      }

      keybinds['0'] = 'V';
      keybinds['1'] = 'B';
      keybinds['2'] = 'F';
      keybinds['3'] = 'G';
      keybinds['4'] = 'E';
      keybinds['9'] = 'P';
      keybinds['K'] = 'X';

      for (const key in keybinds) {
        if (keybinds[key]) {
          const element = document.querySelector(`.florrBinds-key-value[data-key="${key}"]`);
          if (element) {
            element.textContent = formatKeyDisplay(keybinds[key]);
            element.classList.remove('unbound');
          }
        }
      }

      updateReverseMapping();

      Object.keys(keyState).forEach(key => delete keyState[key]);

      showStatus('All keybinds reset to default');
    });

    updateReverseMapping();

    modal.classList.add('hidden');
    showStatus('Keybinds loaded! Press Right Shift to toggle the modal.', 3000);
  });
})();