Coachless Multi-Patch Statistics Merger

Adds a UI to merge additional patches of data on Coachless

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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