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