OC Timing: Travel Blocker

Blocks travel to destinations where the return time would overlap with OC initiation. Includes responsive toggle switch for manual control.

当前为 2025-07-28 提交的版本,查看 最新版本

// ==UserScript==
// @name         OC Timing: Travel Blocker
// @namespace    zonure.scripts
// @version      1.0
// @description  Blocks travel to destinations where the return time would overlap with OC initiation. Includes responsive toggle switch for manual control.
// @author       Zonure [3787510]
// @match        https://www.torn.com/page.php?sid=travel
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  const STORAGE_KEY = 'oc_travel_block_enabled';

  const style = document.createElement('style');
  style.textContent = `
    .script-disabled-button {
      background-color: #a00 !important;
      color: crimson !important;
      font-weight: bold;
      text-transform: uppercase;
    }

    #oc-toggle-container {
      margin: 10px 0;
      padding: 6px 10px;
      background: #222;
      color: #fff;
      border-radius: 5px;
      font-size: 13px;
      display: inline-flex;
      align-items: center;
      gap: 8px;
    }

    .switch {
      position: relative;
      display: inline-block;
      width: 38px;
      height: 20px;
      flex-shrink: 0;
    }

    .switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }

    .slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      transition: 0.3s;
      border-radius: 34px;
    }

    .slider:before {
      position: absolute;
      content: "";
      height: 14px;
      width: 14px;
      left: 3px;
      bottom: 3px;
      background-color: white;
      transition: 0.3s;
      border-radius: 50%;
    }

    input:checked + .slider {
      background-color: #4caf50;
    }

    input:checked + .slider:before {
      transform: translateX(18px);
    }

    @media (max-width: 600px) {
      #oc-toggle-container {
        font-size: 12px;
        padding: 4px 8px;
        gap: 6px;
      }

      .switch {
        width: 32px;
        height: 16px;
      }

      .slider:before {
        height: 10px;
        width: 10px;
        left: 3px;
        bottom: 3px;
      }

      input:checked + .slider:before {
        transform: translateX(14px);
      }
    }
  `;
  document.head.appendChild(style);

  let isEnabled = localStorage.getItem(STORAGE_KEY);
  if (isEnabled === null) {
    isEnabled = 'true';
    localStorage.setItem(STORAGE_KEY, isEnabled);
  }
  isEnabled = isEnabled === 'true';

  const injectToggle = () => {
    const wrapper = document.querySelector('div.content-wrapper.summer');
    if (!wrapper || wrapper.querySelector('#oc-toggle-container')) return;

    const container = document.createElement('div');
    container.id = 'oc-toggle-container';
    container.innerHTML = `
      <span>Travel Blocker:</span>
      <label class="switch">
        <input type="checkbox" id="oc-toggle" ${isEnabled ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span id="oc-status">${isEnabled ? 'Enabled' : 'Disabled'}</span>
    `;

    const input = container.querySelector('#oc-toggle');
    const status = container.querySelector('#oc-status');

    input.addEventListener('change', () => {
      isEnabled = input.checked;
      localStorage.setItem(STORAGE_KEY, isEnabled);
      status.textContent = isEnabled ? 'Enabled' : 'Disabled';
      disableButtonIfNeeded();
    });

    wrapper.prepend(container);
  };

  const disableButtonIfNeeded = () => {
    const travelRoot = document.getElementById('travel-root');
    if (!travelRoot || !isEnabled) return;

    const modelDataRaw = travelRoot.getAttribute('data-model');
    if (!modelDataRaw) return;

    let parsed;
    try {
      parsed = JSON.parse(modelDataRaw);
    } catch (e) {
      return;
    }

    const destinations = parsed?.destinations;
    let shouldDisable = false;

    if (Array.isArray(destinations)) {
      for (const dest of destinations) {
        for (const key of ['standard', 'airstrip', 'private', 'business']) {
          if (dest[key]?.ocReadyBeforeBack === true) {
            shouldDisable = true;
            break;
          }
        }
        if (shouldDisable) break;
      }
    }

    const buttons = document.querySelectorAll("a.torn-btn.btn-dark-bg, button.torn-btn.btn-dark-bg");
    buttons.forEach((btn) => {
      const alreadyBlocked = btn.classList.contains("script-disabled-button");
      if (btn.textContent.trim() === "Continue") {
        if (shouldDisable && !alreadyBlocked) {
          btn.disabled = true;
          btn.textContent = "DISABLED";
          btn.title = "Disabled: OC not ready yet.";
          btn.classList.add("script-disabled-button");
          btn.onclick = (e) => {
            e.preventDefault();
            e.stopImmediatePropagation();
            console.log("Blocked: OC not ready.");
          };
        } else if (!shouldDisable && alreadyBlocked) {
          btn.disabled = false;
          btn.textContent = "Continue";
          btn.title = "";
          btn.classList.remove("script-disabled-button");
          btn.onclick = null;
        }
      }
    });
  };

  const init = () => {
    injectToggle();
    disableButtonIfNeeded();
  };

  const observer = new MutationObserver(() => {
    injectToggle();
    disableButtonIfNeeded();
  });

  observer.observe(document.body, { childList: true, subtree: true });

  init();
})();