Onetricks.gg Score Calculation

Automatically scores OTPs based on win rate, games, LP, and play rate

当前为 2025-10-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         Onetricks.gg Score Calculation
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Automatically scores OTPs based on win rate, games, LP, and play rate
// @author       Joshinon
// @match        https://www.onetricks.gg/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

// NOTE: THIS IS AN UNOFFICIAL COMMUNITY EXTENSION. IT IS NOT CREATED, ENDORSED, OR SUPPORTED BY ONETRICKS.GG. PLEASE USE RESPONSIBLY.

(function() {
    'use strict';

    const POLL_INTERVAL = 300;
    let headerAdded = false;
    let table, tbody, headerRow;
    let sortDirection = 'desc'; // default sort: high → low
    let autoSortByScore = true;
    let scoreHeaderCell;

    function parseNumber(str) {
        return parseFloat(str.replace(/[,%LP\s]/g, '').trim());
    }

    function init() {
        table = document.querySelector("table");
        if (!table) return false;

        tbody = table.querySelector("tbody");
        if (!tbody) return false;

        const rows = Array.from(tbody.querySelectorAll("tr"));
        if (!rows.length) return false;

        headerRow = rows[0];
        if (!headerAdded) {
            scoreHeaderCell = document.createElement("td");
            scoreHeaderCell.className = "score-header";
            scoreHeaderCell.innerText = "Score ↓";
            scoreHeaderCell.style.cursor = "pointer";
            scoreHeaderCell.style.fontWeight = "bold";
            scoreHeaderCell.style.userSelect = "none";
            scoreHeaderCell.style.whiteSpace = "nowrap";
            scoreHeaderCell.style.paddingLeft = "6px";

            scoreHeaderCell.addEventListener("click", () => {
                sortDirection = sortDirection === 'desc' ? 'asc' : 'desc';
                scoreHeaderCell.innerText = sortDirection === 'desc' ? "Score ↓" : "Score ↑";
                autoSortByScore = true;
                calculateScores(true);
            });

            headerRow.appendChild(scoreHeaderCell);

            const otherHeaders = headerRow.querySelectorAll("td:not(.score-header)");
            otherHeaders.forEach(cell => {
                cell.addEventListener("click", () => {
                    autoSortByScore = false;
                    if (scoreHeaderCell) scoreHeaderCell.innerText = "Score";
                });
            });

            headerAdded = true;
        }
        return true;
    }

    function calculateScores(forceSort = false) {
        if (!table && !init()) return;

        const rows = Array.from(tbody.querySelectorAll("tr")).slice(1);
        const scoredRows = [];

        rows.forEach((row, index) => {
            const cells = row.querySelectorAll("td");
            if (cells.length < 12) return;

            const playRate = parseNumber(cells[8]?.innerText || '');
            const games = parseNumber(cells[9]?.innerText || '');
            const winRate = parseNumber(cells[10]?.innerText || '');
            const lp = parseNumber(cells[7]?.innerText || '');

            if ([playRate, games, winRate, lp].some(isNaN)) return;

            const reliability = 1 - Math.exp(-games / 250); // more games = more reliable score boost, full confidence at around 600 games
            const winrateScore = Math.max(0, (winRate - 40) * 2); // every 5% winrate = +10 points
            const lpScore = Math.min(lp * 0.012, 30); // 1.2 point every 100 lp, capped at 2500LP
            const commitmentBonus = playRate >= 50 ? 5 : 0; // + flat 5 if playrate >= 50

            const score = (winrateScore * reliability * 0.8)
                + (lpScore * 0.45)
                + commitmentBonus;

            let scoreCell = row.querySelector(".score-cell");
            if (!scoreCell) {
                scoreCell = document.createElement("td");
                scoreCell.className = "score-cell";
                row.appendChild(scoreCell);
            }

            const roundedScore = score.toFixed(2);
            scoreCell.innerText = roundedScore;
            scoreCell.style.fontWeight = "bold";
            scoreCell.style.color = playRate >= 50 ? "#00ff88" : "#ffffff";

            row.dataset.score = roundedScore;
            row.dataset.lp = lp;
            row.dataset.index = index;
            scoredRows.push(row);
        });

        if (!scoredRows.length) return;

        // 🧠 Only auto-sort if we're sorting by Score
        if (!autoSortByScore && !forceSort) return;

        // ✅ Stable, reversible sort
        const sorted = scoredRows.sort((a, b) => {
            const scoreDiff = parseFloat(b.dataset.score) - parseFloat(a.dataset.score);
            if (Math.abs(scoreDiff) > 0.0001)
                return sortDirection === 'desc' ? scoreDiff : -scoreDiff;

            const lpDiff = parseFloat(b.dataset.lp) - parseFloat(a.dataset.lp);
            if (lpDiff !== 0)
                return sortDirection === 'desc' ? lpDiff : -lpDiff;

            return sortDirection === 'desc'
                ? a.dataset.index - b.dataset.index
                : b.dataset.index - a.dataset.index;
        });

        const others = rows.filter(r => !scoredRows.includes(r));

        const frag = document.createDocumentFragment();
        frag.appendChild(headerRow);
        sorted.forEach(r => frag.appendChild(r));
        others.forEach(r => frag.appendChild(r));

        tbody.innerHTML = '';
        tbody.appendChild(frag);
    }

    setInterval(calculateScores, POLL_INTERVAL);
})();