Analizador de Transferencias en curso (analizarTransferencias) para sub=cheatnow: motor de scoring ponderado (PESOS interno), coloreado por severidad, panel resumen (hubs, recíprocos, top de riesgo), filtro y botones [+]. Inyecta a Cruzar Usuarios vía GA.cargarEnCruce. Requiere ga-core.
Tätä skriptiä ei tulisi asentaa suoraan. Se on kirjasto muita skriptejä varten sisällytettäväksi metadirektiivillä // @require https://update.greasyfork.org/scripts/588167/1882654/GA%20Cheatnow%20%28ManagerZone%29.js.
// ==UserScript==
// @name GA Cheatnow (ManagerZone)
// @namespace https://control.managerzone.com/ga
// @version 1.0.0
// @description Analizador de Transferencias en curso (analizarTransferencias) para sub=cheatnow: motor de scoring ponderado (PESOS interno), coloreado por severidad, panel resumen (hubs, recíprocos, top de riesgo), filtro y botones [+]. Inyecta a Cruzar Usuarios vía GA.cargarEnCruce. Requiere ga-core.
// ==/UserScript==
(function () {
'use strict';
window.GA = window.GA || {};
const GA = window.GA;
// ============================================================
// Analizador de Transferencias en curso (sub=cheatnow)
//
// Puntúa cada transferencia combinando señales. NINGUNA señal
// suelta alcanza: en una muestra real, UB=1 aparece en el 52% de
// las filas y X=0 en el 42%. Lo que discrimina es la CONJUNCIÓN.
//
// Señales (pesos en PESOS, editables):
// · Reciprocidad : PS>0 Y PP>0 (se venden en ambas direcciones)
// · Historial : PS>0 o PP>0 (ya comerciaron)
// · Regalo : precio irrisorio frente al valor del jugador
// · Sobrepago : se pagó muy por encima del valor
// · Sin competencia: UB=1 (un único postor)
// · Hub comprador : el comprador recibe de muchos vendedores
// · Hub vendedor : el vendedor reparte a muchos compradores
// · Par repetido : el mismo par vendedor→comprador varias veces
//
// Agrega: coloreado por severidad, panel resumen (hubs, recíprocos,
// top de riesgo), filtro "solo riesgo" y botones [+] con ID real.
// ============================================================
function analizarTransferencias() {
// ---- Pesos (ajustá a tu criterio) ----
const PESOS = {
reciproco: 40, // PS>0 y PP>0
historial: 25, // PS>0 o PP>0
regalo: 25, // bid/value < RATIO_REGALO
precioBajo: 12, // bid/value < RATIO_BAJO
sobrepago: 20, // bid/value > RATIO_ALTO
sinCompetencia: 10, // UB == 1
hubComprador: 15,
hubVendedor: 10,
parRepetido: 20
};
const RATIO_REGALO = 0.01; // < 1% del valor
const RATIO_BAJO = 0.10; // < 10% del valor
const RATIO_ALTO = 3.0; // paga 3x el valor
const HUB_COMPRADOR = 3; // compra a >= N vendedores distintos
const HUB_VENDEDOR = 4; // vende a >= N compradores distintos
// Calibrados sobre una muestra real de 558 transferencias:
// >=60 -> 36 filas (6.5%) >=50 -> 88 filas (15.8%)
// Por debajo de UMBRAL_MEDIO la fila NO se pinta (solo lleva el
// badge con el score), para que el color siga significando algo.
const UMBRAL_ALTO = 60;
const UMBRAL_MEDIO = 50;
const COL_ALTO = '#f8caca';
const COL_MEDIO = '#ffe0b3';
const COL_BAJO = '#fff3b0';
// ---- Ubicar la tabla (por sus cabeceras características) ----
const tabla = [...document.querySelectorAll('table')].find(t => {
const h = [...t.querySelectorAll('th')].map(x => x.textContent.trim());
return h.includes('Seller') && h.includes('Buyer') && h.includes('PS') && h.includes('PP');
});
if (!tabla) return;
const ths = [...tabla.querySelectorAll('thead th')];
const idx = n => ths.map(t => t.textContent.trim()).indexOf(n);
const C = {
sc: idx('SC'), bc: idx('BC'), seller: idx('Seller'), buyer: idx('Buyer'),
bid: idx('Current bid'), value: idx('Value'), x: idx('X'),
tb: idx('TB'), ub: idx('UB'), ps: idx('PS'), pp: idx('PP'), end: idx('End date')
};
if (C.seller < 0 || C.buyer < 0) return;
const filas = [...tabla.querySelectorAll('tr')].filter(tr => !tr.querySelector('th'));
if (!filas.length) return;
const txt = c => (c?.textContent || '').replace(/\s+/g, ' ').trim();
const num = c => { const n = parseFloat(txt(c).replace(/\s/g, '').replace(',', '.')); return isNaN(n) ? 0 : n; };
// El username viene en un <a>, y el user id en el href (searchstr=NNN)
const userDe = c => {
const a = c?.querySelector('a');
if (!a) return { name: txt(c).split(' ')[0] || '', id: '' };
const m = (a.getAttribute('href') || '').match(/searchstr=(\d+)/);
return { name: a.textContent.trim(), id: m ? m[1] : '' };
};
// ---- 1er pase: leer filas ----
const datos = filas.map(tr => {
const c = tr.children;
const s = userDe(c[C.seller]), b = userDe(c[C.buyer]);
const bid = num(c[C.bid]), value = num(c[C.value]);
return {
tr, celdas: c,
seller: s.name, sellerId: s.id, buyer: b.name, buyerId: b.id,
bid, value, ratio: value > 0 ? bid / value : 0,
ub: C.ub >= 0 ? num(c[C.ub]) : 0,
ps: C.ps >= 0 ? num(c[C.ps]) : 0,
pp: C.pp >= 0 ? num(c[C.pp]) : 0
};
}).filter(d => d.seller && d.buyer);
// ---- Redes: hubs y pares repetidos ----
const compraDe = {}, vendeA = {}, pares = {};
datos.forEach(d => {
(compraDe[d.buyer] = compraDe[d.buyer] || new Set()).add(d.seller);
(vendeA[d.seller] = vendeA[d.seller] || new Set()).add(d.buyer);
const k = d.seller + '>' + d.buyer;
pares[k] = (pares[k] || 0) + 1;
});
// ---- 2do pase: puntuar ----
datos.forEach(d => {
let score = 0;
const motivos = [];
if (d.ps > 0 && d.pp > 0) {
score += PESOS.reciproco;
motivos.push(`recíproco (PS:${d.ps} PP:${d.pp})`);
} else if (d.ps > 0 || d.pp > 0) {
score += PESOS.historial;
motivos.push(d.ps > 0 ? `${d.ps} venta(s) previa(s)` : `${d.pp} compra(s) previa(s)`);
}
if (d.value > 0) {
if (d.ratio < RATIO_REGALO) {
score += PESOS.regalo;
motivos.push(`regalo (${d.bid.toLocaleString()} por un jugador de ${d.value.toLocaleString()})`);
} else if (d.ratio < RATIO_BAJO) {
score += PESOS.precioBajo;
motivos.push(`precio bajo (${Math.round(d.ratio * 100)}% del valor)`);
} else if (d.ratio > RATIO_ALTO) {
score += PESOS.sobrepago;
motivos.push(`sobrepago (${d.ratio.toFixed(1)}× el valor)`);
}
}
if (d.ub === 1) { score += PESOS.sinCompetencia; motivos.push('sin competencia (UB=1)'); }
const nComprAVend = (compraDe[d.buyer] || new Set()).size;
if (nComprAVend >= HUB_COMPRADOR) {
score += PESOS.hubComprador;
motivos.push(`hub: ${d.buyer} compra a ${nComprAVend} vendedores`);
}
const nVendACompr = (vendeA[d.seller] || new Set()).size;
if (nVendACompr >= HUB_VENDEDOR) {
score += PESOS.hubVendedor;
motivos.push(`hub: ${d.seller} vende a ${nVendACompr} compradores`);
}
const rep = pares[d.seller + '>' + d.buyer];
if (rep > 1) { score += PESOS.parRepetido; motivos.push(`par repetido ×${rep}`); }
d.score = score;
d.motivos = motivos;
d.nivel = score >= UMBRAL_ALTO ? 'alto' : score >= UMBRAL_MEDIO ? 'medio' : 'ok';
});
// ---- 3er pase: pintar ----
const tint = (celdas, color) => { for (const td of celdas) td.style.background = color; };
datos.forEach(d => {
if (d.nivel === 'alto') tint(d.celdas, COL_ALTO);
else if (d.nivel === 'medio') tint(d.celdas, COL_MEDIO);
// por debajo del umbral medio no se pinta la fila
if (d.score > 0) {
const b = document.createElement('span');
b.textContent = d.score;
b.title = d.motivos.join(' · ');
b.style.cssText = 'display:inline-block; margin-left:6px; padding:0 5px; border-radius:3px; font-size:10px; font-weight:bold; color:#fff; background:' +
(d.nivel === 'alto' ? '#c0392b' : d.nivel === 'medio' ? '#e67e22' : '#9aa0a6') + ';';
d.celdas[C.buyer].appendChild(b);
}
// [+] con el ID real, para Cruzar Usuarios
[[C.seller, d.sellerId, d.seller], [C.buyer, d.buyerId, d.buyer]].forEach(([col, id, nom]) => {
if (!nom) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = '[+]';
btn.className = 'ga-plus-tr';
btn.title = 'Añadir a Cruzar Usuarios';
btn.style.cssText = 'margin-left:4px; cursor:pointer; background:#007bff; color:#fff; border:none; border-radius:3px; font-size:9px; font-weight:bold; padding:0 3px;';
btn.addEventListener('click', e => {
e.preventDefault();
const input = document.getElementById('ga-simple-cross-input');
if (!input) return;
GA.cargarEnCruce(id || nom);
btn.textContent = '[✓]';
setTimeout(() => (btn.textContent = '[+]'), 800);
});
d.celdas[col].appendChild(btn);
});
});
// ---- Panel resumen ----
const altos = datos.filter(d => d.nivel === 'alto').sort((a, b) => b.score - a.score);
const medios = datos.filter(d => d.nivel === 'medio');
const recip = datos.filter(d => d.ps > 0 && d.pp > 0);
const hubsC = Object.entries(compraDe).filter(([, s]) => s.size >= HUB_COMPRADOR)
.sort((a, b) => b[1].size - a[1].size);
const hubsV = Object.entries(vendeA).filter(([, s]) => s.size >= HUB_VENDEDOR)
.sort((a, b) => b[1].size - a[1].size);
const panel = document.createElement('div');
panel.style.cssText = 'margin:12px 0; padding:12px 15px; border:1px solid #c9a0a0; background:#fff8f8; border-radius:5px; font-family:sans-serif; font-size:13px; color:#333;';
const listaHubs = (arr, verbo) => arr.slice(0, 8).map(([n, s]) =>
`<li><b>${n}</b> ${verbo} <b>${s.size}</b>: <span style="color:#666; font-size:11px;">${[...s].join(', ')}</span></li>`).join('');
panel.innerHTML = `
<div style="font-size:14px; margin-bottom:8px;">
🚨 <b>Transferencias analizadas:</b> ${datos.length} ·
<span style="background:${COL_ALTO}; padding:1px 6px; border-radius:3px;">Alto: ${altos.length}</span>
<span style="background:${COL_MEDIO}; padding:1px 6px; border-radius:3px; margin-left:4px;">Medio: ${medios.length}</span>
<button id="ga-tr-filtro" type="button" style="margin-left:12px; padding:3px 10px; cursor:pointer; background:#c0392b; color:#fff; border:none; border-radius:3px; font-weight:bold;">Ver solo riesgo</button>
<button id="ga-tr-addtop" type="button" style="margin-left:6px; padding:3px 10px; cursor:pointer; background:#28a745; color:#fff; border:none; border-radius:3px; font-weight:bold;">➕ Cruzar los de riesgo alto</button>
</div>
<div style="display:flex; gap:20px; flex-wrap:wrap;">
<div style="flex:1; min-width:260px;">
<b>🔴 Top riesgo</b>
<ol style="margin:4px 0 0 18px; padding:0; font-size:12px;">
${altos.slice(0, 10).map(d => `<li><b>${d.seller}</b> → <b>${d.buyer}</b> <span style="color:#c0392b; font-weight:bold;">(${d.score})</span><br><span style="color:#666; font-size:11px;">${d.motivos.join(' · ')}</span></li>`).join('') || '<li style="color:#666;">ninguna</li>'}
</ol>
</div>
<div style="flex:1; min-width:260px;">
<b>🕸 Hubs compradores</b> <span style="color:#777; font-size:11px;">(reciben de varios)</span>
<ul style="margin:4px 0 8px 18px; padding:0; font-size:12px;">${listaHubs(hubsC, 'compra a') || '<li style="color:#666;">ninguno</li>'}</ul>
<b>🕸 Hubs vendedores</b> <span style="color:#777; font-size:11px;">(reparten a varios)</span>
<ul style="margin:4px 0 8px 18px; padding:0; font-size:12px;">${listaHubs(hubsV, 'vende a') || '<li style="color:#666;">ninguno</li>'}</ul>
<b>🔄 Recíprocos</b> <span style="color:#777; font-size:11px;">(se venden en ambas direcciones)</span>
<ul style="margin:4px 0 0 18px; padding:0; font-size:12px;">
${recip.slice(0, 6).map(d => `<li>${d.seller} ↔ ${d.buyer} <span style="color:#666;">(PS:${d.ps} PP:${d.pp})</span></li>`).join('') || '<li style="color:#666;">ninguno</li>'}
</ul>
</div>
</div>
`;
tabla.parentNode.insertBefore(panel, tabla);
// Filtro: mostrar solo alto/medio
let filtrado = false;
document.getElementById('ga-tr-filtro').addEventListener('click', function () {
filtrado = !filtrado;
datos.forEach(d => {
const ocultar = filtrado && d.nivel !== 'alto' && d.nivel !== 'medio';
d.tr.style.display = ocultar ? 'none' : '';
});
this.textContent = filtrado ? 'Ver todas' : 'Ver solo riesgo';
});
// Cargar los de riesgo alto en Cruzar Usuarios
document.getElementById('ga-tr-addtop').addEventListener('click', () => {
const input = document.getElementById('ga-simple-cross-input');
if (!input) { alert('El widget Cruzar Usuarios no está disponible en esta página.'); return; }
const vals = [];
altos.forEach(d => {
[d.sellerId || d.seller, d.buyerId || d.buyer].forEach(v => { if (v) vals.push(v); });
});
const n = GA.cargarEnCruce(vals);
alert(`Se agregaron ${n} usuario(s) de las transferencias de riesgo alto.`);
});
}
GA.analizarTransferencias = analizarTransferencias;
})();