Top Cut Calculator - Limitless TCG

Calculate tournament top cuts on Limitless TCG

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Top Cut Calculator - Limitless TCG
// @name:pt-BR   Calculadora de Top Cut - Limitless TCG
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Calculate tournament top cuts on Limitless TCG
// @description:pt-BR  Calculadora de top cut para torneios no Limitless TCG
// @author       Marcsx (https://github.com/Marcsx)
// @match        https://play.limitlesstcg.com/tournament/*
// @include      https://play.limitlesstcg.com/tournament/*
// @include      https://play.limitlesstcg.com/tournaments
// @exclude      https://play.limitlesstcg.com/tournament/*/match/*
// @exclude      https://play.limitlesstcg.com/tournament/*/round/*
// @grant        none
// @source       https://github.com/Marcsx/limitless-topcut-calculator
// @supportURL   https://github.com/Marcsx/limitless-topcut-calculator/issues
// @license      MIT
// ==/UserScript==

/*
This is a Tampermonkey adaptation of the Chrome extension created by Marcsx
Original repository: https://github.com/Marcsx/limitless-topcut-calculator
*/

(function() {
    'use strict';

    const translations = {
        en: {
            title: "Top Cut Calculator",
            players: "Players",
            rounds: "Rounds",
            topCut: "Top Cut",
            records: "Records",
            calculate: "Calculate",
            noTopCut: "No Top Cut"
        },
        pt: {
            title: "Calculadora de Top Cut",
            players: "Jogadores",
            rounds: "Rodadas",
            topCut: "Top Cut",
            records: "Recordes",
            calculate: "Calcular",
            noTopCut: "Sem Top Cut"
        }
    };

    class I18n {
        constructor() {
            this.currentLocale = navigator.language.startsWith('pt') ? 'pt' : 'en';
        }

        t(key) {
            return translations[this.currentLocale][key] || translations['en'][key];
        }
    }

    class TopCutCalculator {
        constructor() {
            this.i18n = new I18n();
            this.defaultTopCutRules = [
                { maxPlayers: 8, rounds: 3, topCut: 0 },
                { maxPlayers: 16, rounds: 4, topCut: 4 },
                { maxPlayers: 32, rounds: 6, topCut: 8 },
                { maxPlayers: 64, rounds: 7, topCut: 8 },
                { maxPlayers: 128, rounds: 6, topCut: 16 },
                { maxPlayers: 256, rounds: 7, topCut: 16 },
                { maxPlayers: 512, rounds: 8, topCut: 16 },
                { maxPlayers: 1024, rounds: 9, topCut: 32 },
                { maxPlayers: 2048, rounds: 10, topCut: 32 },
                { maxPlayers: Infinity, rounds: 10, topCut: 64 }
            ];
            this.createStyles();
            this.createElements();
            this.attachEventListeners();
        }

        createStyles() {
            const style = document.createElement('style');
            style.textContent = `
                :root {
                    --primary-color: #121212;
                    --text-color: #ffffff;
                    --surface-color: #1e1e1e;
                    --accent-color: #bb86fc;
                }

                #topcut-fab {
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    width: 56px;
                    height: 56px;
                    border-radius: 50%;
                    background-color: var(--accent-color);
                    box-shadow: 0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12);
                    cursor: pointer;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    z-index: 1000;
                    border: none;
                }

                #topcut-fab:hover {
                    background-color: #9965db;
                }

                #topcut-modal {
                    position: fixed;
                    bottom: 90px;
                    right: 20px;
                    width: 320px;
                    max-width: 90vw;
                    background-color: var(--surface-color);
                    border-radius: 8px;
                    box-shadow: 0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);
                    padding: 16px;
                    color: var(--text-color);
                    z-index: 999;
                    display: none;
                }

                .modal-header {
                    font-size: 18px;
                    font-weight: 500;
                    margin-bottom: 16px;
                }

                .input-group {
                    margin-bottom: 16px;
                }

                .input-group label {
                    display: block;
                    margin-bottom: 8px;
                    color: rgba(255, 255, 255, 0.87);
                }

                .input-group input, .input-group select {
                    width: 100%;
                    padding: 8px;
                    border-radius: 4px;
                    border: 1px solid rgba(255,255,255,0.12);
                    background-color: var(--primary-color);
                    color: var(--text-color);
                }

                .button {
                    background-color: var(--accent-color);
                    color: var(--primary-color);
                    padding: 8px 16px;
                    border-radius: 4px;
                    border: none;
                    cursor: pointer;
                    width: 100%;
                }

                .button:hover {
                    background-color: #9965db;
                }

                .results {
                    margin-top: 16px;
                    padding-top: 16px;
                    border-top: 1px solid rgba(255,255,255,0.12);
                }
            `;
            document.head.appendChild(style);
        }

        createElements() {
            const fab = document.createElement('button');
            fab.id = 'topcut-fab';
            fab.innerHTML = '📊';
            document.body.appendChild(fab);

            const modal = document.createElement('div');
            modal.id = 'topcut-modal';
            modal.innerHTML = `
                <div class="modal-header">
                    ${this.i18n.t('title')}
                </div>
                <div class="input-group">
                    <label>${this.i18n.t('players')}</label>
                    <input type="number" id="players-input" placeholder="Auto-detect">
                </div>
                <div class="input-group">
                    <label>${this.i18n.t('rounds')}</label>
                    <input type="number" id="rounds-input" placeholder="Auto-detect">
                </div>
                <div class="input-group">
                    <label>${this.i18n.t('topCut')}</label>
                    <select id="topcut-input">
                        <option value="auto">Auto</option>
                        <option value="0">${this.i18n.t('noTopCut')}</option>
                        <option value="4">Top 4</option>
                        <option value="8">Top 8</option>
                        <option value="16">Top 16</option>
                        <option value="32">Top 32</option>
                        <option value="64">Top 64</option>
                        <option value="128">Top 128</option>
                    </select>
                </div>
                <button class="button" id="calculate-button">${this.i18n.t('calculate')}</button>
                <div class="results" id="results"></div>
            `;
            document.body.appendChild(modal);
        }

        attachEventListeners() {
            const fab = document.getElementById('topcut-fab');
            const modal = document.getElementById('topcut-modal');
            const calculateButton = document.getElementById('calculate-button');
            const playersInput = document.getElementById('players-input');

            fab.addEventListener('click', () => {
                const isVisible = modal.style.display === 'block';
                modal.style.display = isVisible ? 'none' : 'block';

                if (!isVisible) {
                    this.autoDetectValues();
                }
            });

            calculateButton.addEventListener('click', () => {
                this.calculateTopCut();
            });

            playersInput.addEventListener('change', () => {
                const topCutSelect = document.getElementById('topcut-input');
                if (topCutSelect.value === 'auto') {
                    const players = parseInt(playersInput.value);
                    const suggestedTopCut = this.determineTopCutSize(players);
                    this.updateTopCutSuggestion(suggestedTopCut);
                }
            });
        }

        async autoDetectValues() {
            const url = window.location.href;
            const tournamentId = url.match(/tournament\/(.*?)(\/|$)/)?.[1];

            if (!tournamentId) return;

            try {
                const standingsResponse = await fetch(`https://play.limitlesstcg.com/tournament/${tournamentId}/standings`);
                const standingsText = await standingsResponse.text();
                const playersCount = this.extractPlayersCount(standingsText);

                if (playersCount) {
                    document.getElementById('players-input').value = playersCount;
                    const rule = this.defaultTopCutRules.find(r => playersCount <= r.maxPlayers);

                    if (rule) {
                        document.getElementById('rounds-input').value = rule.rounds;
                        document.getElementById('topcut-input').value = rule.topCut;
                        this.calculateTopCut();
                    }
                }
            } catch (error) {
                console.error('Error auto-detecting values:', error);
            }
        }

        extractPlayersCount(html) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const rows = doc.querySelectorAll('tbody tr');
            return rows.length > 0 ? rows.length - 1 : 0;
        }

        determineTopCutSize(players) {
            const rule = this.defaultTopCutRules.find(r => players <= r.maxPlayers);
            return rule ? rule.topCut : 64;
        }

        calculateTopCut() {
            const playersCount = parseInt(document.getElementById('players-input').value);
            const roundsCount = parseInt(document.getElementById('rounds-input').value);
            const topCutSelect = document.getElementById('topcut-input');

            if (!playersCount || !roundsCount) {
                document.getElementById('results').innerHTML =
                    '<span style="color: rgba(255, 255, 255, 0.87)">Please enter all values.</span>';
                return;
            }

            let topCutSize = topCutSelect.value === 'auto'
                ? this.determineTopCutSize(playersCount)
                : parseInt(topCutSelect.value);

            const results = this.calculatePossibleRecords(roundsCount, topCutSize);
            const topCutPercentage = topCutSize > 0
                ? Math.round((topCutSize/playersCount) * 100)
                : 0;

            document.getElementById('results').innerHTML = `
                <div style="color: rgba(255, 255, 255, 0.87)">
                    <div style="margin-bottom: 2px">${this.i18n.t('topCut')}: ${topCutSize === 0 ? this.i18n.t('noTopCut') : `Top ${topCutSize} (${topCutPercentage}%)`}</div>
                    <div style="white-space: nowrap">${this.i18n.t('records')}: ${results}</div>
                </div>
            `;
        }

        calculatePossibleRecords(rounds, topCutSize) {
            const possibleRecords = [];
            const totalPlayers = parseInt(document.getElementById('players-input').value);
            let remainingSpots = topCutSize;

            for (let wins = rounds; wins >= 0; wins--) {
                const losses = rounds - wins;

                const playersWithThisRecord = Math.round(
                    totalPlayers *
                    this.binomialCoefficient(rounds, wins) *
                    Math.pow(0.5, rounds)
                );

                if (remainingSpots > 0) {
                    const playersAdvancing = Math.min(remainingSpots, playersWithThisRecord);
                    const percentageAdvancing = Math.round((playersAdvancing / playersWithThisRecord) * 100);

                    if (percentageAdvancing > 0) {
                        possibleRecords.push({
                            record: `${wins}-${losses}`,
                            percentage: percentageAdvancing
                        });

                        remainingSpots -= playersAdvancing;
                    }
                }
            }

            return possibleRecords
                .map(r => `${r.record} (${r.percentage}%)`)
                .join(' | ');
        }

        binomialCoefficient(n, k) {
            let result = 1;
            for (let i = 1; i <= k; i++) {
                result *= (n + 1 - i);
                result /= i;
            }
            return result;
        }

        updateTopCutSuggestion(topCut) {
            const results = document.getElementById('results');
            results.innerHTML = topCut === 0
                ? this.i18n.t('noTopCut')
                : `Top ${topCut}`;
        }
    }

    // Initialize the calculator
    new TopCutCalculator();
})();