ManagerZone - Treino % na Tabela

Exibe, em cada atributo do jogador, a porcentagem estimada até o próximo ganho de ponto (bolinha). A informação é mostrada em uma coluna extra, no final da linha da tabela de habilidades, com destaque por cores de acordo com a faixa de progresso. Script baseado em: van.mz.playerAdvanced

// ==UserScript==
// @name         ManagerZone - Treino % na Tabela
// @namespace    https://greasyfork.org/users/1520478-emanuel-neves   // troque pelo seu ID no GreasyFork
// @version      0.0.6
// @description  Exibe, em cada atributo do jogador, a porcentagem estimada até o próximo ganho de ponto (bolinha). A informação é mostrada em uma coluna extra, no final da linha da tabela de habilidades, com destaque por cores de acordo com a faixa de progresso. Script baseado em: van.mz.playerAdvanced
// @author       Emanuel Neves (emanuelsn)
// @match        https://www.managerzone.com/*
// @icon         https://www.managerzone.com/favicon.ico
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// @connect      managerzone.com
// @license      MIT
// ==/UserScript==

(function(){
  'use strict';
  if (window.__MZ_TRAIN_PUBLISHED__) return;
  window.__MZ_TRAIN_PUBLISHED__ = true;

  /** =========================================================================
   *  CONFIGURAÇÕES BÁSICAS
   *  ========================================================================= */
  const SPORT   = 'soccer';
  const TIMEOUT = 12000;

  // Pesos por nível de treino (mantidos conforme metodologia original)
  const WEIGHTS = {
    1: 0.645*1, 2: 0.55*2, 3: 0.7*3, 4: 0.85*4, 5: 0.96*5,
    6: 1.111*6, 7: 1.3*7, 8: 1.6*8, 9: 2.02*9, 10: 2.4*10
  };

  // Mapeamento de atributos → skill_id usados nos gráficos do ManagerZone
  const SKILL_NAME_TO_ID = {
    'velocidade': 2,
    'resistência': 3,
    'inteligência': 4,
    'passe curto': 5,
    'chute': 6,
    'cabeceio': 7,
    'defesa a gol': 8,
    'controle de bola': 9,
    'desarme': 10,
    'passe longo': 11,
    'bola parada': 12
  };

  /** =========================================================================
   *  FUNÇÕES AUXILIARES
   *  ========================================================================= */
  const fmtDate = ts => {
    try { return new Date(ts).toLocaleDateString('pt-BR'); }
    catch(_) { return String(ts); }
  };

  function sumEff(pos, neg){
    let sPos = 0, sNeg = 0;
    for (let i=1;i<=10;i++){
      const w = WEIGHTS[i]||0;
      sPos += w * (pos[i]||0);
      sNeg += w * (neg[i]||0);
    }
    let s = sPos - sNeg;
    if (s < 0) s = 0;
    if (s >= 100) s = 99.99;
    return +s.toFixed(2);
  }

  function isBall(url){
    return /trainingicon\.php\?icon=bar_pos_\d+_ball/i.test(String(url||''));
  }

  function parseIcon(url){
    const m = String(url||'').match(/trainingicon\.php\?icon=bar_(pos|neg)_(\d+)(?:_ball)?(?:&t=([a-z_]+))?/i);
    if (!m) return null;
    return { sign: m[1].toLowerCase(), t: Math.max(1, Math.min(10, +m[2])) };
  }

  /** =========================================================================
   *  REQUISIÇÕES AOS ENDPOINTS DE TREINO
   *  ========================================================================= */
  async function http(url){
    try{
      const r = await fetch(url, {credentials:'include'});
      return { ok:true, data: await r.text() };
    }catch(_){}
    return new Promise((resolve,reject)=>{
      GM_xmlhttpRequest({
        method:'GET', url, timeout: TIMEOUT,
        onload: r => resolve({ ok:true, data:r.responseText }),
        onerror: reject, ontimeout: () => reject(new Error('timeout'))
      });
    });
  }

  async function fetchTrainingData(pid, skillId){
    const tries = [
      `/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=${SPORT}&player_id=${pid}&skill_id=${skillId}`,
      `/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=${SPORT}&player_id=${pid}`,
      `/ajax.php?p=players&sub=training_graphs&pid=${pid}&sport=${SPORT}&skill=${skillId}`,
      `/ajax.php?p=players&sub=training_graphs&pid=${pid}&skill=${skillId}`
    ];
    for (const url of tries){
      try{
        const res = await http(url);
        if (res.ok && res.data && res.data.length > 20) return { url, data: res.data };
      }catch(_){}
    }
    return { url:null, data:null };
  }

  /** =========================================================================
   *  PARSE E EXTRAÇÃO DE PONTOS DO HISTÓRICO
   *  ========================================================================= */
  function extractSeries(data){
    let series;
    try{ eval(data); if (Array.isArray(series)) return series; }catch(_){}
    try{ const j = JSON.parse(data); if (j && Array.isArray(j.series)) return j.series; }catch(_){}
    try{
      const m = data.match(/series\s*=\s*(\[[\s\S]*?\]);/);
      if (m){ const tmp = eval(m[1]); if (Array.isArray(tmp)) return tmp; }
    }catch(_){}
    return [];
  }

  function collectPointsFromSeries(seriesArr){
    const pts = [];
    for (const s of seriesArr){
      const data = (s && (s.data || s.points)) || [];
      for (const g of data){
        const x = g && g.x;
        const marker = g && g.marker && (g.marker.symbol || g.marker.url);
        if (!Number.isFinite(x) || !marker) continue;
        const info = parseIcon(marker);
        if (!info) continue;
        pts.push({x, marker});
      }
    }
    return pts;
  }

  /** =========================================================================
   *  CÁLCULO DO % DE TREINO
   *  ========================================================================= */
  async function getPercentForSkill(pid, skillId){
    const net = await fetchTrainingData(pid, skillId);
    if (!net.data) return 0.00;

    const allSeries = extractSeries(net.data);
    const points = collectPointsFromSeries(allSeries);
    if (!points.length) return 0.00;

    let cutoff = -Infinity;
    for (const p of points){ if (isBall(p.marker) && p.x > cutoff) cutoff = p.x; }
    if (!Number.isFinite(cutoff)) cutoff = Math.min(...points.map(p=>p.x));

    const pos = {}, neg = {};
    for (const p of points){
      if (p.x <= cutoff) continue;
      const info = parseIcon(p.marker); if (!info) continue;
      if (info.sign === 'pos') pos[info.t] = (pos[info.t]||0) + 1;
      else                     neg[info.t] = (neg[info.t]||0) + 1;
    }
    return sumEff(pos, neg);
  }

  /** =========================================================================
   *  UI: INSERÇÃO DA COLUNA E ESTILOS
   *  ========================================================================= */
  function injectCSS(){
    if (document.getElementById('mz-train-css')) return;
    const st = document.createElement('style');
    st.id = 'mz-train-css';
    st.textContent = `
      .mz-train-td{ font-size:11px; white-space:nowrap; padding-left:6px; text-align:right; min-width:56px; }
      .mz-train-td--placeholder{ color:transparent; }
      .mz-badge{ display:inline-block; padding:0 6px; border-radius:10px; font-weight:600; line-height:1.6; }
      .mz-badge--g0 { background: rgba(120,120,120,.15); color:#444; }
      .mz-badge--g20{ background: rgba(255,193,7,.18); color:#8a6d00; }
      .mz-badge--g50{ background: rgba(33,150,243,.18); color:#0f4c81; }
      .mz-badge--g80{ background: rgba(76,175,80,.18); color:#1b5e20; }
    `;
    document.head.appendChild(st);
  }

  function getSkillRows(){
    const table = document.querySelector('.player_skills.player_skills_responsive');
    if (!table) return [];
    const rows = [];
    table.querySelectorAll('tr').forEach(tr=>{
      const td = tr.querySelector('td');
      if (!td) return;
      const nameText = (td.textContent||'').trim().toLowerCase();
      for (const key in SKILL_NAME_TO_ID){
        if (nameText.includes(key)){
          rows.push({tr, key, skillId: SKILL_NAME_TO_ID[key]});
          break;
        }
      }
    });
    return rows;
  }

  function clsForPercent(p){
    if (p >= 80) return 'mz-badge--g80';
    if (p >= 50) return 'mz-badge--g50';
    if (p >= 20) return 'mz-badge--g20';
    return 'mz-badge--g0';
  }

  async function runOnce(){
    const url = new URL(location.href);
    const pid = url.searchParams.get('pid');
    if (!pid) return;

    injectCSS();

    const table = document.querySelector('.player_skills.player_skills_responsive');
    if (!table) return;

    // Garante coluna final em todas as linhas
    table.querySelectorAll('tr').forEach(tr=>{
      if (tr.dataset.mzColAdded) return;
      tr.dataset.mzColAdded = '1';
      const tdPercent = document.createElement('td');
      tdPercent.className = 'mz-train-td mz-train-td--placeholder';
      tdPercent.innerHTML = '&nbsp;';
      tr.appendChild(tdPercent);
    });

    // Preenche apenas para as skills reconhecidas
    const rows = getSkillRows();
    for (const {tr, skillId} of rows){
      const tdPercent = tr.querySelector('.mz-train-td');
      if (!tdPercent) continue;
      tdPercent.classList.remove('mz-train-td--placeholder');
      tdPercent.textContent = '';
      const badge = document.createElement('span');
      badge.className = 'mz-badge mz-badge--g0';
      badge.textContent = '(…)';
      tdPercent.appendChild(badge);

      try{
        const pct = await getPercentForSkill(pid, skillId);
        badge.className = `mz-badge ${clsForPercent(pct)}`;
        badge.textContent = `(${pct.toFixed(1)}%)`;
      }catch(_){
        badge.className = 'mz-badge mz-badge--g0';
        badge.textContent = '(0.0%)';
      }
    }
  }

  /** =========================================================================
   *  OBSERVAÇÃO DE MUDANÇAS NA PÁGINA
   *  ========================================================================= */
  function isPlayerPage(){
    return /[?&]p=players\b/.test(location.href) || /players\.php\b/.test(location.href);
  }

  let lastPid=null, t=null;
  const obs = new MutationObserver(()=>{
    if (!isPlayerPage()) return;
    const pid = new URL(location.href).searchParams.get('pid');
    if (pid && pid!==lastPid){
      lastPid = pid;
      clearTimeout(t);
      t = setTimeout(runOnce, 600);
    }
  });
  obs.observe(document.documentElement, {subtree:true, childList:true});
  if (isPlayerPage()) runOnce();

})();