TI/XP Transfer List Modified

Transfer List: Show Routine (always) and Training Intensity (from Tuesday to Saturday)

// ==UserScript==
// @name         TI/XP Transfer List Modified
// @namespace    http://tampermonkey.net/
// @version      0.3.2
// @description  Transfer List: Show Routine (always) and Training Intensity (from Tuesday to Saturday)
// @match        https://trophymanager.com/transfer/
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const CONST = {
    WRAP: 'div#transfer_list',

    TI_COL: 6,           
    DIFF_COL: 9,         
    XP_COL: 12,
    CURRENT_PRICE_COL_BASE: 7,  

    TI_HEADER: 'TI',
    XP_HEADER: 'XP',
    DIFF_HEADER: 'Price diff',

    TI_PREC: 0,
    XP_PREC: 1,
    DIFF_PREC: 0,
    CONCURRENCY: 8
  };

  const GK = 'GK';

  let initialized = false;
  let columnsAdded = false;

  const currentPriceById = Object.create(null);

  const num = (t) => Number(String(t || '').replace(/[^\d.-]/g, ''));
  const formatCoin = (n, prec = 0) => {
    if (!isFinite(n)) return '-';
    const s = Number(n).toFixed(prec);
    const [i, d] = s.split('.');
    const withSep = i.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    return `<span class='coin'>${d ? `${withSep}.${d}` : withSep}</span>`;
  };

  const getRows = () =>
    Array.from(document.querySelectorAll(`${CONST.WRAP} tr[id^=player_row]`));

  const mapRows = () =>
    getRows().map(row => ({ id: row.id.split('_')[2], row }));

  const getOldASI = (row) => {
    const cells = row.querySelectorAll('td,th');
    const cell = cells[5];
    if (!cell) return NaN;
    const m = cell.innerHTML.match(/[0-9,\.]+/);
    return m ? Number(m[0].replace(/[^\d.-]/g, '')) : NaN;
  };

  const addColumn = (colIndex, headerName) => {
    const headerRow = document.querySelector(`${CONST.WRAP} tr.header`);
    if (!headerRow) return;
    const ths = headerRow.querySelectorAll('th');
    const th = document.createElement('th');
    th.style.width = '80px';
    th.textContent = headerName;

    if (ths.length > colIndex + 1) headerRow.insertBefore(th, ths[colIndex]);
    else headerRow.appendChild(th);

    const rows = getRows();
    rows.forEach(row => {
      if (!row.children.length) return;
      const td = document.createElement('td');
      td.className = 'align_center';
      td.textContent = '-';
      const tds = row.querySelectorAll('td');
      if (tds.length > colIndex && tds[colIndex]) row.insertBefore(td, tds[colIndex]);
      else row.appendChild(td);
    });
  };

  const writeCell = (rowIndex, colIndex, html) => {
    const rows = getRows();
    const row = rows[rowIndex];
    if (!row) return;
    const cells = row.querySelectorAll('td,th');
    if (cells[colIndex]) cells[colIndex].innerHTML = html;
  };

  const TI = {
    compute(asiNew, asiOld, fp) {
      const P = Math.pow;
      if (fp === GK) {
        return (
          (P(asiNew * P(2, 9) * P(5, 4) * P(7, 7), 1 / 7) -
            P(asiOld * P(2, 9) * P(5, 4) * P(7, 7), 1 / 7)) /
            14 * 11 * 10
        );
      }
      return (
        (P(asiNew * P(2, 9) * P(5, 4) * P(7, 7), 1 / 7) -
          P(asiOld * P(2, 9) * P(5, 4) * P(7, 7), 1 / 7)) * 10
      );
    }
  };

  const priceDiff = (asiNew, ageYears, ageMonths, currentPrice, fp) => {
    const age = Number(ageYears) + Number(ageMonths) / 12;
    if (!asiNew || !age || age <= 0 || !isFinite(currentPrice)) return NaN;
    const est = asiNew * 500 * Math.pow(25 / age, 2.5);
    if (fp === GK) return est * 3 / 4 - currentPrice;
    return est - currentPrice;
  };

  const fetchPlayer = (id) =>
    new Promise((resolve, reject) => {
      $.ajax({
        url: '/ajax/tooltip.ajax.php',
        method: 'POST',
        dataType: 'json',
        data: { player_id: id, minigame: undefined }
      })
        .done((data) => {
          const p = data.player || {};
          resolve({
            id: p.player_id,
            fp: p.fp,
            asi: Number(String(p.skill_index || '0').replace(/,/g, '')),
            xp: Number(String(p.routine || '0').replace(',', '.')),
            age: Number(p.age || 0),
            months: Number(p.months || 0)
          });
        })
        .fail(reject);
    });

  const runPool = async (items, worker, concurrency) => {
    const out = Array(items.length);
    let i = 0, active = 0;
    return new Promise((resolve) => {
      const next = () => {
        while (active < concurrency && i < items.length) {
          const cur = i++;
          active++;
          worker(items[cur], cur)
            .then((res) => (out[cur] = res))
            .catch(() => (out[cur] = null))
            .finally(() => {
              active--;
              if (i >= items.length && active === 0) resolve(out);
              else next();
            });
        }
      };
      next();
    });
  };

  const readCurrentPricesBeforeInsert = () => {
    const rows = mapRows();
    rows.forEach(({ id, row }) => {
      const tds = row.querySelectorAll('td'); // cột gốc chưa bị chèn
      const cell = tds[CONST.CURRENT_PRICE_COL_BASE];
      const val = cell ? num(cell.textContent) : NaN;
      currentPriceById[id] = isFinite(val) ? val : NaN;
    });
  };

  const init = async () => {
    if (initialized) return;
    const table = document.querySelector(`${CONST.WRAP} table`);
    if (!table) return;

    initialized = true;

    readCurrentPricesBeforeInsert();

    if (!columnsAdded) {
      addColumn(CONST.TI_COL,   CONST.TI_HEADER);
      addColumn(CONST.DIFF_COL, CONST.DIFF_HEADER);
      addColumn(CONST.XP_COL,   CONST.XP_HEADER);
      columnsAdded = true;
    }

    const items = mapRows();
    if (!items.length) return;

    await runPool(
      items,
      async ({ id, row }, idx) => {
        try {
          const data = await fetchPlayer(id);
          const oldAsi   = getOldASI(row);
          const tiVal    = TI.compute(data.asi, oldAsi, data.fp);
          const curPrice = currentPriceById[id]; // ✅ dùng giá đã đọc trước
          const diffVal  = priceDiff(data.asi, data.age, data.months, curPrice, data.fp);

          requestAnimationFrame(() => {
            writeCell(idx, CONST.TI_COL,   isFinite(tiVal) ? tiVal.toFixed(CONST.TI_PREC) : '-');
            writeCell(idx, CONST.DIFF_COL, isFinite(diffVal) ? formatCoin(diffVal, CONST.DIFF_PREC) : '-');
            writeCell(idx, CONST.XP_COL,   isFinite(data.xp) ? data.xp.toFixed(CONST.XP_PREC) : '-');
          });
        } catch {
          requestAnimationFrame(() => {
            writeCell(idx, CONST.TI_COL,   'Error');
            writeCell(idx, CONST.DIFF_COL, 'Error');
            writeCell(idx, CONST.XP_COL,   'Error');
          });
        }
      },
      CONST.CONCURRENCY
    );
  };

  const host = document.querySelector(CONST.WRAP);
  if (!host) return;

  const observer = new MutationObserver(() => init());
  observer.observe(host, { childList: true });

  init();
})();