Coachless Multi-Patch Statistics Merger

Adds a UI to merge additional patches of data on Coachless

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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());
    }
})();