YATA Target Call Copier (v1.6.26)

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

// ==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();
  }
})();