Torn War Tools

War target tools: travel warnings, available targets, med targets, sorting, shortlists

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Torn War Tools
// @namespace    https://www.torn.com/
// @version      2.3
// @description  War target tools: travel warnings, available targets, med targets, sorting, shortlists
// @author       Systoned
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// ==/UserScript==

(function () {
  'use strict';

  // -- Settings ---------------------------------------------------------

  function getApiKey() { return GM_getValue('wtw_api_key', ''); }
  function getEnemyFactionId() { return GM_getValue('wtw_enemy_faction_id', ''); }
  function setApiKey(v) { GM_setValue('wtw_api_key', v.trim()); }
  function setEnemyFactionId(v) { GM_setValue('wtw_enemy_faction_id', v.trim()); }
  function isConfigured() { return getApiKey() !== '' && getEnemyFactionId() !== ''; }

  // -- Shortlist --------------------------------------------------------

  function getShortlistKey() { return 'wtw_shortlist_' + getEnemyFactionId(); }
  function getShortlist() {
    try { return JSON.parse(GM_getValue(getShortlistKey(), '[]')); }
    catch (e) { return []; }
  }
  function saveShortlist(list) { GM_setValue(getShortlistKey(), JSON.stringify(list)); }
  function isShortlisted(id) { return getShortlist().indexOf(id) !== -1; }
  function toggleShortlist(id) {
    var list = getShortlist();
    var idx = list.indexOf(id);
    if (idx === -1) { list.push(id); } else { list.splice(idx, 1); }
    saveShortlist(list);
  }
  function clearShortlistForFaction(factionId) {
    GM_setValue('wtw_shortlist_' + factionId, '[]');
  }

  // -- Cache ------------------------------------------------------------

  var memberCache = { data: null, time: 0 };
  var CACHE_TTL = 60000; // 60 seconds

  function getCachedMembers() {
    if (memberCache.data && (Date.now() - memberCache.time) < CACHE_TTL) {
      return Promise.resolve(memberCache.data);
    }
    return fetchEnemyMembers().then(function (members) {
      memberCache.data = members;
      memberCache.time = Date.now();
      return members;
    });
  }

  function prefetchMembers() {
    if (!isConfigured()) return;
    getCachedMembers().catch(function () {});
  }

  // -- API --------------------------------------------------------------

  function fetchEnemyMembers() {
    return new Promise(function (resolve, reject) {
      GM_xmlhttpRequest({
        method: 'GET',
        url: 'https://api.torn.com/v2/faction/' + getEnemyFactionId() + '/members?striptags=true',
        headers: {
          'Authorization': 'ApiKey ' + getApiKey(),
          'Accept': 'application/json'
        },
        onload: function (r) {
          try {
            var data = JSON.parse(r.responseText);
            if (data.error) { reject(data.error.error || 'API error'); return; }
            resolve(data.members || []);
          } catch (e) { reject('Parse error'); }
        },
        onerror: function () { reject('Network error'); }
      });
    });
  }

  // -- Threat / Target Logic --------------------------------------------

  function findThreats(members, countryName) {
    var threats = [];
    var cl = countryName.toLowerCase();
    for (var i = 0; i < members.length; i++) {
      var m = members[i];
      var state = m.status && m.status.state;
      var desc = m.status && m.status.description;
      if (!desc) continue;
      var dl = desc.toLowerCase();
      if (state === 'Traveling' && dl.indexOf('traveling to ' + cl) !== -1) {
        threats.push({ id: m.id, name: m.name, level: m.level, type: 'traveling', description: desc });
      }
      if (state === 'Abroad' && dl.indexOf('in ' + cl) !== -1) {
        threats.push({ id: m.id, name: m.name, level: m.level, type: 'abroad', description: desc });
      }
    }
    return threats;
  }

  var currentSort = 'level'; // 'level' or 'activity'
  var sortAsc = false; // false = descending (default), true = ascending

  var activityOrder = { 'Online': 0, 'Idle': 1, 'Offline': 2, 'Unknown': 3 };

  function parseRelativeTime(str) {
    // Convert "X minutes ago", "X hours ago" etc to seconds
    if (!str || str === 'Unknown') return Infinity;
    var match = str.match(/(\d+)\s*(second|minute|hour|day|week|month|year)/i);
    if (!match) return Infinity;
    var num = parseInt(match[1], 10);
    var unit = match[2].toLowerCase();
    var multipliers = { second: 1, minute: 60, hour: 3600, day: 86400, week: 604800, month: 2592000, year: 31536000 };
    return num * (multipliers[unit] || 1);
  }

  function getAvailableTargets(members, sortMode, ascending) {
    var targets = [];
    for (var i = 0; i < members.length; i++) {
      var m = members[i];
      var state = m.status && m.status.state;
      if (state === 'Okay') {
        targets.push({
          id: m.id, name: m.name, level: m.level,
          lastAction: m.last_action ? m.last_action.relative : 'Unknown',
          actionStatus: m.last_action ? m.last_action.status : 'Unknown',
          lastActionSeconds: parseRelativeTime(m.last_action ? m.last_action.relative : 'Unknown')
        });
      }
    }

    if (sortMode === 'activity') {
      targets.sort(function (a, b) {
        var ao = activityOrder[a.actionStatus] !== undefined ? activityOrder[a.actionStatus] : 3;
        var bo = activityOrder[b.actionStatus] !== undefined ? activityOrder[b.actionStatus] : 3;
        if (ao !== bo) return ascending ? bo - ao : ao - bo;
        // Within same status group, sort by recency
        var recency = a.lastActionSeconds - b.lastActionSeconds;
        return ascending ? -recency : recency;
      });
    } else {
      targets.sort(function (a, b) {
        return ascending ? a.level - b.level : b.level - a.level;
      });
    }

    // Pin shortlisted to top
    var shortlist = getShortlist();
    if (shortlist.length > 0) {
      var pinned = [];
      var rest = [];
      for (var j = 0; j < targets.length; j++) {
        if (shortlist.indexOf(targets[j].id) !== -1) {
          pinned.push(targets[j]);
        } else {
          rest.push(targets[j]);
        }
      }
      targets = pinned.concat(rest);
    }

    return targets;
  }

  function getMedTargets(members) {
    var targets = [];
    var now = Math.floor(Date.now() / 1000);
    for (var i = 0; i < members.length; i++) {
      var m = members[i];
      var state = m.status && m.status.state;
      if (state === 'Hospital') {
        var until = m.status.until || 0;
        var remaining = until - now;
        if (remaining < 0) remaining = 0;
        targets.push({
          id: m.id, name: m.name, level: m.level,
          description: m.status.description || '',
          until: until,
          remaining: remaining
        });
      }
    }
    targets.sort(function (a, b) { return a.remaining - b.remaining; });
    return targets;
  }

  function formatTime(seconds) {
    if (seconds <= 0) return 'Now';
    var h = Math.floor(seconds / 3600);
    var m = Math.floor((seconds % 3600) / 60);
    var s = seconds % 60;
    var parts = [];
    if (h > 0) parts.push(h + 'h');
    if (m > 0) parts.push(m + 'm');
    if (h === 0) parts.push(s + 's');
    return parts.join(' ');
  }

  // -- Styles -----------------------------------------------------------

  function injectStyles() {
    var style = document.createElement('style');
    style.textContent = [
      // Modal overlay
      '.wtw-overlay {',
      '  position: fixed;',
      '  top: 0; left: 0; right: 0; bottom: 0;',
      '  background: rgba(0, 0, 0, 0.75);',
      '  z-index: 999999;',
      '  display: flex;',
      '  align-items: center;',
      '  justify-content: center;',
      '  padding: 20px;',
      '}',
      '.wtw-modal {',
      '  background: #1a1a1a;',
      '  border: 1px solid #444;',
      '  border-radius: 8px;',
      '  padding: 20px;',
      '  max-width: 500px;',
      '  width: 100%;',
      '  max-height: 80vh;',
      '  overflow-y: auto;',
      '  color: #e0e0e0;',
      '  font-family: Arial, Helvetica, sans-serif;',
      '  font-size: 13px;',
      '}',
      '.wtw-modal-warn { border-color: #cc3333; }',

      // Header
      '.wtw-header {',
      '  display: flex;',
      '  justify-content: space-between;',
      '  align-items: center;',
      '  margin: 0 0 16px 0;',
      '}',
      '.wtw-header-title {',
      '  font-size: 16px;',
      '  font-weight: bold;',
      '  color: #ccc;',
      '}',
      '.wtw-close {',
      '  background: none;',
      '  border: none;',
      '  color: #666;',
      '  font-size: 20px;',
      '  cursor: pointer;',
      '  padding: 0 4px;',
      '  line-height: 1;',
      '}',
      '.wtw-close:hover { color: #ccc; }',

      // Buttons row
      '.wtw-actions {',
      '  display: flex;',
      '  gap: 8px;',
      '  margin: 0 0 16px 0;',
      '  flex-wrap: wrap;',
      '}',
      '.wtw-action-btn {',
      '  flex: 1;',
      '  min-width: 120px;',
      '  padding: 8px 12px;',
      '  border: 1px solid #444;',
      '  background: #222;',
      '  color: #ccc;',
      '  border-radius: 4px;',
      '  cursor: pointer;',
      '  font-size: 12px;',
      '  font-weight: bold;',
      '  text-align: center;',
      '}',
      '.wtw-action-btn:hover { border-color: #666; color: #fff; }',
      '.wtw-action-btn.wtw-active { border-color: #4a4; color: #4a4; }',

      // Settings form
      '.wtw-form-row { margin: 0 0 10px 0; }',
      '.wtw-form-row label {',
      '  display: block;',
      '  color: #999;',
      '  margin: 0 0 3px 0;',
      '  font-size: 11px;',
      '}',
      '.wtw-form-row input {',
      '  width: 100%;',
      '  box-sizing: border-box;',
      '  background: #111;',
      '  border: 1px solid #444;',
      '  color: #ccc;',
      '  padding: 6px 8px;',
      '  border-radius: 3px;',
      '  font-size: 12px;',
      '  font-family: monospace;',
      '}',
      '.wtw-form-row input:focus { outline: none; border-color: #666; }',
      '.wtw-form-buttons { display: flex; gap: 8px; margin-top: 12px; }',
      '.wtw-form-btn {',
      '  padding: 6px 16px;',
      '  border: none;',
      '  border-radius: 3px;',
      '  font-size: 12px;',
      '  cursor: pointer;',
      '  font-weight: bold;',
      '}',
      '.wtw-form-save { background: #4a4; color: #fff; }',
      '.wtw-form-save:hover { background: #5b5; }',
      '.wtw-form-cancel { background: #333; color: #999; }',
      '.wtw-form-cancel:hover { background: #444; color: #ccc; }',

      // Settings bar (shown when configured)
      '.wtw-settings-bar {',
      '  display: flex;',
      '  align-items: center;',
      '  justify-content: space-between;',
      '  padding: 6px 0;',
      '  margin: 0 0 12px 0;',
      '  border-bottom: 1px solid #333;',
      '  font-size: 11px;',
      '}',
      '.wtw-settings-left {',
      '  display: flex;',
      '  align-items: center;',
      '  gap: 8px;',
      '}',
      '.wtw-dot {',
      '  width: 8px; height: 8px;',
      '  border-radius: 50%;',
      '  background: #4a4;',
      '}',
      '.wtw-settings-edit {',
      '  background: none;',
      '  border: 1px solid #444;',
      '  color: #999;',
      '  padding: 2px 8px;',
      '  border-radius: 3px;',
      '  cursor: pointer;',
      '  font-size: 11px;',
      '}',
      '.wtw-settings-edit:hover { border-color: #666; color: #ccc; }',

      // Target list
      '.wtw-list { margin: 0; padding: 0; list-style: none; }',
      '.wtw-list-item {',
      '  background: #222;',
      '  border-left: 3px solid #444;',
      '  padding: 8px 12px;',
      '  margin: 0 0 6px 0;',
      '  border-radius: 0 4px 4px 0;',
      '}',
      '.wtw-list-item-ok { border-left-color: #4a4; }',
      '.wtw-list-item-hosp { border-left-color: #cc8800; }',
      '.wtw-list-item-threat-traveling { border-left-color: #cc8800; }',
      '.wtw-list-item-threat-abroad { border-left-color: #cc3333; }',
      '.wtw-list-name {',
      '  font-weight: bold;',
      '  color: #fff;',
      '}',
      '.wtw-list-name a { color: #fff; text-decoration: underline; }',
      '.wtw-list-name a:hover { color: #cc3333; }',
      '.wtw-list-detail {',
      '  color: #999;',
      '  font-size: 11px;',
      '  margin-top: 2px;',
      '}',
      '.wtw-tag {',
      '  display: inline-block;',
      '  padding: 1px 6px;',
      '  border-radius: 3px;',
      '  font-size: 10px;',
      '  font-weight: bold;',
      '  margin-left: 6px;',
      '}',
      '.wtw-tag-online { background: #1a331a; color: #4a4; }',
      '.wtw-tag-idle { background: #332e1a; color: #cc8800; }',
      '.wtw-tag-offline { background: #2a2a2a; color: #666; }',
      '.wtw-tag-traveling { background: #332200; color: #cc8800; }',
      '.wtw-tag-abroad { background: #331111; color: #cc3333; }',

      // Status / empty
      '.wtw-status {',
      '  text-align: center;',
      '  color: #666;',
      '  padding: 20px 0;',
      '}',
      '.wtw-count {',
      '  color: #999;',
      '  font-size: 11px;',
      '  margin: 0 0 10px 0;',
      '}',

      // Travel warning title
      '.wtw-warn-title {',
      '  color: #cc3333;',
      '  font-size: 16px;',
      '  font-weight: bold;',
      '  margin: 0 0 4px 0;',
      '}',
      '.wtw-warn-sub {',
      '  color: #999;',
      '  font-size: 12px;',
      '  margin: 0 0 14px 0;',
      '}',
      '.wtw-dismiss {',
      '  display: block;',
      '  width: 100%;',
      '  padding: 10px;',
      '  border: none;',
      '  border-radius: 4px;',
      '  background: #cc3333;',
      '  color: #fff;',
      '  font-size: 13px;',
      '  font-weight: bold;',
      '  cursor: pointer;',
      '  margin-top: 14px;',
      '}',
      '.wtw-dismiss:hover { background: #aa2222; }',

      // Sidebar icon
      '.wtw-sidebar-icon path { fill: currentColor; }',

      // Sort controls
      '.wtw-sort-bar {',
      '  display: flex;',
      '  align-items: center;',
      '  gap: 6px;',
      '  margin: 0 0 10px 0;',
      '  font-size: 11px;',
      '  color: #666;',
      '}',
      '.wtw-sort-label { color: #666; }',
      '.wtw-sort-btn {',
      '  background: none;',
      '  border: 1px solid #333;',
      '  color: #999;',
      '  padding: 2px 8px;',
      '  border-radius: 3px;',
      '  cursor: pointer;',
      '  font-size: 11px;',
      '}',
      '.wtw-sort-btn:hover { border-color: #555; color: #ccc; }',
      '.wtw-sort-btn.wtw-sort-active { border-color: #4a4; color: #4a4; }',

      // Shortlist checkbox
      '.wtw-shortlist-cb {',
      '  float: right;',
      '  cursor: pointer;',
      '  width: 14px;',
      '  height: 14px;',
      '  accent-color: #4a4;',
      '  margin: 2px 0 0 0;',
      '}',

      // Shortlist header
      '.wtw-list-header {',
      '  display: flex;',
      '  justify-content: space-between;',
      '  font-size: 10px;',
      '  color: #666;',
      '  padding: 0 12px 4px 12px;',
      '  text-transform: uppercase;',
      '}',

      // Sort arrow
      '.wtw-sort-arrow {',
      '  margin-left: 3px;',
      '  font-size: 9px;',
      '}',

      // Pinned separator
      '.wtw-pinned-sep {',
      '  border: none;',
      '  border-top: 1px dashed #444;',
      '  margin: 8px 0;',
      '}'
    ].join('\n');
    document.head.appendChild(style);
  }

  // -- Modal Framework --------------------------------------------------

  var currentOverlay = null;

  function closeModal() {
    if (currentOverlay) {
      currentOverlay.remove();
      currentOverlay = null;
    }
  }

  function createOverlay(warnStyle) {
    closeModal();
    var overlay = document.createElement('div');
    overlay.className = 'wtw-overlay';
    overlay.addEventListener('click', function (e) {
      if (e.target === overlay) closeModal();
    });
    var modal = document.createElement('div');
    modal.className = 'wtw-modal' + (warnStyle ? ' wtw-modal-warn' : '');
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    currentOverlay = overlay;
    return modal;
  }

  // -- Main War Modal ---------------------------------------------------

  var currentView = null;

  function openWarModal() {
    var modal = createOverlay(false);

    var header = document.createElement('div');
    header.className = 'wtw-header';
    var title = document.createElement('span');
    title.className = 'wtw-header-title';
    title.textContent = 'War Tools';
    var closeBtn = document.createElement('button');
    closeBtn.className = 'wtw-close';
    closeBtn.textContent = '\u00D7';
    closeBtn.addEventListener('click', closeModal);
    header.appendChild(title);
    header.appendChild(closeBtn);
    modal.appendChild(header);

    var content = document.createElement('div');
    content.id = 'wtw-modal-content';
    modal.appendChild(content);

    if (!isConfigured()) {
      renderSettingsForm(content, false);
    } else {
      renderConfiguredView(content);
    }
  }

  function renderConfiguredView(container) {
    container.innerHTML = '';

    // Settings bar
    var bar = document.createElement('div');
    bar.className = 'wtw-settings-bar';
    var left = document.createElement('div');
    left.className = 'wtw-settings-left';
    var dot = document.createElement('span');
    dot.className = 'wtw-dot';
    var info = document.createElement('span');
    info.style.color = '#999';
    info.textContent = 'Enemy Faction: ' + getEnemyFactionId();
    left.appendChild(dot);
    left.appendChild(info);
    var editBtn = document.createElement('button');
    editBtn.className = 'wtw-settings-edit';
    editBtn.textContent = 'Edit';
    editBtn.addEventListener('click', function () {
      renderSettingsForm(container, true);
    });
    bar.appendChild(left);
    bar.appendChild(editBtn);
    container.appendChild(bar);

    // Action buttons
    var actions = document.createElement('div');
    actions.className = 'wtw-actions';

    var availBtn = document.createElement('button');
    availBtn.className = 'wtw-action-btn';
    availBtn.textContent = 'Available Targets';
    availBtn.addEventListener('click', function () {
      setActiveBtn(availBtn);
      loadAvailableTargets(listArea);
    });

    var medBtn = document.createElement('button');
    medBtn.className = 'wtw-action-btn';
    medBtn.textContent = 'Med Targets';
    medBtn.addEventListener('click', function () {
      setActiveBtn(medBtn);
      loadMedTargets(listArea);
    });

    actions.appendChild(availBtn);
    actions.appendChild(medBtn);
    container.appendChild(actions);

    var listArea = document.createElement('div');
    listArea.id = 'wtw-list-area';
    container.appendChild(listArea);

    function setActiveBtn(active) {
      var btns = actions.querySelectorAll('.wtw-action-btn');
      for (var i = 0; i < btns.length; i++) {
        btns[i].classList.remove('wtw-active');
      }
      active.classList.add('wtw-active');
    }
  }

  function renderSettingsForm(container, hasCancel) {
    container.innerHTML = '';

    var row1 = document.createElement('div');
    row1.className = 'wtw-form-row';
    var lbl1 = document.createElement('label');
    lbl1.textContent = 'Torn API Key';
    var inp1 = document.createElement('input');
    inp1.type = 'password';
    inp1.placeholder = 'Enter your API key';
    inp1.value = getApiKey();
    row1.appendChild(lbl1);
    row1.appendChild(inp1);

    var row2 = document.createElement('div');
    row2.className = 'wtw-form-row';
    var lbl2 = document.createElement('label');
    lbl2.textContent = 'Enemy Faction ID';
    var inp2 = document.createElement('input');
    inp2.type = 'text';
    inp2.placeholder = 'e.g. 32395';
    inp2.value = getEnemyFactionId();
    row2.appendChild(lbl2);
    row2.appendChild(inp2);

    var buttons = document.createElement('div');
    buttons.className = 'wtw-form-buttons';

    var saveBtn = document.createElement('button');
    saveBtn.className = 'wtw-form-btn wtw-form-save';
    saveBtn.textContent = 'Save';
    saveBtn.addEventListener('click', function () {
      var key = inp1.value.trim();
      var fid = inp2.value.trim();
      if (!key) { inp1.style.borderColor = '#cc3333'; return; }
      if (!fid || isNaN(fid)) { inp2.style.borderColor = '#cc3333'; return; }
      var oldFid = getEnemyFactionId();
      setApiKey(key);
      setEnemyFactionId(fid);
      if (oldFid && oldFid !== fid) { clearShortlistForFaction(oldFid); }
      memberCache.data = null;
      memberCache.time = 0;
      prefetchMembers();
      renderConfiguredView(container);
    });
    buttons.appendChild(saveBtn);

    if (hasCancel) {
      var cancelBtn = document.createElement('button');
      cancelBtn.className = 'wtw-form-btn wtw-form-cancel';
      cancelBtn.textContent = 'Cancel';
      cancelBtn.addEventListener('click', function () {
        renderConfiguredView(container);
      });
      buttons.appendChild(cancelBtn);
    }

    container.appendChild(row1);
    container.appendChild(row2);
    container.appendChild(buttons);
  }

  // -- List Renderers ---------------------------------------------------

  function loadAvailableTargets(container) {
    container.innerHTML = '';
    var status = document.createElement('div');
    status.className = 'wtw-status';
    status.textContent = 'Loading...';
    container.appendChild(status);

    getCachedMembers().then(function (members) {
      container.innerHTML = '';
      var targets = getAvailableTargets(members, currentSort, sortAsc);
      var shortlist = getShortlist();

      // Sort bar
      var sortBar = document.createElement('div');
      sortBar.className = 'wtw-sort-bar';
      var sortLabel = document.createElement('span');
      sortLabel.className = 'wtw-sort-label';
      sortLabel.textContent = 'Sort:';
      sortBar.appendChild(sortLabel);

      var levelBtn = document.createElement('button');
      levelBtn.className = 'wtw-sort-btn' + (currentSort === 'level' ? ' wtw-sort-active' : '');
      levelBtn.innerHTML = 'Level' + (currentSort === 'level' ? '<span class="wtw-sort-arrow">' + (sortAsc ? '&#9650;' : '&#9660;') + '</span>' : '');
      levelBtn.addEventListener('click', function () {
        if (currentSort === 'level') {
          sortAsc = !sortAsc;
        } else {
          currentSort = 'level';
          sortAsc = false;
        }
        loadAvailableTargets(container);
      });
      sortBar.appendChild(levelBtn);

      var actBtn = document.createElement('button');
      actBtn.className = 'wtw-sort-btn' + (currentSort === 'activity' ? ' wtw-sort-active' : '');
      actBtn.innerHTML = 'Last Active' + (currentSort === 'activity' ? '<span class="wtw-sort-arrow">' + (sortAsc ? '&#9650;' : '&#9660;') + '</span>' : '');
      actBtn.addEventListener('click', function () {
        if (currentSort === 'activity') {
          sortAsc = !sortAsc;
        } else {
          currentSort = 'activity';
          sortAsc = false;
        }
        loadAvailableTargets(container);
      });
      sortBar.appendChild(actBtn);
      container.appendChild(sortBar);

      var count = document.createElement('div');
      count.className = 'wtw-count';
      var pinnedCount = 0;
      for (var c = 0; c < targets.length; c++) {
        if (shortlist.indexOf(targets[c].id) !== -1) pinnedCount++;
      }
      count.textContent = targets.length + ' available target' + (targets.length !== 1 ? 's' : '') +
        (pinnedCount > 0 ? ' (' + pinnedCount + ' shortlisted)' : '');
      container.appendChild(count);

      if (targets.length === 0) {
        var empty = document.createElement('div');
        empty.className = 'wtw-status';
        empty.textContent = 'No available targets';
        container.appendChild(empty);
        return;
      }

      // Column header
      var listHeader = document.createElement('div');
      listHeader.className = 'wtw-list-header';
      var targetLabel = document.createElement('span');
      targetLabel.textContent = 'Target';
      var shortlistLabel = document.createElement('span');
      shortlistLabel.textContent = 'Shortlist';
      listHeader.appendChild(targetLabel);
      listHeader.appendChild(shortlistLabel);
      container.appendChild(listHeader);

      var list = document.createElement('ul');
      list.className = 'wtw-list';
      var separatorAdded = false;

      for (var i = 0; i < targets.length; i++) {
        var t = targets[i];
        var isPinned = shortlist.indexOf(t.id) !== -1;

        // Add separator after last pinned item
        if (!isPinned && !separatorAdded && pinnedCount > 0) {
          var sep = document.createElement('hr');
          sep.className = 'wtw-pinned-sep';
          list.appendChild(sep);
          separatorAdded = true;
        }

        var li = document.createElement('li');
        li.className = 'wtw-list-item wtw-list-item-ok';

        var cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.className = 'wtw-shortlist-cb';
        cb.checked = isPinned;
        cb.title = isPinned ? 'Remove from shortlist' : 'Add to shortlist';
        (function (tid) {
          cb.addEventListener('change', function () {
            toggleShortlist(tid);
            loadAvailableTargets(container);
          });
        })(t.id);
        li.appendChild(cb);

        var nameEl = document.createElement('div');
        nameEl.className = 'wtw-list-name';
        var link = document.createElement('a');
        link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
        link.target = '_blank';
        link.textContent = t.name;
        nameEl.appendChild(link);

        var tag = document.createElement('span');
        var tagClass = 'wtw-tag wtw-tag-' + t.actionStatus.toLowerCase();
        tag.className = tagClass;
        tag.textContent = t.actionStatus;
        nameEl.appendChild(tag);

        var detail = document.createElement('div');
        detail.className = 'wtw-list-detail';
        detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.lastAction;

        li.appendChild(nameEl);
        li.appendChild(detail);
        list.appendChild(li);
      }
      container.appendChild(list);
    }).catch(function (err) {
      container.innerHTML = '';
      var errEl = document.createElement('div');
      errEl.className = 'wtw-status';
      errEl.style.color = '#cc3333';
      errEl.textContent = 'Error: ' + err;
      container.appendChild(errEl);
    });
  }

  function loadMedTargets(container) {
    container.innerHTML = '';
    var status = document.createElement('div');
    status.className = 'wtw-status';
    status.textContent = 'Loading...';
    container.appendChild(status);

    getCachedMembers().then(function (members) {
      container.innerHTML = '';
      var targets = getMedTargets(members);

      var count = document.createElement('div');
      count.className = 'wtw-count';
      count.textContent = targets.length + ' hospitalised member' + (targets.length !== 1 ? 's' : '');
      container.appendChild(count);

      if (targets.length === 0) {
        var empty = document.createElement('div');
        empty.className = 'wtw-status';
        empty.textContent = 'No members in hospital';
        container.appendChild(empty);
        return;
      }

      var list = document.createElement('ul');
      list.className = 'wtw-list';
      for (var i = 0; i < targets.length; i++) {
        var t = targets[i];
        var li = document.createElement('li');
        li.className = 'wtw-list-item wtw-list-item-hosp';

        var isPinned = isShortlisted(t.id);
        var cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.className = 'wtw-shortlist-cb';
        cb.checked = isPinned;
        cb.title = isPinned ? 'Remove from shortlist' : 'Add to shortlist';
        (function (tid) {
          cb.addEventListener('change', function () {
            toggleShortlist(tid);
            loadMedTargets(container);
          });
        })(t.id);
        li.appendChild(cb);

        var nameEl = document.createElement('div');
        nameEl.className = 'wtw-list-name';
        var link = document.createElement('a');
        link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
        link.target = '_blank';
        link.textContent = t.name;
        nameEl.appendChild(link);

        var tag = document.createElement('span');
        tag.className = 'wtw-tag wtw-tag-idle';
        tag.textContent = formatTime(t.remaining);
        nameEl.appendChild(tag);

        var detail = document.createElement('div');
        detail.className = 'wtw-list-detail';
        detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.description;

        li.appendChild(nameEl);
        li.appendChild(detail);
        list.appendChild(li);
      }
      container.appendChild(list);
    }).catch(function (err) {
      container.innerHTML = '';
      var errEl = document.createElement('div');
      errEl.className = 'wtw-status';
      errEl.style.color = '#cc3333';
      errEl.textContent = 'Error: ' + err;
      container.appendChild(errEl);
    });
  }

  // -- Travel Warning Modal ---------------------------------------------

  function showTravelWarning(countryName, threats) {
    var modal = createOverlay(true);

    var title = document.createElement('div');
    title.className = 'wtw-warn-title';
    title.textContent = 'War Targets in ' + countryName;

    var sub = document.createElement('div');
    sub.className = 'wtw-warn-sub';
    sub.textContent = threats.length + ' enemy member' + (threats.length !== 1 ? 's' : '') + ' detected:';

    modal.appendChild(title);
    modal.appendChild(sub);

    var list = document.createElement('ul');
    list.className = 'wtw-list';
    for (var i = 0; i < threats.length; i++) {
      var t = threats[i];
      var li = document.createElement('li');
      li.className = 'wtw-list-item wtw-list-item-threat-' + t.type;

      var nameEl = document.createElement('div');
      nameEl.className = 'wtw-list-name';
      var link = document.createElement('a');
      link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
      link.target = '_blank';
      link.textContent = t.name;
      nameEl.appendChild(link);

      var tag = document.createElement('span');
      tag.className = 'wtw-tag wtw-tag-' + t.type;
      tag.textContent = t.type === 'traveling' ? 'EN ROUTE' : 'IN COUNTRY';
      nameEl.appendChild(tag);

      var detail = document.createElement('div');
      detail.className = 'wtw-list-detail';
      detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.description;

      li.appendChild(nameEl);
      li.appendChild(detail);
      list.appendChild(li);
    }
    modal.appendChild(list);

    var dismiss = document.createElement('button');
    dismiss.className = 'wtw-dismiss';
    dismiss.textContent = 'Dismiss';
    dismiss.addEventListener('click', closeModal);
    modal.appendChild(dismiss);
  }

  // -- Sidebar Injection ------------------------------------------------

  var warSvg = '<svg xmlns="http://www.w3.org/2000/svg" class="wtw-sidebar-icon" stroke="transparent" stroke-width="0" width="16" height="16" viewBox="0 0 16 16"><path d="M8,0L0,3v5c0,4.4,3.4,8.5,8,10c4.6-1.5,8-5.6,8-10V3L8,0z M12,9H9v3H7V9H4V7h3V4h2v3h3V9z"/></svg>';

  function injectDesktopSidebar() {
    var travelNav = document.querySelector('#nav-travel_agency');
    if (!travelNav) return false;
    if (document.querySelector('#nav-war_tools')) return true;

    var wrapper = document.createElement('div');
    wrapper.className = travelNav.className;
    wrapper.id = 'nav-war_tools';

    var row = document.createElement('div');
    row.className = 'area-row___iBD8N';

    var link = document.createElement('a');
    link.href = '#';
    link.className = travelNav.querySelector('a') ? travelNav.querySelector('a').className : '';
    link.addEventListener('click', function (e) {
      e.preventDefault();
      openWarModal();
    });

    var iconWrap = document.createElement('span');
    iconWrap.className = 'svgIconWrap___AMIqR';
    var iconInner = document.createElement('span');
    iconInner.className = 'defaultIcon___iiNis mobile___paLva';
    iconInner.innerHTML = warSvg;
    iconWrap.appendChild(iconInner);

    var nameSpan = document.createElement('span');
    nameSpan.className = 'linkName___FoKha';
    nameSpan.textContent = 'War Tools';

    link.appendChild(iconWrap);
    link.appendChild(nameSpan);
    row.appendChild(link);
    wrapper.appendChild(row);

    travelNav.parentNode.insertBefore(wrapper, travelNav.nextSibling);
    return true;
  }

  function injectMobileSidebar() {
    var travelMobile = document.querySelector('#sidebar #nav-travel_agency');
    if (!travelMobile) return false;
    if (document.querySelector('#nav-war_tools_mobile')) return true;

    var slide = travelMobile.closest('[class^="swiper-slide"]');
    if (!slide) return false;

    var newSlide = document.createElement('div');
    newSlide.className = slide.className;
    newSlide.style.cssText = slide.style.cssText;

    var wrapper = document.createElement('div');
    wrapper.className = travelMobile.className;
    wrapper.id = 'nav-war_tools_mobile';

    var row = document.createElement('div');
    row.className = 'area-row___iBD8N';

    var link = document.createElement('a');
    link.href = '#';
    link.tabIndex = 0;
    var mobileLink = travelMobile.querySelector('a');
    link.className = mobileLink ? mobileLink.className : '';
    link.addEventListener('click', function (e) {
      e.preventDefault();
      openWarModal();
    });

    var iconWrap = document.createElement('span');
    iconWrap.className = 'svgIconWrap___AMIqR';
    var iconInner = document.createElement('span');
    iconInner.className = 'defaultIcon___iiNis mobile___paLva';
    iconInner.innerHTML = warSvg;
    iconWrap.appendChild(iconInner);

    var nameSpan = document.createElement('span');
    nameSpan.textContent = 'War';

    link.appendChild(iconWrap);
    link.appendChild(nameSpan);
    row.appendChild(link);
    wrapper.appendChild(row);
    newSlide.appendChild(wrapper);

    slide.parentNode.insertBefore(newSlide, slide.nextSibling);
    return true;
  }

  function injectSidebar() {
    injectDesktopSidebar();
    injectMobileSidebar();
  }

  // -- Travel Page: Destination Click Interception ----------------------

  function isTravelPage() {
    return window.location.href.indexOf('sid=travel') !== -1 ||
           window.location.href.indexOf('travelagency') !== -1;
  }

  function getCountryFromDesktopButton(el) {
    var s = el.querySelector('[class^="country"]');
    if (s) return s.textContent.trim();
    var n = el.querySelector('[class^="name"]');
    if (n) {
      var t = n.textContent.trim();
      var d = t.indexOf(' -');
      return d > 0 ? t.substring(0, d).trim() : t;
    }
    return null;
  }

  function getCountryFromMapPin(el) {
    var label = el.getAttribute('aria-label');
    if (label) {
      var d = label.indexOf(' -');
      return d > 0 ? label.substring(0, d).trim() : label.trim();
    }
    var parent = el.closest('[class^="destinationLabel"]');
    if (parent) {
      var radio = parent.querySelector('input[aria-label]');
      if (radio) {
        var rl = radio.getAttribute('aria-label');
        var di = rl.indexOf(' -');
        return di > 0 ? rl.substring(0, di).trim() : rl.trim();
      }
    }
    return null;
  }

  function extractCountryFromClick(e) {
    var target = e.target;

    // Desktop list
    var desktopBtn = target.closest('button[class^="expandButton"]');
    if (desktopBtn) {
      var dest = desktopBtn.closest('[class^="destination"]');
      return getCountryFromDesktopButton(dest || desktopBtn);
    }

    // Map label
    var mapLabel = target.closest('[class^="destinationLabel"]');
    if (mapLabel) {
      var radio = mapLabel.querySelector('input[aria-label]');
      if (radio) return getCountryFromMapPin(radio);
    }

    // Map pin
    var pin = target.closest('[class^="pin"]');
    if (pin) {
      var pl = pin.closest('[class^="destinationLabel"]');
      if (pl) {
        var pr = pl.querySelector('input[aria-label]');
        if (pr) return getCountryFromMapPin(pr);
      }
    }

    return null;
  }

  var travelCheckInProgress = false;

  function attachTravelListeners() {
    document.addEventListener('click', function (e) {
      if (!isTravelPage()) return;
      if (!isConfigured()) return;
      if (travelCheckInProgress) return;
      if (e.target.closest('#wtw-container') || e.target.closest('.wtw-overlay')) return;

      var country = extractCountryFromClick(e);
      if (!country) return;

      // If cache is ready, check synchronously
      if (memberCache.data && (Date.now() - memberCache.time) < CACHE_TTL) {
        var threats = findThreats(memberCache.data, country);
        if (threats.length > 0) {
          e.preventDefault();
          e.stopPropagation();
          e.stopImmediatePropagation();
          showTravelWarning(country, threats);
        }
        // No threats: click passes through naturally
        return;
      }

      // No cache -- need to fetch, must block
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation();
      travelCheckInProgress = true;

      var loadOverlay = createOverlay(false);
      var loadModal = loadOverlay.querySelector('.wtw-modal');
      var loadMsg = document.createElement('div');
      loadMsg.className = 'wtw-status';
      loadMsg.textContent = 'Checking war targets...';
      loadModal.appendChild(loadMsg);

      getCachedMembers().then(function (members) {
        closeModal();
        travelCheckInProgress = false;
        var threats = findThreats(members, country);
        if (threats.length > 0) {
          showTravelWarning(country, threats);
        }
        // No threats: user clicks again, cache is now ready, passes through
      }).catch(function (err) {
        closeModal();
        travelCheckInProgress = false;
        console.error('[WTW] Fetch error:', err);
        // Fail open -- don't block travel on error
      });
    }, true);
  }

  // -- Init -------------------------------------------------------------

  injectStyles();

  function init() {
    injectSidebar();
    attachTravelListeners();
    prefetchMembers();

    // Re-inject sidebar if SPA removes it
    new MutationObserver(function () {
      if (!document.querySelector('#nav-war_tools')) {
        injectDesktopSidebar();
      }
      if (!document.querySelector('#nav-war_tools_mobile')) {
        injectMobileSidebar();
      }
    }).observe(document.body, { childList: true, subtree: true });

    // Refresh cache periodically
    setInterval(function () {
      if (isConfigured()) prefetchMembers();
    }, CACHE_TTL);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    setTimeout(init, 300);
  }

  console.log('[War Tools] v2.1 loaded');
})();