Coachless Multi-Patch Statistics Merger

Adds a UI to merge additional patches of data on Coachless

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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