OC Timing: Travel Blocker (Modular)

Blocks travel to destinations where the return time would overlap with OC initiation. Works on both mobile and desktop. Toggle in UI, disables Continue after selecting a destination.

// ==UserScript==
// @name         OC Timing: Travel Blocker (Modular)
// @namespace    zonure.scripts
// @version      1.2.0
// @description  Blocks travel to destinations where the return time would overlap with OC initiation. Works on both mobile and desktop. Toggle in UI, disables Continue after selecting a destination.
// @author       Zonure [3787510]
// @match        https://www.torn.com/page.php?sid=travel
// @grant        none
// @license      MIT
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  const DEBUG = false; // Toggle to true for detailed logs

  const STORAGE_KEY = 'oc_travel_block_enabled';

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

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

    #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);

  const getDataModel = () => {
    const travelRoot = document.getElementById('travel-root');
    if (!travelRoot) return null;

    try {
      return JSON.parse(travelRoot.getAttribute('data-model'));
    } catch (e) {
      console.error("❌ Failed to parse data-model:", e);
      return null;
    }
  };

  const disableContinueButton = (country, method) => {
    const buttons = document.querySelectorAll("a.torn-btn.btn-dark-bg, button.torn-btn.btn-dark-bg");

    buttons.forEach((btn) => {
      if (btn.textContent.trim() === "Continue") {
        console.warn(`🔒 Blocking travel to ${country} via ${method}`);
        btn.disabled = true;
        btn.textContent = "DISABLED";
        btn.title = "Blocked: OC not ready before return.";
        btn.classList.add("script-disabled-button");

        const prevent = (e) => {
          e.preventDefault();
          e.stopImmediatePropagation();
        };

        btn.onclick = prevent;
        btn.onmousedown = prevent;
        btn.addEventListener("click", prevent, true);
        btn.addEventListener("mousedown", prevent, 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';
    });

    wrapper.prepend(container);
  };

  const handleDesktopClick = (event) => {
    const button = event.target.closest("button.torn-btn.btn-dark-bg, a.torn-btn.btn-dark-bg");
    if (!button) return;

    const flightGrid = button.closest(".flightDetailsGrid___uAttX");
    const countrySpan = flightGrid?.querySelector(".country___wBPip");
    const countryName = countrySpan?.textContent.trim();
    if (!countryName) return;

    if (DEBUG) console.log(`[DESKTOP] Clicked travel button for: ${countryName}`);

    const parsed = getDataModel();
    const match = parsed?.destinations?.find(dest =>
      dest.country.toLowerCase() === countryName.toLowerCase()
    );

    if (!match) {
      console.warn(`⚠️ No destination found in data-model for ${countryName}`);
      return;
    }

    const methodInput = document.querySelector('fieldset.travelTypeSelector___zK5N4 input[name="travelType"]:checked');
    const selectedMethod = methodInput?.value;

    if (!selectedMethod) {
      console.warn("⚠️ Could not detect selected travel method.");
      return;
    }

    if (DEBUG) console.log(`📦 Selected travel method: ${selectedMethod}`);

    const ocReady = match[selectedMethod]?.ocReadyBeforeBack;
    if (DEBUG) console.log(`🚦 OC Ready Before Back: ${ocReady} for method: ${selectedMethod}`);

    if (ocReady === true) {
      disableContinueButton(countryName, selectedMethod);
    } else {
      if (DEBUG) console.log(`✅ Travel allowed to ${countryName} via ${selectedMethod}`);
    }
  };

  const handleMobileClick = (event) => {
    const button = event.target.closest("button");
    if (!button) return;

    const spans = button.querySelectorAll("span");
    const countryName = [...spans].map(s => s.textContent.trim())
      .find(t => /^[A-Za-z\s]+$/.test(t));

    if (!countryName) return;

    if (DEBUG) console.log(`[MOBILE] Clicked destination: ${countryName}`);

    const parsed = getDataModel();
    const match = parsed?.destinations?.find(dest =>
      dest.country.toLowerCase() === countryName.toLowerCase()
    );

    if (!match) {
      console.warn(`⚠️ No match found in data-model for ${countryName}`);
      return;
    }

    for (const key of ['standard', 'airstrip', 'private', 'business']) {
      const method = match[key];
      const ocReady = method?.ocReadyBeforeBack;
      if (DEBUG) console.log(`🔍 Checking ${key} => ocReadyBeforeBack: ${ocReady}`);
      if (ocReady === true) {
        disableContinueButton(countryName, key);
        return;
      }
    }

    if (DEBUG) console.log(`✅ Travel allowed to ${countryName} via all methods`);
  };

  const setupClickInterceptor = () => {
    const isMobile = window.matchMedia("(max-width: 600px)").matches;

    document.body.addEventListener("click", (e) => {
      if (!isEnabled) return;
      if (isMobile) {
        handleMobileClick(e);
      } else {
        handleDesktopClick(e);
      }
    });
  };

  const init = () => {
    console.log("🚀 OC Travel Blocker initialized.");
    injectToggle();
    setupClickInterceptor();
  };

  const observer = new MutationObserver(() => injectToggle());
  observer.observe(document.body, { childList: true, subtree: true });

  init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址