CBG Helper

A helper tool for Onmyoji player to look for good account.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         CBG Helper
// @namespace    https://yys.zhebu.work/
// @version      0.1.7
// @description  A helper tool for Onmyoji player to look for good account.
// @author       CJ
// @match        https://yys.cbg.163.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==
(function() {
    'use strict';
    let panel_class_name = 'content-overview';
    let acct_info = {
        ready: false
    };
    let FRAC_N = 5;
    let url_match = "api/get_equip_detail";
    let suit_imp = ["散件", "招财猫", "火灵", "蚌精", "共潜", '遗念火']; // 重要套装,可自行添加
    let suit_by_props = {
        '暴击': ["针女", "三味", "网切", "伤魂鸟", "破势", "镇墓兽", "青女房", "海月火玉"],
        '攻击加成': ["蝠翼", "轮入道", "狰", "鸣屋", "心眼", "阴摩罗", "狂骨", "兵主部", "贝吹坊"],
        '防御加成': ["珍珠", "魅妖", "雪幽魂", "招财猫", "反枕", "日女巳时", "木魅", "出世螺"],
        '生命加成': ["地藏像", "涅槃之火", "被服", "镜姬", "钟灵", "薙魂", "树妖", "涂佛", "恶楼"],
        '效果抵抗': ["骰子鬼", "返魂香", "魍魉之匣", "幽谷响", "共潜"],
        '效果命中': ["蚌精", "火灵", "飞缘魔", "遗念火"],
        '首领御魂': ["土蜘蛛", "胧车", "荒骷髅", "地震鲶", "蜃气楼", "鬼灵歌伎"]
    }

    let _open = XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function(method, URL) {
        let _onreadystatechange = this.onreadystatechange,
            _this = this;

        _this.onreadystatechange = function() {
            // catch only completed 'api/search/universal' requests
            if (_this.readyState === 4 && _this.status === 200 && ~URL.indexOf(url_match)) {
                try {
                    //////////////////////////////////////
                    // THIS IS ACTIONS FOR YOUR REQUEST //
                    //             EXAMPLE:             //
                    //////////////////////////////////////
                    let data = JSON.parse(_this.responseText); // {"fields": ["a","b"]}

                    data = floatify(data)

                    // rewrite responseText
                    Object.defineProperty(_this, 'responseText', {
                        value: JSON.stringify(data)
                    });
                    Object.defineProperty(_this, 'response', {
                        value: JSON.stringify(data)
                    });
                    /////////////// END //////////////////
                } catch (e) {}

                console.log('Caught! :)', method, URL /*, _this.responseText*/ );
            }
            // call original callback
            if (_onreadystatechange) _onreadystatechange.apply(this, arguments);
        };

        // detect any onreadystatechange changing
        Object.defineProperty(this, "onreadystatechange", {
            get: function() {
                return _onreadystatechange;
            },
            set: function(value) {
                _onreadystatechange = value;
            }
        });

        return _open.apply(_this, arguments);
    };

    function nowrapText(textLabel) {
        return `<span class="cbghelper_nowrap">${textLabel}</span>`
    }

    function addExtendedHighlight() {
        if (document.getElementById('cbghelper_exthighlight') || !acct_info.hasOwnProperty("summary")) {
            return;
        }
        let {
            fastest,
            heads,
            feet,
            hero_info
        } = acct_info.summary;
        let itms = [];
        let build_item = function(label, id) {
            let li = document.createElement('li');
            li.innerText = label;
            return li
        };
        //collection of heros
        let total = hero_info['ssr']['all'] + hero_info['sp']['all'];
        let got_total = hero_info['ssr']['got'] + hero_info['sp']['got'];
        if (total === got_total) {
            itms.push(build_item('SSR/SP全收集'));
        } else if (hero_info['ssr']['all'] === hero_info['ssr']['got']) {
            itms.push(build_item('SSR全收集'));
        }
        if (hero_info['x']['all'] === hero_info['x']['got']) {
            itms.push(build_item('联动全收集'));
        }
        //number of heads and feet
        if (heads.length > 0 || feet.length > 0) {
            let x = heads.length > 0 ? heads.length : '无';
            let y = feet.length > 0 ? feet.length : '无';
            let label = `${x}头${y}脚`;
            itms.push(build_item(label))
        }
        //fastest speed
        let fastest_spd_label = `最快一速${[1, 2, 3, 4, 5, 6].reduce((total, p) => total + fastest[p]['散件'], 0).toFixed(2)}`;
        let fastest_spd = build_item(fastest_spd_label)
        fastest_spd.id = 'cbghelper_exthighlight';
        itms.push(fastest_spd);
        //fastest zhaocai speed
        let zc_spd_val = [1, 2, 3, 4, 5, 6].reduce((total, p) => total + fastest[p]['招财猫'], 0);
        let spd_inc = [1, 2, 3, 4, 5, 6].map(p => fastest[p]['散件'] - fastest[p]['招财猫'], 0);
        spd_inc.sort((a, b) => b - a);
        zc_spd_val += spd_inc[0] + spd_inc[1];
        let zc_spd_label = `招财一速${zc_spd_val.toFixed(2)}`;
        itms.push(build_item(zc_spd_label));

        let highlight = document.getElementsByClassName('highlight')[0];
        for (let li of itms) {
            highlight.appendChild(li);
        }
    }

    function summaryPage() {
        let wrapper = document.createElement('div');
        wrapper.classList.add('module');
        if (!acct_info.hasOwnProperty('summary')) {
            wrapper.appendChild(document.createTextNode("数据加载出错,请尝试刷新页面"))
            return wrapper;
        }
        let decimal = 2;
        let {
            fastest,
            heads,
            feet,
            fullspd_cnt
        } = acct_info.summary;
        let fullspd_suit = Object.fromEntries(suit_imp.map(name => [name, 0]));
        fastest = JSON.parse(JSON.stringify(fastest)); // make a deep copy
        let suit_stats = {};
        for (let p of [1, 2, 3, 4, 5, 6]) {
            for (let name in fullspd_cnt[p]) {
                if (fullspd_suit[name] === 0) {
                    continue;
                }
                if (name in suit_stats) {
                    suit_stats[name].push(p);
                } else {
                    suit_stats[name] = [p];
                }
            }
        }
        for (let name in suit_stats) {
            if (suit_stats[name].length >= 4) {
                if (name in fullspd_suit) {
                    continue;
                } else {
                    fullspd_suit[name] = 0;
                }
            }
        }
        let fast_suit_speed = function(name) {
            let suit_fastest = Object.fromEntries([1, 2, 3, 4, 5, 6].map(p => [p, name in fastest[p] ? fastest[p][name] : 0]));
            let suit_spd_val = [1, 2, 3, 4, 5, 6].reduce((total, p) => total + suit_fastest[p], 0);
            let spd_inc = [1, 2, 3, 4, 5, 6].map(p => fastest[p]['散件'] - suit_fastest[p]);
            spd_inc.sort((a, b) => b - a);
            suit_spd_val += spd_inc[0] + spd_inc[1];
            return suit_spd_val;
        }
        Object.keys(fullspd_suit).forEach(name => {
            fullspd_suit[name] = fast_suit_speed(name);
        })

        let sortByValue = function(a, b) {
            return b.value - a.value
        }
        let headStr = heads.length > 0 ? heads.sort(sortByValue).map(itm => `<span class="data-value">${itm.name}: ${(itm.value).toFixed(decimal)}</span>`.trim()).join(", ") : "无";
        let feetStr = feet.length > 0 ? feet.sort(sortByValue).map(itm => `<span class="data-value">${itm.name}: ${(itm.value).toFixed(decimal)}</span>`.trim()).join(", ") : "无";
        let td_val = function(pos, name) {
            let fullspd = fullspd_cnt[pos][name] > 0;
            let spd = name in fastest[pos] ? fastest[pos][name].toFixed(decimal) : 0;
            let res = `<span${fullspd? "":" class=disabled"}>${spd}</span> `
            if (fullspd) {
                res += nowrapText(`(${fullspd_cnt[pos][name]})`)
            }
            return res;
        }
        Object.keys(fastest[2]).forEach(k => fastest[2][k] = fastest[2][k] - 57 > 0 ? fastest[2][k] - 57 : 0)
        let speed_summary = function(name) {
            return `<tr> <td>${name}</td> ${[1, 2, 3, 4, 5, 6, 7].map(i => `<td>${td_val(i, name)}</td>`)} </tr>`;
        }
        let fastest_tbl = `<table width="100%">
        <tr> <th>位置</th> ${[1, 2, 3, 4, 5, 6].map(i => `<th>${i}</th>`)} <th>4${nowrapText("(命中)")}</th> </tr>
        ${ Object.keys(fullspd_suit).map(name => speed_summary(name)).join(" ") }
    </table>`;
        let suit_table = `<table width="100%">
        <tr> <th>御魂名称</th> <th>套装一速</th></tr>
        ${ Object.keys(fullspd_suit).map(name => `<tr> <th>${name}</th> <td>${fullspd_suit[name].toFixed(5)}</td></tr>\n`).join("") }
    </table>`;

        let title = document.createElement('div')
        title.classList.add('title');
        title.innerText = "御魂亮点"
        let spd = document.createElement('section')
        spd.innerHTML = `<div><span class="data-name">头:</span> ${headStr} </div>
    <div><span class="data-name">脚:</span> ${feetStr} </div>`;
        let title2 = document.createElement('div');
        title2.innerText = "套装一速(非独立)";
        title2.classList.add('title');
        let suit = document.createElement('section');
        suit.innerHTML = suit_table;

        let title3 = document.createElement('div');
        title3.innerText = "各位置一速(满速个数)";
        title3.classList.add('title');

        let fastest_sec = document.createElement('section');
        fastest_sec.innerHTML = fastest_tbl;
        if (fastest_sec.firstChild.nodeType === Node.TEXT_NODE) {
            fastest_sec.firstChild.textContent = '';
        }

        wrapper.appendChild(title);
        wrapper.appendChild(spd);
        wrapper.appendChild(title2);
        wrapper.appendChild(suit);
        wrapper.appendChild(title3);
        wrapper.appendChild(fastest_sec);
        return wrapper;
    }

    function addHighlightView() {
        if (document.getElementById('cbghelper_highlight')) {
            return;
        }
        let div = document.createElement('div');
        div.id = 'cbghelper_highlight';
        div.class = 'module';
        div.appendChild(summaryPage());
        let wrapper = document.getElementsByClassName(panel_class_name)[0];
        wrapper.appendChild(div)
    }

    function addDownloadBtn() {
        if (document.getElementById('cbghelper_download')) {
            return;
        }
        let b = document.createElement('a');
        b.innerText = "(💾保存为JSON)";
        b.onclick = function() {
            console.log("To save data!");
            saveToJsonHelper();
        }
        b.id = "cbghelper_download"
        b.style.cursor = "pointer";
        let yuhun_list = document.getElementsByClassName('content-top-left')[0];
        yuhun_list.getElementsByTagName('h3')[1].appendChild(b);
    }

    function addDownloadBtnWrapper() {
        if (document.getElementsByClassName('yuhun-list').length) {
            addDownloadBtn();
        }
    }

    function addExtHighlightWrapper() {
        if (document.getElementsByClassName('highlight').length) {
            addExtendedHighlight();
        }
    }

    function addHighlightViewWrapper() {
        if (document.getElementsByClassName(panel_class_name).length && acct_info.ready) {
            addHighlightView();
        }
    }

    function init() {
        let checkfn_list = {
            'cbghelper_download': addDownloadBtnWrapper,
            'cbghelper_exthighlight': addExtHighlightWrapper,
            'cbghelper_highlight': addHighlightViewWrapper
        };
        let handlers = {};

        let checkExist = setInterval(function() {
            if (!document.URL.startsWith("https://yys.cbg.163.com/cgi/mweb/equip")) {
                return;
            }
            for (let eid of Object.keys(checkfn_list)) {
                if (document.getElementById(eid) && eid in handlers) {
                    clearInterval(handlers[eid]);
                    delete handlers[eid];
                } else if (document.getElementById(eid) || eid in handlers) {
                    continue;
                } else {
                    handlers[eid] = setInterval(checkfn_list[eid], 200);
                }
            }
        }, 100);
    };

    init();
    const floatify = function(data) {
        let equip = data['equip'];
        let acct_detail = JSON.parse(equip['equip_desc']);
        let mitama_list = acct_detail['inventory'];
        let hero_list = acct_detail['heroes'];
        let hero_info = acct_detail['hero_history'];

        try {
            var message = {
                name: equip.seller_name,
                roleid: equip.seller_roleid,
                ordersn: equip.game_ordersn,
                mitama_list
            };
            var event = new CustomEvent("SaveLastAccount", {
                detail: message
            });
            window.dispatchEvent(event);
            acct_info.latest = message;
        } catch (error) {}

        Object.entries(mitama_list).forEach(([key, value]) => {
            mitama_list[key] = floatify_mitama(value);
        });
        Object.entries(hero_list).forEach(([key, value]) => {
            hero_list[key] = floatify_hero(value, mitama_list);
        });
        acct_detail['inventory'] = mitama_list;
        equip['equip_desc'] = JSON.stringify(acct_detail);
        data['equip'] = equip;

        acctHighlight(mitama_list, hero_info);

        return data;
    }

    function getPropValue(mitama_set, mitama_list, propName) {
        let res = 0;
        for (let mitama_id of mitama_set) {
            var {
                attrs,
                single_attr = []
            } = mitama_list[mitama_id];
            for (let [p, v] of attrs) {
                if (p === propName) {
                    res += parseFloat(v);
                }
            }
            if (single_attr.length > 0 && single_attr[0] === propName) {
                res += parseFloat(single_attr[1]);
            }
        }
        return res
    }

    function floatify_hero(hero_data, mitama_list) {
        var {
            attrs,
            equips
        } = hero_data
        Object.keys(attrs).forEach(propName => {
            if (propName === '速度' && parseFloat(attrs[propName].add_val) > 0) {
                if (hero_data.heroId === 255 && hero_data.awake === 1) { //觉醒阎魔+10速度
                    attrs[propName].add_val = 10.0
                } else {
                    attrs[propName].add_val = 0.0
                }
                attrs[propName].add_val += getPropValue(equips, mitama_list, propName);
                attrs[propName].add_val = attrs[propName].add_val.toFixed(FRAC_N)
            }
            if (propName === '暴击' && parseFloat(attrs[propName].add_val) > 0) {
                let suit_cp = suit_by_props['暴击'];
                attrs[propName].add_val = getPropValue(equips, mitama_list, propName);
                let suit_names = equips.map(x => mitama_list[x].name);
                let suit_count = {};
                for (let n of suit_names) {
                    if (n in suit_count) {
                        suit_count[n] += 1;
                    } else {
                        suit_count[n] = 1;
                    }
                }
                Object.keys(suit_count).forEach(n => {
                    if (suit_count[n] >= 2 && suit_cp.includes(n)) {
                        attrs[propName].add_val += suit_count[n] === 6 ? 30 : 15;
                    }
                })
                attrs[propName].add_val = attrs[propName].add_val.toFixed(2) + "%";
            }
        })

        return hero_data;
    }

    function floatify_mitama(mitama) {
        var {
            rattr,
            attrs
        } = mitama;
        mitama["attrs"] = [attrs[0], ...calAttrs(rattr)];
        return mitama;
    }

    function calAttrs(rattrs, format = true) {
        var enAttrNames = ['attackAdditionRate',
            'attackAdditionVal',
            'critPowerAdditionVal',
            'critRateAdditionVal',
            'debuffEnhance',
            'debuffResist',
            'defenseAdditionRate',
            'defenseAdditionVal',
            'maxHpAdditionRate',
            'maxHpAdditionVal',
            'speedAdditionVal'
        ]

        var cnAttrNames = ['攻击加成', '攻击', '暴击伤害', '暴击',
            '效果命中', '效果抵抗', '防御加成',
            '防御', '生命加成', '生命', '速度'
        ]

        var basePropValue = {
            '攻击加成': 3,
            '攻击': 27,
            '暴击伤害': 4,
            '暴击': 3,
            '效果抵抗': 4,
            '效果命中': 4,
            '防御加成': 3,
            '防御': 5,
            '生命加成': 3,
            '生命': 114,
            '速度': 3
        }

        var percentProp = {
            '攻击加成': true,
            '攻击': false,
            '暴击伤害': true,
            '暴击': true,
            '效果抵抗': true,
            '效果命中': true,
            '防御加成': true,
            '防御': false,
            '生命加成': true,
            '生命': false,
            '速度': false
        }

        var e2cNameMap = Object.assign({}, ...enAttrNames.map((n, index) => ({
            [n]: cnAttrNames[index]
        })));
        var res = Object();
        for (let rattr of rattrs) {
            var [prop, v] = rattr;
            prop = e2cNameMap[prop];
            if (prop in res) {
                res[prop] += v;
            } else {
                res[prop] = v;
            }
        }

        return Object.keys(res).sort().map(p => {
            var v = res[p] * basePropValue[p]
            if (format) {
                v = v.toFixed(FRAC_N);
                if (percentProp[p]) {
                    v += "%";
                }
            }

            return [p, v];
        })
    }

    function soulToJson(soulItem) {
        const {
            attrs,
            level,
            qua,
            rattr,
            uuid,
            name,
            pos,
            single_attr = []
        } = soulItem;
        var born = parseInt(uuid.substring(0, 8), 16);
        let soulDict = {
            '固有属性': single_attr.length ? single_attr[0] : null,
            '生成时间': born,
            '御魂等级': level,
            '御魂星级': qua,
            '御魂ID': uuid,
            '御魂类型': name,
            '位置': pos
        };
        let PROPNAMES = ['攻击', '攻击加成', '防御',
            '防御加成', '暴击', '暴击伤害', '生命', '生命加成', '效果命中',
            '效果抵抗', '速度'
        ];
        PROPNAMES.map(function(e, i) {
            soulDict[e] = 0;
        });

        let percent = ['攻击加成', '防御加成', '暴击', '暴击伤害', '生命加成', '效果命中', '效果抵抗'];
        for (let [p, v] of [attrs[0], ...calAttrs(rattr, false)]) {
            v = parseFloat(v)
            if (percent.includes(p)) {
                v = v / 100;
            }
            soulDict[p] += v;
        }
        if (single_attr.length) {
            const [p, v] = single_attr;
            soulDict[p] += parseFloat(v) / 100;
        }

        return soulDict;
    }

    function saveToJson(soulLists) {
        var fileContent = 'data:text/json;charset=utf-8,'
        let soulListJson = Object.values(soulLists).map(soulToJson);
        soulListJson.unshift('yuhun_ocr2.0');
        fileContent += JSON.stringify(soulListJson);

        var encodedUri = encodeURI(fileContent);
        var link = document.createElement('a');
        link.setAttribute('href', encodedUri);
        link.setAttribute('download', 'yuhun.json');
        link.innerHTML = 'Click Here to download your data';
        document.body.appendChild(link); // Required for FF

        link.click();
        link.parentNode.removeChild(link);
    }

    function acctHighlight(mitama_list, hero_info) {
        let fastest = {};
        let fullspd_cnt = {}
        let heads = [];
        let feet = [];
        let all_pos = [1, 2, 3, 4, 5, 6];

        for (let p of [1, 2, 3, 4, 5, 6, 7]) { //7 for 命中@4
            fastest[p] = {};
            fullspd_cnt[p] = {};
            for (let name of suit_imp) {
                fastest[p][name] = 0;
                fullspd_cnt[p][name] = 0;
            }
        }

        Object.entries(mitama_list).forEach(([key, m]) => {
            let {
                attrs,
                pos,
                name,
                qua,
                rattr
            } = m;
            let spd = 0,
                spdpt = 0;
            for (let [p, v] of attrs) {
                if (p === '速度') {
                    spd += parseFloat(v);
                }
            }
            for (let rattr_entry of rattr) {
                var [prop, v] = rattr_entry;
                if (prop === 'speedAdditionVal') {
                    spdpt += 1
                }
            }
            if (spdpt < 1 || (pos === 2 && spd < 57)) {
                return;
            }
            if (spdpt === 6 && (pos !== 2 || spd > 70)) {
                fullspd_cnt[pos]['散件'] += 1
                if (name in fullspd_cnt[pos]) {
                    fullspd_cnt[pos][name] += 1;
                } else {
                    fullspd_cnt[pos][name] = 1;
                }
                if (pos === 2) {
                    heads.push({
                        pos,
                        name,
                        value: spd - 57
                    });
                } else if (pos === 4 && attrs[0][0] === '效果命中') {
                    fullspd_cnt[7]['散件'] += 1;
                    if (name in fullspd_cnt[pos]) {
                        fullspd_cnt[7][name] += 1;
                    } else {
                        fullspd_cnt[7][name] = 1;
                    }
                    feet.push({
                        pos,
                        name,
                        value: spd
                    });
                }
            }
            if (name in fastest[pos]) {
                fastest[pos][name] = fastest[pos][name] > spd ? fastest[pos][name] : spd;
            } else {
                fastest[pos][name] = spd;
            }
            fastest[pos]['散件'] = fastest[pos]['散件'] > spd ? fastest[pos]['散件'] : spd;
            if (pos === 4 && attrs[0][0] === '效果命中') {
                pos = 7;
                if (name in fastest[pos]) {
                    fastest[pos][name] = fastest[pos][name] > spd ? fastest[pos][name] : spd;
                } else {
                    fastest[pos][name] = spd;
                }
                fastest[pos]['散件'] = fastest[pos]['散件'] > spd ? fastest[pos]['散件'] : spd;
            }
        });
        acct_info.summary = {
            heads,
            feet,
            fastest,
            fullspd_cnt,
            hero_info
        }
        acct_info.ready = true;
    }

    function saveToJsonHelper() {
        // var event = new CustomEvent("LoadLastAccount", {});
        // window.dispatchEvent(event);
        // console.log("Account data requested!");
        saveToJson(acct_info.latest.mitama_list);
    }
    // function needed that is not included from chrome extension
    var cssRules = `
.cbghelper_nowrap { 
    white-space: nowrap; 
}
`

    function injectCSS() {
        var style = document.createElement('style');
        style.innerHTML = cssRules;
        document.getElementsByTagName('head')[0].appendChild(style);
    }

    injectCSS();
})()