Codeforces Visual Enhancer (Dynamic Elo Tier)

rank 排名着色,提交结果流光效果

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Codeforces Visual Enhancer (Dynamic Elo Tier)
// @namespace    http://tampermonkey.net/
// @version      4.7
// @license      MIT
// @description  rank 排名着色,提交结果流光效果
// @author       Gemini & Guanglong Shen
// @match        https://codeforces.com/*
// @match        http://codeforces.com/*
// @match        https://mirror.codeforces.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- 高级配置区 ---
    const BASE_THRESHOLDS = {
        LGM: 0.0006, GM: 0.0037, MAS: 0.0064, CM: 0.0512, EXP: 0.1213, SPC: 0.2189, PPL: 0.3986
    };

    // 1. 样式定义
    const style = document.createElement('style');
    style.innerHTML = `
        @keyframes shine-slide {
            0% { transform: translateX(-150%) skewX(-25deg); opacity: 0; }
            5% { opacity: 1; }
            20% { transform: translateX(250%) skewX(-25deg); opacity: 1; }
            100% { transform: translateX(250%) skewX(-25deg); opacity: 0; }
        }
        .tm-vfx-base { position: relative; display: inline-block; overflow: hidden; vertical-align: middle; line-height: inherit; !important; font-weight: bold !important; padding: 0 2px; }
        .tm-vfx-base::after { content: ''; position: absolute; top: 0; left: 0; width: 60%; height: 100%;
            background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0) 100%);
            animation: shine-slide 2.5s ease-in-out infinite; pointer-events: none; }

        .rank-lgm { color: #aa0000 !important; font-weight: bold; }
        .rank-lgm::first-letter { color: #000000 !important; }
        .rank-gm { color: #ff0000 !important; font-weight: bold; }
        .rank-master { color: #ff8c00 !important; font-weight: bold; }
        .rank-cm { color: #aa00aa !important; font-weight: bold; }
        .rank-expert { color: #0000ff !important; font-weight: bold; }
        .rank-specialist { color: #03a89e !important; font-weight: bold; }
        .rank-pupil { color: #008000 !important; font-weight: bold; }
        .rank-newbie { color: #808080 !important; font-weight: bold; }

        /* 修改:层级信息样式 - 右对齐且不设固定颜色 */
        .tm-tier-info {
            float: right;
            font-size: 0.9em;
            font-weight: bold !important;
            margin-left: 10px;
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
            line-height: 1.2;
        }

        .tm-ac { color: #00a900 !important; }
        .tm-pre { color: #006400 !important; }
        .tm-wa { color: #ff0000 !important; }
        .tm-orange { color: #FF8C00 !important; }
        .tm-blue { color: #00008B !important; }
        .tm-hack { color: #B8860B !important; }
    `;
    document.head.appendChild(style);

    // 2. 获取有效人数
    function getTotalParticipants() {
        const allRows = document.querySelectorAll('.standings tr');
        let statRow = null;
        for (let row of allRows) {
            if (row.innerText.includes('Accepted')) {
                statRow = row;
                break;
            }
        }
        if (!statRow) {
            const pagination = document.querySelector('.pagination');
            if (pagination) {
                const match = pagination.innerText.match(/of\s+(\d+)/);
                if (match) return parseInt(match[1]);
            }
            return 8500;
        }
        const acceptedCounts = [];
        const cells = statRow.querySelectorAll('td');
        cells.forEach(cell => {
            const numbers = cell.innerText.match(/\d+/g);
            if (numbers && numbers.length > 0) {
                acceptedCounts.push(parseInt(numbers[0])); // 只取 Accepted 数
            }
        });
        return acceptedCounts.length > 0 ? Math.max(...acceptedCounts) : 8500;
    }

    // 3. 计算动态阈值
    function getDynamicThresholds(N) {
        const baseline = 8000;
        let ratio = N / baseline;
        if (ratio < 1) ratio = 1;
        const compression = Math.pow(ratio, 0.4);

        return {
            LGM: BASE_THRESHOLDS.LGM / compression,
            GM:  BASE_THRESHOLDS.GM / compression,
            MAS: BASE_THRESHOLDS.MAS / compression,
            CM:  BASE_THRESHOLDS.CM / compression,
            EXP: BASE_THRESHOLDS.EXP,
            SPC: BASE_THRESHOLDS.SPC,
            PPL: BASE_THRESHOLDS.PPL
        };
    }

    // 4. 排名染色及 Who 列增强
    function applyRankColoring() {
        const total = getTotalParticipants();
        const thresholds = getDynamicThresholds(total);
        const rankCells = document.querySelectorAll('.standings td:first-child:not(.header)');

        const tierBounds = [
            { cls: 'rank-lgm', minP: 0, maxP: thresholds.LGM },
            { cls: 'rank-gm', minP: thresholds.LGM, maxP: thresholds.GM },
            { cls: 'rank-master', minP: thresholds.GM, maxP: thresholds.MAS },
            { cls: 'rank-cm', minP: thresholds.MAS, maxP: thresholds.CM },
            { cls: 'rank-expert', minP: thresholds.CM, maxP: thresholds.EXP },
            { cls: 'rank-specialist', minP: thresholds.EXP, maxP: thresholds.SPC },
            { cls: 'rank-pupil', minP: thresholds.SPC, maxP: thresholds.PPL },
            { cls: 'rank-newbie', minP: thresholds.PPL, maxP: 1.0 }
        ];

        // 自动定位 Who 列索引 (防止列顺序变动)
        let whoIndex = 2;
        const headerCells = document.querySelectorAll('.standings th');
        headerCells.forEach((th, idx) => { if(th.innerText.includes('Who')) whoIndex = idx; });

        rankCells.forEach(cell => {
            if (cell.dataset.rankApplied) return;
            const text = cell.innerText.trim();
            if (!text) return;

            let rank = 0;
            const parenMatch = text.match(/\((\d+)\)/); //
            rank = parenMatch ? parseInt(parenMatch[1]) : parseInt(text);
            if (!rank || isNaN(rank)) return;

            const p = rank / total;
            let targetTier = tierBounds[tierBounds.length - 1];

            for (const tier of tierBounds) {
                if (p < tier.maxP) {
                    targetTier = tier;
                    break;
                }
            }

            // 排名列颜色应用
            cell.classList.remove('rank-lgm','rank-gm','rank-master','rank-cm','rank-expert','rank-specialist','rank-pupil','rank-newbie');
            cell.classList.add(targetTier.cls);
            cell.dataset.rankApplied = 'true';

            // 可选:鼠标悬停显示计算的百分比(Debug用)
            cell.title = `Rank: ${rank}/${total} (Top ${(p*100).toFixed(2)}%)`;

            // 计算层级区间信息
            const A = Math.max(1, Math.floor(targetTier.minP * total) + 1);
            const B = Math.floor(targetTier.maxP * total);
            let C = ((rank - A) / Math.max(1, B - A)) * 100;
            C = Math.max(0, Math.min(100, C)).toFixed(1);

            // 注入 Who 列
            const row = cell.parentElement;
            const whoCell = row.cells[whoIndex];
            if (whoCell) {
                // 清理可能存在的旧标签
                const oldInfo = whoCell.querySelector('.tm-tier-info');
                if (oldInfo) oldInfo.remove();

                const infoSpan = document.createElement('span');
                // 关键点:infoSpan 使用和排名一样的 class 来获得颜色
                infoSpan.className = `tm-tier-info ${targetTier.cls}`;
                infoSpan.innerText = `\t(${A}~${B}, ${C}%)`;

                // 确保 Who 单元格内容左对齐,而 span 向右浮动
                whoCell.style.textAlign = 'left';
                whoCell.appendChild(infoSpan);
                whoCell.title = `Tier Range: ${A}~${B}, you are ≈ ${C}% into this tier.`;
            }
        });
    }

    // 5. Verdict 流光 (保持不变)
    function applyVerdictVFX() {
        const verdicts = document.querySelectorAll('span.verdict-accepted, span.verdict-rejected, span.verdict-failed, span.submissionVerdictWrapper, .popup-content span');
        verdicts.forEach(el => {
            if (el.dataset.vfxApplied) return;
            const text = el.innerText.trim();
            if (!text || text.length < 2) return;
            el.classList.add('tm-vfx-base');
            if (text.includes('Accepted') || text.includes('passed')) el.classList.add('tm-ac');
            else if (text.includes('Pretests')) el.classList.add('tm-pre');
            else if (text.includes('Wrong')) el.classList.add('tm-wa');
            else if (text.includes('error')) el.classList.add('tm-orange');
            else if (text.includes('limit')) el.classList.add('tm-blue');
            else if (text.includes('Hacked')) el.classList.add('tm-hack');
            el.dataset.vfxApplied = 'true';
        });
    }

    const run = () => { applyRankColoring(); applyVerdictVFX(); };
    const observer = new MutationObserver(run);
    observer.observe(document.body, { childList: true, subtree: true });
    run();
})();