YATA Target Call Copier (v1.6.26)

Adds a Call button to the YATA Target List for easier callouts during Ranked Wars

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YATA Target Call Copier (v1.6.26)
// @namespace    torn.yata.call.btn
// @version      1.6.26
// @description  Adds a Call button to the YATA Target List for easier callouts during Ranked Wars
// @match        https://yata.yt/target/*
// @match        https://yata.yt/target/
// @grant        none
// @license      MIT
// @run-at       document-end
// @homepageURL  https://greasyfork.org/scripts/545505
// @supportURL   https://greasyfork.org/scripts/545505/feedback
// ==/UserScript==

(function () {
  'use strict';

  // ---------- Templates ----------
  // default & emoji:
  //   Left click:   no time
  //   Shift+Left:   include time (2nd line)
  //   Alt+Click :   include ID (after name, wrapped in [ ])
  //   Ctrl+Click:   UPPERCASE
  //
  // ASCII template ignores ALL modifiers and always copies:
  //   (leading newline)
  //   ╭══ … CALLING … ╮   ← top & bottom expand with extra "═" to roughly match middle width
  //        {name} [id]    ← roughly centered; ID wrapped in [ ]
  //   ╰═══ {time|Ready} ═══╯
  var TEMPLATES = {
    "default": "Calling: {name}{id}{time}",
    "emoji":   "🎯 Calling: {name}{id}{time}",
    "ascii":   "ASCII_BOX"
  };

  var CURRENT_TEMPLATE = localStorage.getItem('yata-template') || 'default';
  if (CURRENT_TEMPLATE === 'loud')  CURRENT_TEMPLATE = 'emoji';
  if (CURRENT_TEMPLATE === 'short') CURRENT_TEMPLATE = 'ascii';

  var TARGET_TABLE = null;
  var TBODY_OBSERVER = null;

  // ---------- Styles ----------
  (function injectStyles(){
    var css = ''
      /* Call column sizing & button */
      + '.call-col-th, .call-col-td{width:80px;min-width:80px;box-sizing:border-box;text-align:center;white-space:nowrap;}'
      + '.call-col-td{padding:0 10px;}'
      + '.yata-call-btn{display:inline-block;padding:2px 6px;font-size:12px;line-height:1.4;border:1px solid #6b7280;border-radius:4px;background:#f3f4f6;cursor:pointer;white-space:nowrap;transition:transform .05s ease,background .15s ease,border-color .15s ease;}'
      + '.yata-call-btn:hover{background:#e5e7eb;}'
      + '.yata-call-btn:active{transform:scale(0.98);}'
      + '.yata-call-btn--ok{background:#d1fae5!important;border-color:#10b981!important;}'
      + '.yata-call-btn--err{background:#fee2e2!important;border-color:#ef4444!important;}'

      /* Hidden helper for reliable Notes sorting */
      + '.yata-notes-sortkey{display:none !important;}'

      /* Header-integrated template picker */
      + '.yata-template-wrap{display:flex;align-items:center;gap:.35rem;}'
      + '.yata-template-label{font: inherit; color: inherit; letter-spacing: inherit;}'
      + '.yata-template-select{'
        + 'appearance:auto; -webkit-appearance:menulist; -moz-appearance:menulist;'
        + 'font: inherit; line-height: 1.2;'
        + 'padding:2px 6px; border:1px solid #c9c9c9; border-radius:4px;'
        + 'background:#fff !important; color:#4a4a4a !important;'
        + 'min-width:130px;'
      + '}'
      + '.yata-template-select option{background:#fff !important; color:#4a4a4a !important;}'
      + '.yata-template-select:focus{outline:none; box-shadow:0 0 0 2px rgba(68,126,155,0.25);}';
    var style = document.createElement('style');
    style.type = 'text/css';
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);
  })();

  // ---------- Small helpers ----------
  function round10(s){ s = Math.max(0, Math.floor((s+5)/10)*10); return s; }
  function fmtDur(sec){
    sec = round10(sec);
    var h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = sec%60;
    if (h) return h+'h '+m+'m '+s+'s';
    if (m) return m+'m '+s+'s';
    return s+'s';
  }
  function parseHospFromText(txt){
    if (!txt) return null;
    var m = txt.match(/H\s*for\s*(\d{1,2}:)?\d{1,2}:\d{2}/i);
    if (!m) return null;
    var parts = m[0].split(/\s*for\s*/i).pop().trim().split(':').map(function(n){ return parseInt(n,10); });
    if (parts.length === 2) return parts[0]*60 + parts[1];
    if (parts.length === 3) return parts[0]*3600 + parts[1]*60 + parts[2];
    return null;
  }
  function hospLeft(tr){
    var statusCell = tr.querySelector('td.text-start.status');
    if (!statusCell) return 0;
    var isHosp = statusCell.classList.contains('player-status-red') || /H\s*for/i.test(statusCell.textContent||'');
    if (!isHosp) return 0;
    var dv = statusCell.getAttribute('data-val');
    if (dv && /^\d+$/.test(dv)){
      var t = parseInt(dv,10)*1000;
      return Math.ceil((t - Date.now())/1000);
    }
    var fromTxt = parseHospFromText(statusCell.textContent||'');
    return (fromTxt != null) ? fromTxt : 0;
  }
  function etaTooltip(tr){
    var statusCell = tr.querySelector('td.text-start.status');
    if (statusCell){
      var dv = statusCell.getAttribute('data-val');
      if (dv && /^\d+$/.test(dv)){
        var d = new Date(parseInt(dv,10)*1000);
        return 'Out at: '+d.toLocaleTimeString()+' ('+d.toLocaleDateString()+')';
      }
    }
    var s = hospLeft(tr);
    return s>0 ? 'ETA: '+fmtDur(s) : 'Ready';
  }
  function colorize(btn,tr){
    var s = tr.querySelector('td.text-start.status');
    btn.style.background = '#f3f4f6';
    btn.style.borderColor = '#6b7280';
    if (!s) return;
    if (s.classList.contains('player-status-green')){
      btn.style.background = '#e6ffed';
      btn.style.borderColor = '#10b981';
    } else if (s.classList.contains('player-status-red')){
      btn.style.background = '#ffe4e6';
      btn.style.borderColor = '#ef4444';
    } else {
      btn.style.background = '#f3f4f6';
      btn.style.borderColor = '#9ca3af';
    }
  }
  function copyText(t){
    if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(t);
    return new Promise(function(resolve,reject){
      var ta = document.createElement('textarea');
      ta.value = t; ta.style.position='fixed'; ta.style.top='-9999px';
      document.body.appendChild(ta); ta.select();
      var ok = document.execCommand('copy');
      document.body.removeChild(ta);
      ok ? resolve() : reject(new Error('copy failed'));
    });
  }
  function flash(btn, kind){
    var cls = (kind==='ok') ? 'yata-call-btn--ok' : 'yata-call-btn--err';
    var prev = btn.textContent;
    btn.classList.add(cls);
    btn.textContent = (kind==='ok') ? 'Copied!' : 'Failed';
    setTimeout(function(){ btn.classList.remove(cls); btn.textContent = prev; }, 850);
  }

  // ---------- tablesorter nudge (minimal) ----------
  var tsTimer = null;
  function scheduleTablesorterUpdate(){
    if (tsTimer) return;
    tsTimer = setTimeout(function(){
      tsTimer = null;
      try {
        var $ = window.jQuery || window.$;
        var table = TARGET_TABLE || document.querySelector('table.tablesorter') || document.querySelector('table');
        if ($ && table) $(table).trigger('update');
      } catch(err){ /* ignore */ }
    }, 60);
  }

  // ---------- Header insertion ----------
  function insertCallHeader(){
    var headerRow = document.querySelector('thead tr.tablesorter-headerRow');
    if (!headerRow || headerRow.dataset.callColAdded) return false;

    TARGET_TABLE = headerRow.closest('table');

    var resultTh = headerRow.querySelector('th[data-column="1"]');
    if (!resultTh) return false;

    var th = document.createElement('th');
    th.className = 'a tablesorter-header tablesorter-headerUnSorted call-col-th sorter-false';
    th.setAttribute('title','Quick call');
    th.setAttribute('tabindex','0');
    th.setAttribute('scope','col');
    th.setAttribute('role','columnheader');
    th.setAttribute('aria-disabled','true'); // non-sortable
    th.setAttribute('data-sorter','false');
    th.setAttribute('unselectable','on');
    th.setAttribute('aria-sort','none');
    var ac = resultTh.getAttribute('aria-controls');
    if (ac) th.setAttribute('aria-controls', ac);
    th.style.userSelect = 'none';

    var inner = document.createElement('div');
    inner.className = 'tablesorter-header-inner';
    inner.textContent = 'Call';
    th.appendChild(inner);

    resultTh.insertAdjacentElement('afterend', th);
    headerRow.dataset.callColAdded = '1';

    scheduleTablesorterUpdate();
    return true;
  }

  // ---------- Row utilities ----------
  function cellEndingAtColumn(tr, targetEndCol){
    var col = 0, i, td, span;
    for (i=0; i<tr.children.length; i++){
      td = tr.children[i];
      span = parseInt(td.getAttribute('colspan')||'1',10) || 1;
      col += span;
      if (col === targetEndCol) return td;
    }
    return null;
  }

  // Ensure Call cell exists; returns {cell, created}
  function ensureCallCell(tr){
    var existing = tr.querySelector('td.call-col-td');
    if (existing) return { cell: existing, created: false };

    if (tr.children.length === 1){
      var only = tr.children[0];
      if (only.hasAttribute('colspan')){
        var cs = parseInt(only.getAttribute('colspan'),10);
        if (!isNaN(cs)) only.setAttribute('colspan', String(cs+1));
      }
      return { cell: null, created: false };
    }

    var endAt2 = cellEndingAtColumn(tr, 2);
    if (!endAt2) return { cell: null, created: false };

    var td = document.createElement('td');
    td.className = 'call-col-td';
    endAt2.insertAdjacentElement('afterend', td);
    return { cell: td, created: true };
  }

  function getNameVariants(tr){
    var link = tr.querySelector('a[href*="profiles.php?XID="]');
    if (!link) return {};
    var full = (link.textContent || '').trim();
    var nameOnly = full.replace(/\s*\[\d+\]\s*$/, '').trim();

    var idMatch = full.match(/\[(\d+)\]/);
    var id = idMatch ? idMatch[1] : null;
    if (!id && link.href){
      var m = link.href.match(/XID=(\d+)/i);
      if (m) id = m[1];
    }
    return { full: full, nameOnly: nameOnly, id: id };
  }

  function watchStatus(tr, btn, baseHint){
    var statusCell = tr.querySelector('td.text-start.status');
    if (!statusCell) return;
    var update = function(){
      btn.title = baseHint + '\n' + etaTooltip(tr);
      colorize(btn, tr);
    };
    var mo = new MutationObserver(update);
    mo.observe(statusCell, { characterData:true, subtree:true, attributes:true, attributeFilter:['data-val','class','title'] });
    update();
    tr._yataStatusWatch = mo;
  }

  // ---------- Notes: hidden sort key ----------
  function findNotesTd(tr){
    for (var i=0; i<tr.children.length; i++){
      var td = tr.children[i];
      if (td.querySelector && td.querySelector('input.target-list-note')) return td;
    }
    return null;
  }

  function ensureNotesSortKey(tr){
    var td = findNotesTd(tr);
    if (!td) return {updated:false};
    var input = td.querySelector('input.target-list-note');
    var val = '';
    if (input) val = (input.value || '').trim();

    var span = td.querySelector('.yata-notes-sortkey');
    if (!span){
      span = document.createElement('span');
      span.className = 'yata-notes-sortkey';
      td.insertBefore(span, td.firstChild);
    }
    var changed = (span.textContent !== val);
    if (changed) span.textContent = val;

    if (td.getAttribute('data-sort-value') !== val) {
      td.setAttribute('data-sort-value', val);
      changed = true;
    }
    return {updated: changed};
  }

  function watchNotes(tr){
    var td = findNotesTd(tr);
    if (!td) return;
    var input = td.querySelector('input.target-list-note');
    if (!input || input._yataNotesWatch) return;
    var handler = function(){
      var res = ensureNotesSortKey(tr);
      if (res.updated) scheduleTablesorterUpdate();
    };
    input.addEventListener('input', handler);
    input.addEventListener('change', handler);
    input._yataNotesWatch = true;
  }

  // ---------- Message builders ----------
  function buildAsciiMessage(opts){
    // Always include ID if present; always include time (or "Ready")
    var name = opts.nameOnly || '';
    var idPart = opts.id ? (' [' + opts.id + ']') : '';
    var timeTxt = (opts.timeSec > 0) ? fmtDur(opts.timeSec) : 'Ready';

    // Base building blocks
    var coreTop = ' 🎯 CALLING ';
    var leftCapTop = '╭', rightCapTop = '╮';
    var leftCapBot = '╰', rightCapBot = '╯';

    // Base counts of '='
    var topLeftEqBase = 1, topRightEqBase = 1;
    var botLeftEqBase = 3, botRightEqBase = 3;

    // Build minimal variants for measurement
    function topWithCounts(l, r){
      return leftCapTop + '═'.repeat(l) + coreTop + '═'.repeat(r) + rightCapTop;
    }
    function bottomWithCounts(l, r){
      return leftCapBot + '═'.repeat(l) + ' ' + timeTxt + ' ' + '═'.repeat(r) + rightCapBot;
    }

    var topMin = topWithCounts(topLeftEqBase, topRightEqBase);
    var bottomMin = bottomWithCounts(botLeftEqBase, botRightEqBase);

    var midCore = name + idPart;
    var midLen  = midCore.length;
    var topMinLen = topMin.length;
    var bottomMinLen = bottomMin.length;

    // Choose a target width: must accommodate the widest of (middle, topMin, bottomMin)
    var targetWidth = Math.max(midLen, topMinLen, bottomMinLen);

    // Expand TOP to target
    var addTop = Math.max(0, targetWidth - topMinLen);
    var addTopLeft  = Math.floor(addTop / 2);
    var addTopRight = addTop - addTopLeft;
    var top = topWithCounts(topLeftEqBase + addTopLeft, topRightEqBase + addTopRight);

    // Expand BOTTOM to target
    var addBot = Math.max(0, targetWidth - bottomMinLen);
    var addBotLeft  = Math.floor(addBot / 2);
    var addBotRight = addBot - addBotLeft;
    var bottom = bottomWithCounts(botLeftEqBase + addBotLeft, botRightEqBase + addBotRight);

    // Recompute final target (in case rounding produced off-by-one)
    var finalWidth = Math.max(top.length, bottom.length, targetWidth);

    // Re-balance if needed to match finalWidth exactly
    if (top.length < finalWidth){
      var diff = finalWidth - top.length;
      top = topWithCounts(topLeftEqBase + addTopLeft + Math.floor(diff/2),
                          topRightEqBase + addTopRight + Math.ceil(diff/2));
    }
    if (bottom.length < finalWidth){
      var diffB = finalWidth - bottom.length;
      bottom = bottomWithCounts(botLeftEqBase + addBotLeft + Math.floor(diffB/2),
                                botRightEqBase + addBotRight + Math.ceil(diffB/2));
    }

    // Middle line roughly centered under the final width
    var padLeft = Math.max(0, Math.floor((finalWidth - midLen) / 2));
    var middle = (padLeft ? ' '.repeat(padLeft) : '') + midCore;

    // Leading newline before the box (requested behavior)
    return '\n' + top + '\n' + middle + '\n' + bottom;
  }

  function buildMessage(opts){
    // opts: { nameOnly, id, includeId, includeTime, timeSec }
    if (CURRENT_TEMPLATE === 'ascii') {
      return buildAsciiMessage(opts); // ignores modifiers entirely
    }

    var includeId   = !!opts.includeId;
    var includeTime = !!opts.includeTime;

    var timeTxt = '';
    if (includeTime && opts.timeSec > 0) timeTxt = fmtDur(opts.timeSec);

    var tpl = TEMPLATES[CURRENT_TEMPLATE] || TEMPLATES["default"];
    var idPart = (includeId && opts.id) ? ' [' + opts.id + ']' : '';

    var timePart = '';
    if (timeTxt) {
      if (CURRENT_TEMPLATE === 'emoji') {
        timePart = '\n⏲️ ' + timeTxt;
      } else { // default
        timePart = '\n' + 'Hitting in ' + timeTxt;
      }
    }

    return tpl
      .replace('{name}', opts.nameOnly || '')
      .replace('{id}',   idPart)
      .replace('{time}', timePart);
  }

  // returns true if button was newly added
  function attachButtonInto(callCell, tr){
    if (!callCell) return false;
    if (callCell.querySelector('.yata-call-btn')) return false;

    var vars = getNameVariants(tr);
    if (!vars.full) return false;

    var btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'yata-call-btn';
    btn.textContent = 'Call';
    var baseHint = 'Click: copy (Shift=+time on new line, Alt=+ID, Ctrl=UPPER; ASCII ignores modifiers; Middle-click: CALL: <name>)';
    btn.title = baseHint;

    btn.addEventListener('click', function(ev){
      ev.stopPropagation();

      var tpl = CURRENT_TEMPLATE;

      var payload = buildMessage({
        nameOnly: vars.nameOnly,
        id: vars.id,
        includeId:   (tpl === 'ascii') ? true : !!ev.altKey,     // ASCII: always include ID if present
        includeTime: (tpl === 'ascii') ? true : !!ev.shiftKey,   // ASCII: always include time/Ready
        timeSec: hospLeft(tr)
      });

      if (ev.ctrlKey && tpl !== 'ascii') payload = payload.toUpperCase();

      copyText(payload).then(function(){ flash(btn,'ok'); }).catch(function(){
        flash(btn,'err'); window.prompt('Copy manually:', payload);
      });
    });

    btn.addEventListener('auxclick', function(ev){
      if (ev.button !== 1) return;
      ev.preventDefault();
      var quick = 'CALL: ' + vars.nameOnly; // quick middle-click format (no ID/time)
      copyText(quick).then(function(){ flash(btn,'ok'); }).catch(function(){ flash(btn,'err'); });
    });

    callCell.appendChild(btn);
    watchStatus(tr, btn, baseHint);
    return true;
  }

  function watchRow(tr){
    if (tr._yataRowWatch) return;
    var mo = new MutationObserver(function(){
      var res = ensureCallCell(tr);
      if (res.cell) attachButtonInto(res.cell, tr);

      var updated = ensureNotesSortKey(tr).updated;
      watchNotes(tr);
      if (res.created || updated) scheduleTablesorterUpdate();
    });
    mo.observe(tr, { childList:true, subtree:false });
    tr._yataRowWatch = mo;
  }

  function processRow(tr){
    insertCallHeader();
    var res = ensureCallCell(tr);
    if (res.cell) attachButtonInto(res.cell, tr);

    var updated = ensureNotesSortKey(tr).updated;
    watchNotes(tr);
    watchRow(tr);

    if (res.created || updated) scheduleTablesorterUpdate();
  }

  function scanExistingRows(){
    if (!TARGET_TABLE) {
      insertCallHeader();
      var headerRow = document.querySelector('thead tr.tablesorter-headerRow');
      if (headerRow) TARGET_TABLE = headerRow.closest('table');
    }
    var tbody = TARGET_TABLE ? TARGET_TABLE.tBodies[0] : document.querySelector('tbody');
    if (!tbody) return;

    var rows = tbody.querySelectorAll('tr[id^="target-list-refresh-"]');
    for (var i=0; i<rows.length; i++) processRow(rows[i]);

    if (!TBODY_OBSERVER){
      TBODY_OBSERVER = new MutationObserver(function(muts){
        for (var j=0; j<muts.length; j++){
          var m = muts[j];
          if (m.type === 'childList'){
            for (var k=0; k<m.addedNodes.length; k++){
              var node = m.addedNodes[k];
              if (node && node.nodeType === 1 && node.tagName === 'TR'){
                processRow(node);
              }
            }
          }
        }
      });
      TBODY_OBSERVER.observe(tbody, { childList:true, subtree:false });
    }
  }

  // ---------- Header template selector ----------
  function addTemplateControlInHeader(){
    var h2 = document.querySelector('h2.title .d-flex');
    if (!h2) return false;

    var refreshBlockElm = document.querySelector('#target-refresh');
    var refreshBlock = refreshBlockElm ? refreshBlockElm.closest('.px-2') : null;

    var host = document.createElement('div');
    host.className = 'px-2';

    var wrap = document.createElement('div');
    wrap.className = 'yata-template-wrap';
    var label = document.createElement('label');
    label.className = 'yata-template-label';
    label.setAttribute('for','yata-template-select');
    label.textContent = 'Call Style:';
    var sel = document.createElement('select');
    sel.id = 'yata-template-select';
    sel.className = 'yata-template-select';

    Object.keys(TEMPLATES).forEach(function(k){
      var opt = document.createElement('option');
      opt.value = k;
      var text = (k === 'emoji') ? 'emoji' : (k === 'ascii') ? 'ascii' : 'default';
      opt.textContent = text;
      sel.appendChild(opt);
    });
    sel.value = CURRENT_TEMPLATE;

    sel.addEventListener('change', function(e){
      CURRENT_TEMPLATE = e.target.value;
      localStorage.setItem('yata-template', CURRENT_TEMPLATE);
    });

    wrap.appendChild(label);
    wrap.appendChild(sel);
    host.appendChild(wrap);

    if (refreshBlock) {
      h2.insertBefore(host, refreshBlock);
    } else {
      h2.appendChild(host);
    }
    return true;
  }

  // ---------- Init ----------
  function init(){
    addTemplateControlInHeader();
    insertCallHeader();
    scanExistingRows();
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();