Greasy Fork镜像 还支持 简体中文。

Tenhou4 Score Pane + Korean translations

Tenhou score pane with safe Korean translation of yaku/text (Tampermonkey userscript) — spacing & full-score fix

目前為 2025-09-09 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Tenhou4 Score Pane + Korean translations
// @namespace    https://example.local/
// @version      1.2.4
// @description  Tenhou score pane with safe Korean translation of yaku/text (Tampermonkey userscript) — spacing & full-score fix
// @author       ChatGPT
// @match        *://tenhou.net/0*
// @match        *://tenhou.net/4*
// @match        *://ron2.jp/3*
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    "use strict";

    const SCORE_SHOW_DELAY_BASE = 20;       // 기본 지연(ms). 0이면 즉시 표시
    const SCORE_SHOW_DELAY_PER_YAKU = 20;  // 역 한 개당 추가 지연(ms). 0이면 즉시 표시

    /*************************************************************************
     * TRANSLATIONS
     * - 일본어 원문 => 한글 대응을 여기에 추가/수정하세요.
     * - 사이트가 출력하는 정확한 문자열(공백/기호 포함)을 복사해서 키로 넣어야 합니다.
     * - 길이가 긴 키가 우선 치환됩니다.
     **************************************************************************/
    const TRANSLATIONS = {
        "立直": "리치",
        "立直(リーチ)": "리치",
        "ダブル立直": "더블리치",
        "一発": "일발",
        "槍槓": "창깡",
        "嶺上開花": "영상개화",
        "海底撈月": "해저로월",
        "河底撈魚": "하저로어",
        "門前清自摸和": "멘젠쯔모",
        "平和": "핑후",
        "一盃口": "이페코",
        "二盃口": "랑페코",
        "七対子": "치또이쯔",
        "役牌 白": "역패 백",
        "役牌 發": "역패 발",
        "役牌 中": "역패 중",
        "役牌 自風牌": "역패 자풍패",
        "自風 北": "자풍 북",
        "自風 南": "자풍 남",
        "自風 西": "자풍 서",
        "自風 東": "자풍 동",
        "役牌 場風牌": "역패 장풍패",
        "場風 北": "자풍 북",
        "場風 南": "자풍 남",
        "場風 西": "자풍 서",
        "場風 東": "자풍 동",
        "断么九": "탕야오",
        "混全帯么九": "찬타",
        "混全帶么九": "찬타",
        "混全帶么9": "찬타",
        "混全帯么9": "찬타",
        "一気通貫": "일기통관",
        "三色同順": "삼색동순",
        "三色同刻": "삼색동각",
        "三槓子": "산깡쯔",
        "対々和": "또이또이",
        "三暗刻": "삼암각",
        "小三元": "소삼원",
        "混老頭": "혼노두",
        "純全帯么九": "준찬타",
        "混一色": "혼일색",
        "清一色": "청일색",
        "ドラ": "도라",
        "裏ドラ": "뒷도라",
        "赤ドラ": "아카도라",
        "1氣通貫": "일기통관",
        "一氣通貫": "일기통관",
        /****************************************/
        // 일반 표기
        "満貫": "만관",
        "跳満": "하네만",
        "倍満": "배만",
        "三倍満": "삼배만",
        "数え役満": "헤아림 역만",
        "役満": "역만",

        "符": "부",
        "飜": "판",
        "点": "점",
        "流局": "유국",
        "流局(九種九牌)": "(유국)9종9패",

        "ロン": "론",
        "ツモ": "쯔모",

        "東": "동",
        "南": "남",
        "西": "서",
        "北": "북",
        // 숫자/기본 문자열 예시
        "Hand ": "패 ",
        "Draw": "유국"
    };

    function escapeRegExp(s) {
        return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    function translateString(s) {
        if (!s || typeof s !== 'string') return s;
        const keys = Object.keys(TRANSLATIONS).sort((a, b) => b.length - a.length);
        for (let k of keys) {
            try {
                const re = new RegExp(escapeRegExp(k), 'g');
                s = s.replace(re, TRANSLATIONS[k]);
            } catch (e) {}
        }
        return s;
    }
    function translateHtml(html) {
        if (!html || typeof html !== 'string') return html;
        const container = document.createElement('div');
        container.innerHTML = html;
        const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
        let node = walker.nextNode();
        while (node) {
            node.nodeValue = translateString(node.nodeValue);
            node = walker.nextNode();
        }
        return container.innerHTML;
    }

    // --- CSS injection (간격 최소화) ---
    const css = `#azpspane {
    position: absolute;
    top: 10px;
    height: 95%;
    right: 0;
    border: 1px solid #444;
    font-family: Arial, sans-serif;
    font-size: 0.9em;
}
#azpspane > div.hands {
    overflow-y: scroll;
    padding: 4px 12px;
    position: absolute;
    bottom: 0;
    right: 0;
    top: 20px;
    left: 0;
    z-index:20;
}
#azpspane > canvas { position: absolute; top: 10px; padding: 0 12px; }
#azpspane > div.hands > div { border-bottom: 1px solid #444; padding: 0.2em 0; margin: 0; line-height: 1.1; }
#azpspane .hidden { display: none; }
#azpspane > div >  div:nth-child(2) { background-color: #111; }
#azpspane table { margin: 0.15em 0; padding: 0; border-collapse: collapse; }
#azpspane h2, #azpspane h3 { margin: 0.12em 0; padding: 0; font-weight: 600; }
#azpspane table td { padding: 0 0.15em; vertical-align: middle; }
.azpsscores td { text-align: right; }
.azpsscores td:nth-child(1), .azpsscores td:nth-child(2) { text-align: center; }
#azpspane canvas.tile-image { display: inline-block; vertical-align: top; max-height: 54px; margin-right: 6px; }
.azpsicons { font-family: icons2, sans-serif; }
.azpsplus { color: #8F8; }
.azpsminus { color: #F88; }
.azpsgrey { color: grey; font-size: 0.95em; }`;
    const styleEl = document.createElement('style');
    styleEl.textContent = css;
    document.head.appendChild(styleEl);

    // --- 기존 로직 (개선 포함) ---
    const $ = window.jQuery;
    let mutationObserver;
    let isT4;
    let isParlour;
    let isMultipleRon;
    let thisHandName = '';
    let previousHandName = 'y';
    let timeOfLastWin = 0;
    let handNum = 1;
    let playerName = null;
    let graphData = {};
    let allowNewHands = true;
    const paneID = 'azpspane';

    // 중복 처리 방지용 WeakSet
    let processedNodes = new WeakSet();

    function resetGraphData() {
        graphData = {
            data: {
                datasets: [{
                    borderColor: "#A00",
                    data: [ ],
                    fill: false,
                    label: 'A'
                }, {
                    borderColor: "#22F",
                    data: [ ],
                    fill: false,
                    label: 'B'
                }, {
                    borderColor: "#3F3",
                    data: [ ],
                    fill: false,
                    label: 'C'
                }, {
                    borderColor: "#FF3",
                    borderWidth: 6,
                    data: [ ],
                    fill: false,
                    label: 'D'
                }],
                labels: [0]
            },
            options: {
                elements: {
                    line: {
                        borderWidth: 3,
                        cubicInterpolationMode: 'monotone',
                        lineTension: 0,
                        spanGaps: true,
                        steppedLine: true
                    }
                },
                layout: { padding: { bottom: 0, left: 0, right: 10, top: 0 } },
                legend: { labels: { boxWidth: 20, fontColor: '#EEE' } },
                scales: {
                    xAxes: [{ ticks: { display: false } }],
                    yAxes: [{ ticks: { callback: function(value) { return '' + value/1000 + 'k'; } } }]
                },
                title: { display: true, padding: 0, position: 'bottom', text: 'ApplySci Tenhou Score Pane' }
            },
            type: 'line'
        };
    }

    function getGamePane() {
        if (isT4 === undefined) {
            isT4 = window.location.pathname.substring(0,2) === '/4';
        }
        if (isT4) return $('div.nosel:lt(2)');
        return $('div.nosel > div.nosel.tbl:first');
    }

    function setToObserve() {
        mutationObserver.observe(document.documentElement, { characterData: true, childList: true, subtree: true });
    }

    function setWidth() {
        let gamePane = getGamePane();
        $('#' + paneID).css({ 'width': Math.floor($('body').width() - gamePane.width() - 10) });
        moveMainPane();
    }

    function moveMainPane() {
        let gamePane = getGamePane();
        if (isT4) {
            gamePane.css('transform' ,'translateX(0)');
        } else {
            gamePane.css({'margin-left': 10, 'left':0}).next().css('left', 0);
            $.find('.tbc.ts0:not(.bblink)').forEach(function bringForward(el) { $(el).parent().css('z-index', 50); });
        }
    }

    function scorePaneInit() {
        allowNewHands = true;
        isParlour = false;
        $('#' + paneID)
            .append($('<button>').addClass('azpsreset').click(resetPane).text('reset score pane'))
            .append($('<div>').addClass('hands').append($('<h3>').text('The ApplySci Score Pane').attr('id', 'azps_start')));
    }

    function scorePane() {
        let pane = $('#' + paneID);
        let fontsize = isT4 ? '0.7em' : '0.4em';
        if (pane.length === 0) {
            pane = $('<div>').prop('id', paneID).css('fontSize', fontsize);
            $('body').append(pane);
            setWidth();
            scorePaneInit();
            resetBetweenGames();
        }
        if (!('data' in graphData)) resetGraphData();
        return pane;
    }

    function resetPane() {
        resetBetweenGames();
        scorePane().empty();
        scorePaneInit();
    }

    function rememberPlayerName(node) {
        if (playerName !== null) return;
        let players;
        if (isT4) {
            players = $('.bbg5', node);
            let me = players.eq(players.length - 1);
            if (players.length === 3 && graphData.data.datasets.length === 4) graphData.data.datasets.splice(2,1);
            if (me.length) playerName = me.children('span:eq(1)').text();
            for (let i=0; i < players.length; i++) {
                let name = players.eq(i).children('span:last').text();
                graphData.data.datasets[i].label = name;
            }
        } else {
            let player = $('#sc00', node);
            if (player.length) {
                if ($('#sc03', node).length === 0 && graphData.data.datasets.length === 4) graphData.data.datasets.splice(2,1);
                playerName = player.children('span:last').text();
                graphData.data.datasets[graphData.data.datasets.length - 1].label = decodeURIComponent(playerName);
                for (let i=1; i<4; i++) {
                    player = $('#sc0'+i, node);
                    if (player.length > 0) {
                        let name = player.children('span:last').text();
                        graphData.data.datasets[3 - i].label = name;
                    }
                }
            }
        }
    }

    function getHandName(node) {
        if (isT4) {
            try {
                let scoreTable = getT4ScoreTable(node);
                if (!scoreTable || scoreTable.length === 0) return false;
                let honbaString = scoreTable.find('td:first')[0].childNodes[1].nodeValue;
                if (honbaString === null) return false;
                let nHonba =  honbaString.trim();
                let hand = $('div.nosel > div.nopp > div.nopp > span.gray:first').eq(0).parent().find('span').slice(0,2).text();
                if (nHonba !== '0') hand += '-' + nHonba;
                handNum++;
                return hand;
            } catch (e) {
                return false;
            }
        } else {
            return 'Hand ' + handNum++;
        }
    }

    // safer showResult: 텍스트 노드만 번역 후 DOM 삽입
    function showResult(texts, handName, node, hide) {
        try {
            texts = translateHtml(String(texts));
        } catch (e) {
            console.error('translateHtml failed', e);
        }

        // handName이 주어졌고 같은 id가 이미 존재하면 중복 삽입하지 않음
        if (handName) {
            const id = 'azps_' + handName.replace(/ /g, '_');
            if (document.getElementById(id)) {
                const existing = document.getElementById(id).parentElement;
                if (existing) {
                    $('div.hands', scorePane()).prepend(existing);
                    return $(existing);
                }
            }
        }

        let newEl = $('<div>').html(texts);
        if (hide) newEl.addClass('hidden');
        $('div.hands', scorePane()).prepend(newEl).prop('scrollTop', 0);

        // only create tile-canvas if getHandImageT4 exists (avoid empty large canvas)
        if (node !== null) {
            if (isT4) {
                if (typeof window.getHandImageT4 === 'function') {
                    let tiles = document.createElement('canvas');
                    tiles.className = 'tile-image';
                    newEl.prepend(tiles);
                    try { window.getHandImageT4(node, tiles); } catch(e) {}
                }
            } else {
                // T3 image handling skipped
            }
        }

        newEl.prepend($('<h2>').text(handName || '').attr('id', 'azps_' + (handName ? handName.replace(/ /g, '_') : Math.random().toString(36).slice(2))));
        return newEl;
    }

    function getVal(node) { return node.nodeValue || node.innerText; }

    function appendNodes(fromDom) {
        let toString = '';
        fromDom.childNodes.forEach(function appendOneNode(node) { toString += getVal(node) + ' '; });
        // trim 연속 공백 및 줄바꿈 제거
        return toString.replace(/\s+/g, ' ').trim();
    }

    function riichiHonba(node) {
        try {
            return '<span class=azpsicons>' + $("tr:first td:first", node)[0].innerText + '</span>';
        } catch (e) {
            return '';
        }
    }

    // doubleZero: 원래대로 작은 '00' span 복구 — T4에서 240 + 00 => 24000 형태로 보이게 함
    const doubleZero = '<span style="font-size:85%;opacity:0.75;">00</span>';

    function chartOneScore(player, totalScore, score) {
        if (graphData.data.datasets[player].data.length === 0) graphData.data.datasets[player].data.push(totalScore);
        graphData.data.datasets[player].data.push(totalScore + parseFloat(score));
    }

    function checkParlour(node, nNodes) {
        let brCount = 0;
        for (let i=0; i < nNodes; i++) {
            if (node.childNodes[i].tagName !== undefined && node.childNodes[i].tagName.toUpperCase() === 'BR') brCount++;
        }
        return brCount > 1;
    }

    function deShuugify(txt) { return txt.replace( /^([-+0-9]+).*$/ , '$1🔴' ); }

    function getOneScore(node, player) {
        let nNodes = node.childNodes.length;
        if (nNodes === 0) return '';
        let isBystander, totalLine = '', totalScore, deltaScore, totalShuugi, deltaShuugi;
        isParlour = checkParlour(node, nNodes);
        [0, 2].forEach(function (idx) { totalLine += '<td>' + getVal(node.childNodes[idx]) + '</td>'; });
        totalLine += '<td>';
        if (isT4) {
            // 기존 로직 유지: tenhou T4는 점수를 '100단위'로 둬서 화면에는 '숫자 + 00'을 붙이는 방식 사용
            isBystander = (isParlour && nNodes === 7) || nNodes == 5;
            if (isParlour) {
                totalScore = parseFloat(getVal(node.childNodes[4])) / 100;
                totalShuugi = deShuugify(getVal(node.childNodes[isBystander ? 6 : 7]));
                totalLine += totalScore + doubleZero + '</td><td>' + totalShuugi;
                deltaScore = isBystander ? 0 : node.childNodes[5].innerHTML.slice(0, -2); // 마지막 '00' 제거
                deltaShuugi = isBystander || node.childNodes.length < 9 ? 0 : deShuugify(getVal(node.childNodes[8]));
            } else {
                totalScore = parseFloat(getVal(node.childNodes[4])) / 100;
                totalLine += totalScore + doubleZero;
                deltaScore = isBystander ? 0 : node.childNodes[5].innerHTML.slice(0, -2);
            }
        } else {
            isBystander = (isParlour && nNodes === 8) || nNodes == 6;
            if (isParlour) {
                totalScore = parseFloat(getVal(node.childNodes[4]));
                totalShuugi = deShuugify(getVal(node.childNodes[isBystander ? 7 : 9]));
                totalLine += totalScore + doubleZero + '</td><td>' + totalShuugi;
                deltaScore = isBystander ? 0 : getVal(node.childNodes[7].childNodes[0]);
                deltaShuugi = isBystander || node.childNodes.length < 11 ? 0 : deShuugify(getVal(node.childNodes[10]));
            } else {
                totalScore = parseFloat(getVal(node.childNodes[4]));
                totalLine += totalScore + doubleZero;
                deltaScore = isBystander ? 0 : getVal(node.childNodes[7].childNodes[0]);
            }
        }
        if (isBystander) {
            totalLine = '<tr>' + totalLine + '</td><td>' + (isParlour ? '</td><td>' : '');
        } else {
            totalLine =  '<tr class="' + (deltaScore > 0 ? 'azpsplus' : 'azpsminus') + '">' + totalLine + '<td>' + deltaScore + doubleZero;
            if (isParlour) totalLine += '</td><td>' + (deltaShuugi === 0 ? '' : deltaShuugi);
        }
        // chart expects raw point values (so multiply appropriately)
        // 기존 코드가 사용하던 방식 유지: graph receives 100*totalScore (so restoring doubleZero doesn't break graph)
        chartOneScore(player, 100*totalScore, 100*parseFloat(deltaScore || 0));
        return totalLine + '</td></tr>';
    }

    function scoreTableT3(node) {
        let totalLine = '<table class=azpsscores>';
        isMultipleRon = false;
        for (let i=0; i<4; i++) {
            let elem = $('#sc0' + i, node);
            if (elem.length) totalLine += getOneScore(elem[0], 3 - i);
        }
        return totalLine + '</table>';
    }

    function scoreTableT4(node) {
        let players = $('.bbg5', node);
        let table = '<table class=azpsscores>';
        isMultipleRon = thisHandName === previousHandName;
        for (let i=0; i < players.length; i++) table += getOneScore(players.eq(i)[0], i);
        return table + '</table>';
    }

    function getT4ScoreTable(node) {
        let t = $('table .bbg5', node).parents('table:first');
        if (!t || t.length === 0) {
            t = $('table', node).first();
        }
        return t;
    }

    function waitForYakuAndScore(node, callback, attempt) {
        attempt = typeof attempt === 'number' ? attempt : 0;
        const MAX_ATTEMPTS = 12;
        const INTERVAL_MS = 80;

        try {
            let yakuPresent = $(node).find('.yk, .ym, .hn').length > 0;
            let scoreTablePresent = getT4ScoreTable(node).length > 0;
            if (yakuPresent && scoreTablePresent) {
                callback();
                return;
            }
        } catch (e) {}

        if (attempt < MAX_ATTEMPTS) {
            setTimeout(function() { waitForYakuAndScore(node, callback, attempt + 1); }, INTERVAL_MS);
        } else {
            try { callback(); } catch (e) { console.error('final callback failed', e); }
        }
    }

    function showExhaustiveDraw(node) {
        scorePane();
        rememberPlayerName(node);
        let outcome, block = '<h3>Draw ';
        if (isT4) {
            outcome = $('table', node);
            let handName = getHandName(node);
            if (handName && !graphData.data.labels.includes(handName)) graphData.data.labels.push(handName);
            block += riichiHonba(getT4ScoreTable(node)) + '</h3>' + scoreTableT4(outcome);
            showResult(block, handName, null, false);
        } else {
            outcome = node.childNodes[0].childNodes[1];
            let handName = getHandName(node);
            if (handName && !graphData.data.labels.includes(handName)) graphData.data.labels.push(handName);
            block += riichiHonba(outcome) + '</h3>' + scoreTableT3(outcome);
            showResult(block, handName, null, false);
        }
    }

    function yakuLine(yaku, han) {
        let nHanElements = han && han.childNodes ? han.childNodes.length : 0;
        let hanString;
        if (nHanElements < 2) hanString = getVal(han);
        else hanString = (getVal(han.childNodes[0]).trimLeft ? getVal(han.childNodes[0]).trimLeft() : getVal(han.childNodes[0]).trim()) + ' ' + getVal(han.childNodes[1]);
        if (nHanElements > 2) hanString += ' ' + getVal(han.childNodes[2]) + '🔴';
        return '<tr' + ((hanString.length > 0 && hanString[0] === '0') ? ' class=azpsgrey' : '') + '><td>' + yaku + '</td><td>' + hanString + '</td></tr>';
    }

    function isLogReplay() { return false; }

    function insertWinTableIntoDOM(node, totalLine, nYaku) {
        // 결과 처리 시작 시 handAssignedForCurrentResult 같은 플래그는 이미 초기화되어 있다고 가정
        let handName = getHandName();
        if (handName !== false) {
            graphData.data.labels.push(handName);
            let scoreDiv = showResult(totalLine, handName, node, true);

            // 지연을 계산해서 보여줌
            const delay = Math.max(0, SCORE_SHOW_DELAY_BASE + (nYaku || 0) * SCORE_SHOW_DELAY_PER_YAKU);
            if (delay === 0) {
                scoreDiv.removeClass('hidden');
            } else {
                setTimeout(() => scoreDiv.removeClass('hidden'), delay);
            }
        }
    }

    function winTableT3(newNode) {
        let totalLine, nYaku;
        let now = Date.now();
        let node = newNode.children[0];
        if (now - timeOfLastWin < 20000 && !isLogReplay()) handNum--;
        timeOfLastWin = now;
        totalLine = appendNodes(node.children[0]) + '<br>' + riichiHonba(node.childNodes[2]);
        totalLine += '<table>';
        let yakuTable = $("tr:not(:has(table))", node.childNodes[1]);
        nYaku = yakuTable.length;
        yakuTable.each(function addYakuLine(row) { totalLine += yakuLine(getVal(this.childNodes[0]), this.childNodes[1]); });
        totalLine += '</table>';
        totalLine += scoreTableT3(node.childNodes[2]);
        insertWinTableIntoDOM(node, totalLine, nYaku);
    }

    function winTableT4(node) {
        if (processedNodes.has(node)) return;
        waitForYakuAndScore(node, function doWinTable() {
            if (processedNodes.has(node)) return;
            try {
                let scoreTable = getT4ScoreTable(node);
                let totalLine = '';
                try {
                    let s0div = $('div.s0 > div:eq(1)', node)[0];
                    if (s0div) totalLine = appendNodes(s0div) + '<br>' + riichiHonba(scoreTable);
                    else totalLine = riichiHonba(scoreTable);
                } catch (e) {
                    totalLine = riichiHonba(scoreTable);
                }

                totalLine += '<table>';
                let yakuNames = $('.yk', node);
                if (yakuNames.length === 0) yakuNames = $('.ym', node);
                let yakuHans  = $('.hn', node);
                let nYaku = yakuNames.length;
                for (let i = 0; i < nYaku; i++) {
                    try {
                        totalLine += yakuLine($(yakuNames[i]).text(), yakuHans[i]);
                    } catch (e) {}
                }
                totalLine += '</table>';
                totalLine += scoreTableT4(scoreTable);
                insertWinTableIntoDOM(node, totalLine, nYaku);
                processedNodes.add(node);
            } catch (e) {
                console.error('winTableT4 failed', e);
            }
        }, 0);
    }

    function handleWin(node) {
        if (processedNodes.has(node)) return;
        scorePane();
        rememberPlayerName(node);
        if (isT4) winTableT4(node); else winTableT3(node);
    }

    function hasWon() { console.log('winner, winner, chicken dinner'); }
    function resetBetweenGames() { playerName = null; handNum = 1; resetGraphData(); processedNodes = new WeakSet(); }

    function curryClickChart(chart, labels) {
        return function clickChart(evt){
            evt.stopPropagation(); evt.preventDefault();
            const activeXPoints = chart.getElementsAtXAxis(evt);
            if (!activeXPoints || !activeXPoints[0]) return false;
            let handNumber = activeXPoints[0]._index;
            let id;
            if (handNumber === 0) id = 'azps_start'; else id = 'azps_' + labels[handNumber].replace(/ /g, '_');
            let el = document.getElementById(id);
            if (el) el.scrollIntoView();
            return false;
        };
    }

    function scoreChart() {
        let pane = $('#'+paneID);
        if ($('canvas.chart', pane).length) return;
        let chartEl = $('<canvas>').addClass('chart');
        pane.prepend(chartEl);
        chartEl.height = Math.ceil(pane.width * 0.6);
        try { Chart.platform.disableCSSInjection = true; } catch(e) {}
        const chart = new Chart(chartEl[0], graphData);
        $('div.hands', pane).css('top', chartEl.offset().top + chartEl.outerHeight(true) + 20);
        chartEl.click(curryClickChart(chart, graphData.data.labels));
    }

    function checkWinner(node) {
        try {
            let winner;
            if (isT4) winner = $('.bbg5:first')[0].childNodes[0].nodeValue;
            else winner = $('table > tbody > tr > td:first', node)[0].childNodes[0].nodeValue;
            let isWinner = winner === playerName;
            if (isWinner && ($('div.tbc.bgb:contains(Exit)').length + $('button:contains(Exit)').length === 0)) hasWon();
        } catch (e) {}
    }

    function handleEnd(node) {
        scorePane();
        allowNewHands = false;
        scoreChart();
        resetBetweenGames();
        checkWinner(node);
    }

    function removePane() {
        $('#' + paneID).remove();
        let gamePane = getGamePane();
        if (isT4) {
            gamePane.css('transform' ,'translateX(' + Math.round(($('body').width() - gamePane.width())/2) + 'px)');
        } else {
            gamePane.css('margin', '0 auto');
        }
        resetBetweenGames();
        allowNewHands = true;
    }

    function showAbortiveDraw(node) { return showExhaustiveDraw(node); }

    function handleStart(node) {
        allowNewHands = true;
        if ($('#' + paneID + ' > div.hands > div').length > 0) return false;
        resetPane(); rememberPlayerName(node);
    }

    function stringStartsWith(haystack, needles) {
        let found = false;
        needles.some(function testOneNeedle(needle) {
            if (haystack.substr(0, needle.length) === needle) { found = true; return true; }
        });
        return found;
    }

    function checkNode(oneNode) {
        let testText = oneNode.innerText;
        if (typeof testText === 'undefined' || testText === null) return;
        if ($('#' + paneID).length && ( $('#pane1', oneNode).length || (isT4 && oneNode.className.includes('s0') && testText.includes('Online:')) )) {
            return removePane();
        }
        if (!allowNewHands) return;

        if (oneNode.className && oneNode.className.includes(isT4 ? 'nopp' : 'tbc') && testText.length > 10) {
            if (stringStartsWith(testText, ['Start', '對局', 'Début', 'Bắt đầu'])) return handleStart(oneNode);
            if (stringStartsWith(testText, ['終局','End', 'Fin', 'Koniec'])) return handleEnd(oneNode);
            if (stringStartsWith(testText, ['Redeal', '流局', 'Ryuukyoku', 'Rejouer', 'Ván hoà', 'Powtórka'])) return showExhaustiveDraw(oneNode);
        }

        try {
            if (oneNode.childNodes && oneNode.childNodes[0] && oneNode.childNodes[0].childNodes && oneNode.childNodes[0].childNodes[0] && oneNode.childNodes[0].childNodes[0].id === 'total') {
                return handleWin(oneNode);
            }
        } catch (e) {}

        try {
            if (isT4) {
                if ($(oneNode).find('.yk, .ym, .hn').length > 0 || $(oneNode).find('.bbg5').length > 0) {
                    return handleWin(oneNode);
                }
                if (oneNode.className && oneNode.className.includes('nopp') && testText.length > 20) {
                    return handleWin(oneNode);
                }
            } else {
                if (oneNode.className === 'tbc' && $('button', oneNode).length && $('table', oneNode).length === 1 && $('#sc00', oneNode).length && testText.includes('') && testText.includes('')) {
                    if (stringStartsWith(testText, ['觀戰', 'Redeal: ', 'Torpillage: ', 'Ván hoà: ', 'Powtórka (', 'Kyuushu kyuuhai', 'Kyūshu kyūhai', 'Suukaikan', 'Sūkaikan', 'Suufon renda', 'Sūfon renda', 'Sanchahou', 'Sanchahō', 'Suucha riichi', 'Sūcha riichi'])) {
                        return showAbortiveDraw(oneNode);
                    }
                }
            }
        } catch (e) {}
    }

    function onMutate(mutations) {
        mutationObserver.disconnect();
        mutations.forEach(function doAMutation(oneMutation) {
            if (oneMutation.addedNodes && oneMutation.addedNodes.length) {
                oneMutation.addedNodes.forEach(function do1node(node) {
                    try { if (node.childNodes && node.childNodes.length) checkNode(node); } catch (e) { console.log(e); }
                });
            }
            if (oneMutation.type === 'characterData' && oneMutation.target && oneMutation.target.parentNode) {
                try { checkNode(oneMutation.target.parentNode); } catch (e) {}
            }
        });
        setToObserve();
    }

    // init
    (function init() {
        try { Chart.platform.disableCSSInjection = true; } catch (e) {}
        getGamePane();
        mutationObserver = new MutationObserver(onMutate);
        setToObserve();
        let timeout;
        $(window).on('resize', function() {
            if (timeout) clearTimeout(timeout);
            timeout = setTimeout(setWidth, 1000);
        });
    }());

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址