Rating-ul se calculeaza din datele raw ale meciului, comparat cu toti participantii prin rank-normalizare. Formula e role-agnostic — tank, support, ADC sau fighter sunt evaluate echitabil. Nu exista bonus/penalizare pentru victorie/infrangere — scopul e masurarea contributiei individuale pentru echilibrarea echipelor.
Composite = 0.70×Impact + 0.15×Supravietuire + 0.15×Eficienta
Se aplica o functie de stretch (exponent 0.65) pentru a pastra diferente clare fara a impinge excesiv notele spre 1 sau 10:
S = stretch(Composite); Rating = 1.0 + 9.0 × (3S / (1 + 2S)) + Bonusuri
Aceasta calibrare foloseste aceeasi scala pentru toate formatele: scorul neutru 0.5 devine nota 7.75. Curba pastreaza ordinea performantelor; notele slabe pot ramane sub 5, iar capetele scalei raman 1 si 10.
KP = (K+A) / killurile echipei. Media KP a meciului primeste scorul 0.5; diferentele reale fata de medie se pastreaza, fara salturi de clasament. Un procent apropiat de al altui jucator produce un punctaj apropiat, indiferent de ordinea lor.
KDA, support si eficienta se compara cu toti participantii meciului prin ranking, de la cel mai bun la cel mai slab. Valorile egale primesc acelasi punctaj. Damage, tanking si kill/assist share sunt ajustate la marimea propriei echipe, cu 5vs5 ca referinta. Damping-ul trage capetele clasamentului catre 0.5 in meciurile cu putini jucatori, pentru a evita extremele. Asta inseamna ca un meci de 3v3 nu va produce rating-uri la fel de polarizate ca un 5v5.
Bonusurile si penalizarile sunt dezactivate temporar. Regulile sunt pastrate mai jos pentru reactivare ulterioara.
Tabul Last 10 Games foloseste ultimele 10 meciuri. Tabul Overall foloseste ultimele maximum 100 de meciuri per player, cu minimum 10 meciuri pentru eligibilitate.
// ── Rank-normalizare ──
function rankNormalize(values) {
const n = values.length;
if (n <= 1) return values.map(() => 0.5);
const indexed = values.map((v, i) => ({ v, i })).sort((a, b) => b.v - a.v);
const scores = new Array(n);
const damping = Math.min(1, 1 - 2 / (n + 1));
// Tied values share their average rank, independent of participant order.
for (let start = 0; start < n;) {
let end = start + 1;
while (end < n && indexed[end].v === indexed[start].v) end++;
const raw = 1 - ((start + end - 1) / 2) / (n - 1);
const score = 0.5 + (raw - 0.5) * damping;
for (let rank = start; rank < end; rank++) scores[indexed[rank].i] = score;
start = end;
}
return scores;
}
// ── Stretch + Rating ──
function stretchFromCenter(value, exp) {
if (value < 0.5) return 0.5 - Math.pow((0.5 - value) / 0.5, exp) * 0.5;
return 0.5 + Math.pow((value - 0.5) / 0.5, exp) * 0.5;
}
function compositeToRating(composite, bonusAdj) {
const stretched = stretchFromCenter(Math.max(0, Math.min(1, composite)), 0.65);
const calibrated = (3 * stretched) / (1 + 2 * stretched);
return Math.max(1.0, Math.min(10.0, Math.round((1.0 + 9.0 * calibrated + bonusAdj) * 100) / 100));
}
// ── Raw metrics per participant ──
const raw = participants.map(p => ({
team_size: teamSize, // numarul de jucatori din echipa lui p
kp: Math.min(1, (p.kills + p.assists) / teamKills),
damage_share: p.damage / teamDamage,
tank_share: (p.damage_taken + p.self_mitigated) / teamTakenMit,
support_metric: p.healing + p.cc_score * 100,
ka_share: (p.kills + p.assists) / teamKA,
kda: (p.kills + p.assists) / (p.deaths + 1),
damage_per_gold: p.damage / p.gold,
cc_per_gold: p.cc_score / p.gold,
damage: p.damage,
gold: p.gold,
deaths: p.deaths,
damage_taken: p.damage_taken,
self_mitigated: p.self_mitigated,
}));
// ── Rank-normalize all metrics ──
// KP pastreaza distantele reale fata de media meciului, fara ranking.
const kpBaseline = raw.reduce((sum, r) => sum + r.kp, 0) / raw.length;
const kpScores = raw.map(r => Math.max(0, Math.min(1, 0.5 + r.kp - kpBaseline)));
const rSupport = rankNormalize(raw.map(r => r.support_metric));
const rKda = rankNormalize(raw.map(r => r.kda));
const rDmgPerGold = rankNormalize(raw.map(r => r.damage_per_gold));
const rCcPerGold = rankNormalize(raw.map(r => r.cc_per_gold));
const rGold = rankNormalize(raw.map(r => r.gold));
const rHealing = rankNormalize(raw.map(r => r.support_metric));
// ═══ 1. IMPACT (70%) ═══
const rolePerf = Math.min(1.2, Math.max(
r.damage_share * r.team_size / 2, // Damage dealer
r.tank_share * r.team_size / 2, // Tank
rSupport[i] // Support (rank-normalized)
));
const kaScore = r.ka_share * r.team_size / 5;
const impact = 0.40 * kpScores[i] + 0.35 * rolePerf + 0.25 * kaScore;
// ═══ 2. SURVIVAL (15%) ═══
const survival = rKda[i];
// ═══ 3. EFFICIENCY (15%) ═══
const efficiency = Math.max(rDmgPerGold[i], rCcPerGold[i]);
// ═══ Composite ═══
let composite = 0.70 * impact + 0.15 * survival + 0.15 * efficiency;
// ═══ Bonusuri & Penalizari ═══
const highestDmg = Math.max(...participants.map(p => p.damage));
const lowerDamage = participants.map(p => p.damage).filter(d => d < highestDmg);
const secondHighestDmg = lowerDamage.length ? Math.max(...lowerDamage) : null;
const rank1Damage = p.damage === highestDmg;
const fewestDeaths = p.deaths === Math.min(...participants.map(p => p.deaths));
const mostTakenMitigated = p.damage_taken + p.self_mitigated === Math.max(
...participants.map(p => p.damage_taken + p.self_mitigated)
);
const topGold = rGold[i] === Math.max(...rGold);
const bottomDamage = p.damage === Math.min(...participants.map(p => p.damage));
const mostDeaths = p.deaths === Math.max(...participants.map(p => p.deaths));
const isHealer = rHealing[i] >= 0.75;
const isFrontline = r.tank_share >= 0.30;
const isProtected = isHealer || isFrontline;
// ── Bonusuri dezactivate temporar ──
// if (rank1Damage && r.kp >= 0.65) bonus += 0.60; // The Carry
// if (rank1Damage && secondHighestDmg !== null
// && highestDmg > secondHighestDmg * 1.4) bonus += 0.30; // Hard Carry / 1v9
// if (fewestDeaths && r.kp > 0.50) bonus += 0.40; // Smart Player
// if (mostTakenMitigated && r.kp > 0.40) bonus += 0.40; // The Frontline
// ── Penalizari dezactivate temporar ──
// if (r.kp < 0.45 && bottomDamage && !isProtected) bonus -= 1.50; // Passenger
// if (topGold && r.damage_share < 0.35 && !isProtected) bonus -= 0.70; // Gold Sink
// if (mostDeaths && r.kp < 0.80 && r.damage_share < 0.20 && !isProtected) bonus -= 0.50; // Dead Weight
// ═══ Final Rating (1-10) ═══
rating = compositeToRating(composite, bonus);