Coachless Multi-Patch Statistics Merger

Adds a UI to merge additional patches of data on Coachless

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Coachless Multi-Patch Statistics Merger
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds a UI to merge additional patches of data on Coachless
// @author       FlayInAHook (and AI)
// @match        https://coachless.gg/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const TARGET_URL = 'api/ChampionWinprob/GetGlobalItemStatistics';

    // --- UI & STATE ---
    // "0" means only the current patch. "1" means current + 1 previous, etc.
    let additionalPatches = parseInt(localStorage.getItem('coachless_extra_patches')) ?? 1;

    function createUI() {
        const container = document.createElement('div');
        container.id = 'patch-merger-ui';
        container.innerHTML = `
            <div style="font-weight: bold; margin-bottom: 5px; font-size: 12px; color: #00ceff; letter-spacing: 0.5px;">COACHLESS HELPER</div>
            <div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
                <label style="font-size: 11px; white-space: nowrap;">Additional patches:</label>
                <input type="number" id="extra-patch-input" value="${additionalPatches}" min="0" max="10"
                       style="width: 45px; background: #1a1a1a; color: #00ceff; border: 1px solid #333; border-radius: 4px; padding: 2px 4px; font-weight: bold; text-align: center;">
            </div>
            <div style="font-size: 9px; margin-top: 6px; color: #888; text-align: right;">Range: 0 - 10</div>
        `;

        Object.assign(container.style, {
            position: 'fixed',
            bottom: '24px',
            right: '24px',
            backgroundColor: 'rgba(10, 10, 10, 0.95)',
            border: '1px solid #00ceff',
            borderRadius: '10px',
            padding: '12px',
            zIndex: '99999',
            color: 'white',
            fontFamily: '"Segoe UI", Roboto, Helvetica, Arial, sans-serif',
            boxShadow: '0 8px 32px rgba(0,0,0,0.8)',
            backdropFilter: 'blur(6px)',
            transition: 'all 0.3s ease'
        });

        document.body.appendChild(container);

        document.getElementById('extra-patch-input').addEventListener('change', (e) => {
            let val = parseInt(e.target.value);
            if (isNaN(val)) val = 0;
            if (val < 0) val = 0;
            if (val > 10) val = 10;

            additionalPatches = val;
            e.target.value = val;
            localStorage.setItem('coachless_extra_patches', additionalPatches);
            console.log(`[Coachless] Now set to fetch ${additionalPatches} additional patches.`);
        });
    }

    // Inject UI when document is ready
    const injectInterval = setInterval(() => {
        if (document.body) {
            createUI();
            clearInterval(injectInterval);
        }
    }, 500);


    // --- XHR INTERCEPTION ---
    const rawOpen = XMLHttpRequest.prototype.open;
    const rawSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._url = url;
        this._method = method;
        return rawOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = async function(body) {
        if (this._url && this._url.includes(TARGET_URL) && this._method === 'POST') {
            console.group('%c[Coachless Multi-Interceptor]', 'color: #00ceff; font-weight: bold;');

            try {
                const baseBody = JSON.parse(body);
                const startPatch = baseBody.commonFilters.patch.patch;
                const requests = [];

                console.log(`Main request: Patch ${startPatch}. Additional to fetch: ${additionalPatches}`);

                // Loop: i=0 is current patch, i=1 is current-1, etc.
                // We fetch up to (0 + additionalPatches)
                for (let i = 0; i <= additionalPatches; i++) {
                    const targetPatch = startPatch - i;
                    if (targetPatch < 0) {
                        console.warn(`Stopping at patch 0. Cannot go negative.`);
                        break;
                    }

                    const newBody = JSON.parse(body);
                    newBody.commonFilters.patch.patch = targetPatch;

                    requests.push(
                        fetch(this._url, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(newBody)
                        }).then(r => r.json().catch(() => [])) // Handle potential empty/fail responses
                    );
                }

                const allResults = await Promise.all(requests);
                console.log(`Retrieved ${allResults.length} total datasets.`);

                const mergedData = mergeAllStats(allResults);

                // Re-inject the combined data back into the original XHR object
                const mergedString = JSON.stringify(mergedData);
                Object.defineProperty(this, 'responseText', { value: mergedString, writable: true });
                Object.defineProperty(this, 'response', { value: mergedString, writable: true });
                Object.defineProperty(this, 'status', { value: 200, writable: true });
                Object.defineProperty(this, 'readyState', { value: 4, writable: true });

                console.log(`Merge complete: ${mergedData.length} unique items generated.`);
                console.groupEnd();

                // Fake the completion events so the UI updates
                this.dispatchEvent(new Event('readystatechange'));
                this.dispatchEvent(new Event('load'));
                return;

            } catch (err) {
                console.error('[Coachless] Error during merge:', err);
                console.groupEnd();
            }
        }
        return rawSend.apply(this, arguments);
    };

    /**
     * Standard Weighted Average Merger
     */
    function mergeAllStats(resultsArray) {
        const itemMap = new Map();
        const weight = (val1, n1, val2, n2) => {
            if (n1 + n2 === 0) return 0;
            return ((val1 * n1) + (val2 * n2)) / (n1 + n2);
        };

        resultsArray.forEach(patchData => {
            if (!Array.isArray(patchData)) return;

            patchData.forEach(item => {
                if (itemMap.has(item.itemId)) {
                    const existing = itemMap.get(item.itemId);
                    const totalOccur = existing.occurrence + item.occurrence;

                    existing.winrateObserved = weight(existing.winrateObserved, existing.occurrence, item.winrateObserved, item.occurrence);
                    existing.winrateExpected = weight(existing.winrateExpected, existing.occurrence, item.winrateExpected, item.occurrence);
                    existing.wpaOverall = weight(existing.wpaOverall, existing.occurrence, item.wpaOverall, item.occurrence);
                    existing.bias = weight(existing.bias, existing.occurrence, item.bias, item.occurrence);
                    existing.averagePurchaseTime = weight(existing.averagePurchaseTime, existing.occurrence, item.averagePurchaseTime, item.occurrence);

                    existing.occurrence = totalOccur;
                } else {
                    itemMap.set(item.itemId, { ...item });
                }
            });
        });

        return Array.from(itemMap.values());
    }
})();