咕咕镇数据采集

咕咕镇数据采集,目前采集已关闭,兼作助手

目前為 2022-07-01 提交的版本,檢視 最新版本

// ==UserScript==
// @name         咕咕镇数据采集
// @namespace    https://gf.qytechs.cn/users/448113
// @version      1.4.31
// @description  咕咕镇数据采集,目前采集已关闭,兼作助手
// @author       paraii
// @match        https://www.guguzhen.com/*
// @grant        GM_xmlhttpRequest
// @connect      www.guguzhen.com
// @run-at       document-body
// @license      MIT License
// ==/UserScript==
// @connect      notes.orga.cat
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/js/tooltip.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/js/popover.js
(function() {
    'use strict'

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // common utilities
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    const g_modificationVersion = '2022-07-01 18:00:00';

    const g_kfUser = document.querySelector('div.btn-toolbar > div.row > div.col-md-10 > div > span.fyg_colpz06.fyg_f24').innerText;
    const g_autoTaskEnabledStorageKey = g_kfUser + '_autoTaskEnabled';
    const g_indexRallyStorageKey = g_kfUser + '_indexRally';
    const g_keepPkRecordStorageKey = g_kfUser + '_keepPkRecord';
    const g_amuletGroupsStorageKey = g_kfUser + '_amulet_groups';
    const g_equipmentExpandStorageKey = g_kfUser + '_equipment_Expand';
    const g_equipmentBGStorageKey = g_kfUser + '_equipment_BG';
    const g_ignoreWishpoolExpirationStorageKey = g_kfUser + '_ignoreWishpoolExpiration';
    const g_beachIgnoreStoreMysEquipStorageKey = g_kfUser + '_beach_ignoreStoreMysEquip';
    const g_beachForceExpandStorageKey = g_kfUser + '_beach_forceExpand';
    const g_beachBGStorageKey = g_kfUser + '_beach_BG';
    const g_userDataStorageKeyConfig = [ g_kfUser, g_autoTaskEnabledStorageKey, g_indexRallyStorageKey, g_keepPkRecordStorageKey,
                                         g_amuletGroupsStorageKey, g_equipmentExpandStorageKey, g_equipmentBGStorageKey,
                                         g_ignoreWishpoolExpirationStorageKey, g_beachIgnoreStoreMysEquipStorageKey,
                                         g_beachForceExpandStorageKey, g_beachBGStorageKey ];
    const g_userDataStorageKeyExtra = [ 'attribute', 'cardName', 'title', 'over', 'halo_max', 'beachcheck', 'dataReward', 'keepcheck' ];

    const USER_STORAGE_RESERVED_SEPARATORS = /[:;,|=+*%!#$&?<>{}^`"\\\/\[\]\r\n\t\v\s]/;
    const USER_STORAGE_KEY_VALUE_SEPARATOR = ':';

    console.log(g_kfUser)

    // perform a binary search. array must be sorted, but no matter in ascending or descending order.
    // in this manner, you must pass in a proper comparer function for it works properly, aka, if the
    // array was sorted in ascending order, then the comparer(a, b) should return a negative value
    // while a < b or a positive value while a > b; otherwise, if the array was sorted in descending
    // order, then the comparer(a, b) should return a positive value while a < b or a negative value
    // while a > b, and in both, if a equals b, the comparer(a, b) should return 0. if you pass nothing
    // or null / undefined value as comparer, then you must make sure about that the array was sorted
    // in ascending order.
    //
    // in this particular case, we just want to check whether the array contains the value or not, we
    // don't even need to point out the first place where the value appears (if the array actually
    // contains the value), so we perform a simplest binary search and return an index (may not the
    // first place where the value appears) or a negative value (means value not found) to indicate
    // the search result.
    function searchElement(array, value, fnComparer) {
        if (array?.length > 0) {
            fnComparer ??= ((a, b) => a < b ? -1 : (a > b ? 1 : 0));
            let li = 0;
            let hi = array.length - 1;
            while (li <= hi) {
                let mi = ((li + hi) >> 1);
                let cr = fnComparer(value, array[mi]);
                if (cr == 0) {
                    return mi;
                }
                else if (cr > 0) {
                    li = mi + 1;
                }
                else {
                    hi = mi - 1;
                }
            }
        }
        return -1;
    }

    // perform a binary insertion. the array and comparer must exactly satisfy as it in the searchElement
    // function. this operation behaves sort-stable, aka, the newer inserting element will be inserted
    // into the position after any existed equivalent elements.
    function insertElement(array, value, fnComparer) {
        if (array != null) {
            fnComparer ??= ((a, b) => a < b ? -1 : (a > b ? 1 : 0));
            let li = 0;
            let hi = array.length - 1;
            while (li <= hi) {
                let mi = ((li + hi) >> 1);
                let cr = fnComparer(value, array[mi]);
                if (cr >= 0) {
                    li = mi + 1;
                }
                else {
                    hi = mi - 1;
                }
            }
            array.splice(li, 0, value);
            return li;
        }
        return -1;
    }

    // it's not necessary to have newArray been sorted, but the oldArray must be sorted since we are calling
    // searchElement. if there are some values should be ignored in newArray, the comparer(a, b) should be
    // implemented as return 0 whenever parameter a equals any of values that should be ignored.
    function findNewObjects(newArray, oldArray, fnComparer, findIndices) {
        let newObjects = [];
        for (let i = (newArray?.length ?? 0) - 1; i >= 0; i--) {
            if (searchElement(oldArray, newArray[i], fnComparer) < 0) {
                newObjects.unshift(findIndices ? i : newArray[i]);
            }
        }
        return newObjects;
    }

    function loadUserConfigData() {
        return JSON.parse(localStorage.getItem(g_kfUser));
    }

    function saveUserConfigData(json) {
        localStorage.setItem(g_kfUser, JSON.stringify(json));
    }

    function getPostData(functionPattern, dataPattern) {
        let sc = document.getElementsByTagName('script');
        for (let i = (sc?.length ?? 0) - 1; i >= 0 ; i--) {
            let func = sc[i].innerText.match(functionPattern);
            if (func != null) {
                return func[0].match(dataPattern)[0];
            }
        }
        return null;
    }

    // HTTP requests
    var g_httpRequests = [];
    function httpRequestRegister(request) {
        if (request != null) {
            g_httpRequests.push(request);
        }
    }

    function httpRequestAbortAll() {
        while (g_httpRequests.length > 0) {
            g_httpRequests.pop().abort();
        }
        g_httpRequests = [];
    }

    function httpRequestClearAll() {
        g_httpRequests = [];
    }

    // read objects from bag and store with title filter
    const g_postMethod = 'POST'
    const g_readUrl = 'https://www.guguzhen.com/fyg_read.php'
    const g_postUrl = 'https://www.guguzhen.com/fyg_click.php'
    const g_postHeader = { 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8' , 'Cookie' : document.cookie };
    const g_networkTimeoutMS = 120 * 1000;
    function beginReadObjects(bag, store, fnFurtherProcess, fnParams) {
        if (bag != null || store != null) {
            let request = GM_xmlhttpRequest({
                method: g_postMethod,
                url: g_readUrl,
                headers: g_postHeader,
                data: 'f=7',
                onload: response => {
                    let div = document.createElement('div');
                    div.innerHTML = response.responseText;

                    if (bag != null) {
                        bag.push(div.querySelectorAll('div.alert-danger > div.content > button.fyg_mp3'));
                    }
                    if (store != null) {
                        store.push(div.querySelectorAll('div.alert-success > div.content > button.fyg_mp3'));
                    }

                    if (fnFurtherProcess != null) {
                        fnFurtherProcess(fnParams);
                    }
                }
            });
            httpRequestRegister(request);
        }
        else if (fnFurtherProcess != null) {
            fnFurtherProcess(fnParams);
        }
    }

    function beginReadObjectIds(bagIds, storeIds, key, ignoreEmptyCell, fnFurtherProcess, fnParams) {
        function parseObjectIds() {
            if (bagIds != null) {
                objectIdParseNodes(bagIds.pop(), bagIds, key, ignoreEmptyCell);
            }
            if (storeIds != null) {
                objectIdParseNodes(storeIds.pop(), storeIds, key, ignoreEmptyCell);
            }

            if (fnFurtherProcess != null) {
                fnFurtherProcess(fnParams);
            }
        }

        if (bagIds != null || storeIds != null) {
            beginReadObjects(bagIds, storeIds, parseObjectIds, null);
        }
        else if (fnFurtherProcess != null) {
            fnFurtherProcess(fnParams);
        }
    }

    function objectIdParseNodes(nodes, ids, key, ignoreEmptyCell) {
        for (let node of nodes) {
            if (node.className?.endsWith('fyg_mp3')) {
                let id = node.getAttribute('onclick')?.match(/\d+/)[0];
                if (id != undefined) {
                    if (objectMatchTitle(node, key)) {
                        ids.push(parseInt(id));
                    }
                }
                else if (!ignoreEmptyCell) {
                    ids.push(-1);
                }
            }
        }
    }

    function objectMatchTitle(node, key){
        return (!(key?.length > 0) || (node.getAttribute('data-original-title') ??
                                       node.getAttribute('title'))?.indexOf(key) >= 0);
    }

    // we wait the response(s) of the previous batch of request(s) to send another batch of request(s)
    // rather than simply send them all within an inside foreach - which could cause too many requests
    // to server simultaneously, that can be easily treated as D.D.O.S attack and therefor leads server
    // to returns http status 503: Service Temporarily Unavailable
    // * caution * the parameter 'objects' is required been sorted by their indices in ascending order
    const g_ConcurrentRequestCount = { min : 1 , max : 8 , default : 4 };
    const g_object_move_path = { bag2store : 0 , store2bag : 1 , bag2beach : 2 , beach2bag : 3 };
    const g_object_move_data = [ null, null, null, null ];
    var g_maxConcurrentRequests = g_ConcurrentRequestCount.default;
    var g_objectMoveRequestsCount = 0;
    var g_objectMoveTargetSiteFull = false;
    function beginMoveObjects(objects, path, fnFurtherProcess, fnParams) {
        if (!g_objectMoveTargetSiteFull && objects?.length > 0) {
            g_object_move_data[g_object_move_path.bag2store] ??= (getPostData(/puti\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');
            g_object_move_data[g_object_move_path.store2bag] ??= (getPostData(/puto\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');
            g_object_move_data[g_object_move_path.bag2beach] ??= (getPostData(/stdel\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');
            g_object_move_data[g_object_move_path.beach2bag] ??= (getPostData(/stpick\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');

            if (!(g_object_move_data[path]?.length > 0)) {
                if (!(g_object_move_data[g_object_move_path.store2bag]?.length > 0) &&
                      g_object_move_data[g_object_move_path.bag2store]?.length > 0) {
                    g_object_move_data[g_object_move_path.store2bag] =
                        g_object_move_data[g_object_move_path.bag2store].replace('c=21&', 'c=22&');
                }
                if (!(g_object_move_data[g_object_move_path.bag2store]?.length > 0) &&
                      g_object_move_data[g_object_move_path.store2bag]?.length > 0) {
                    g_object_move_data[g_object_move_path.bag2store] =
                        g_object_move_data[g_object_move_path.store2bag].replace('c=22&', 'c=21&');
                }
                if (!(g_object_move_data[g_object_move_path.bag2beach]?.length > 0) &&
                      g_object_move_data[g_object_move_path.beach2bag]?.length > 0) {
                    g_object_move_data[g_object_move_path.bag2beach] =
                        g_object_move_data[g_object_move_path.beach2bag].replace('c=1&', 'c=7&');
                }
                if (!(g_object_move_data[g_object_move_path.beach2bag]?.length > 0) &&
                      g_object_move_data[g_object_move_path.bag2beach]?.length > 0) {
                    g_object_move_data[g_object_move_path.beach2bag] =
                        g_object_move_data[g_object_move_path.bag2beach].replace('c=7&', 'c=1&');
                }
            }

            if (g_object_move_data[path].length > 0) {
                let ids = [];
                while (ids.length < g_maxConcurrentRequests && objects.length > 0) {
                    let id = objects.pop();
                    if (id >= 0) {
                        ids.push(id);
                    }
                }
                if ((g_objectMoveRequestsCount = ids.length) > 0) {
                    while (ids.length > 0) {
                        let request = GM_xmlhttpRequest({
                            method: g_postMethod,
                            url: g_postUrl,
                            headers: g_postHeader,
                            data: g_object_move_data[path].replace('"+id+"', ids.shift()),
                            onload: response => {
                                if (response.responseText != 'ok') {
                                    g_objectMoveTargetSiteFull = true;
                                    console.log(response.responseText);
                                }
                                if (--g_objectMoveRequestsCount == 0) {
                                    beginMoveObjects(objects, path, fnFurtherProcess, fnParams);
                                }
                            }
                        });
                        httpRequestRegister(request);
                    }
                    return;
                }
            }
        }
        g_objectMoveTargetSiteFull = false;
        if (fnFurtherProcess != null) {
            fnFurtherProcess(fnParams);
        }
    }

    // read currently mounted role card and halo informations
    // roleInfo = [ roleId, roleName ]
    // haloInfo = [ haloPoints, haloSlots, [ haloItem1, haloItem2, ... ] ]
    function beginReadRoleAndHalo(roleInfo, haloInfo, fnFurtherProcess, fnParams) {
        let asyncOperations = 0;
        let error = 0;
        let requestRole;
        let requestHalo;

        if (roleInfo != null) {
            asyncOperations++;
            requestRole = GM_xmlhttpRequest({
                method: g_postMethod,
                url: g_readUrl,
                headers: g_postHeader,
                data: 'f=9',
                onload: response => {
                    let div = document.createElement('div');
                    div.innerHTML = response.responseText;
                    let role = g_roleMap.get(div.querySelector('div.text-info.fyg_f24.fyg_lh60')?.children[0]?.innerText);
                    if (role != undefined) {
                        roleInfo.push(role.id);
                        roleInfo.push(role.name);
                    }
                    asyncOperations--;
                },
                onerror : err => {
                    error++;
                    asyncOperations--;
                },
                ontimeout : err => {
                    error++;
                    asyncOperations--;
                }
            });
        }

        if (haloInfo != null) {
            asyncOperations++;
            requestHalo = GM_xmlhttpRequest({
                method: g_postMethod,
                url: g_readUrl,
                headers: g_postHeader,
                data: 'f=5',
                onload: response => {
                    let haloPS = response.responseText.match(/<h3>[^\d]*(\d+)[^\d]*(\d+)[^<]+<\/h3>/);
                    if (haloPS?.length == 3) {
                        haloInfo.push(parseInt(haloPS[1]));
                        haloInfo.push(parseInt(haloPS[2]));
                    }
                    else {
                        haloInfo.push(0);
                        haloInfo.push(0);
                    }

                    let halo = [];
                    for (let item of response.responseText.matchAll(/halotfzt2\((\d+)\)/g)) {
                        halo.push(item[1]);
                    }
                    haloInfo.push(halo);
                    asyncOperations--;
                },
                onerror : err => {
                    error++;
                    asyncOperations--;
                },
                ontimeout : err => {
                    error++;
                    asyncOperations--;
                }
            });
        }

        let timeout = 0;
        let timer = setInterval(() => {
            if (asyncOperations == 0 || error > 0 || (++timeout * 200) >= g_networkTimeoutMS) {
                clearInterval(timer);
                if (asyncOperations > 0) {
                    requestRole?.abort();
                    requestHalo?.abort();
                }
                if (fnFurtherProcess != null) {
                    fnFurtherProcess(fnParams);
                }
            }
        }, 200);
    }

    function beginReadWishpool(points, misc, fnFurtherProcess, fnParams) {
        GM_xmlhttpRequest({
            method: g_postMethod,
            url: g_readUrl,
            headers: g_postHeader,
            data: `f=19`,
            onload: response => {
                let a = response.responseText.split('#');
                if (misc != null) {
                    misc[0] = a[0];
                    misc[1] = a[1];
                    misc[2] = a[2];
                }
                if (points != null) {
                    for (let i = a.length - 1; i >= 3; i--) {
                        points[i - 3] = a[i];
                    }
                }
                if (fnFurtherProcess != null) {
                    fnFurtherProcess(fnParams);
                }
            }
        });
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // amulet management
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    const AMULET_STORAGE_GROUP_SEPARATOR = '|';
    const AMULET_STORAGE_GROUPNAME_SEPARATOR = '=';
    const AMULET_STORAGE_AMULET_SEPARATOR = ',';
    const AMULET_TYPE_ID_FACTOR = 100000000000000;
    const AMULET_LEVEL_ID_FACTOR = 10000000000000;
    const AMULET_ENHANCEMENT_FACTOR = 1000000000000;
    const AMULET_BUFF_MAX_FACTOR = AMULET_ENHANCEMENT_FACTOR;

    const g_amuletLevelIds = {
        稀有 : 0,
        史诗 : 1,
        传奇 : 2
    };

    const g_amuletTypeIds = {
        苹果 : 0,
        葡萄 : 1,
        樱桃 : 2
    };

    const g_amuletLevelNames = [ '稀有', '史诗', '传奇' ];
    const g_amuletTypeNames = [ '苹果', '葡萄', '樱桃' ];
    const g_amuletBuffs = [
        { index : -1 , name : '力量' , type : 0 , typeName : '苹果' , maxValue : 80 , unit : '点' , shortMark : 'STR' },
        { index : -1 , name : '敏捷' , type : 0 , typeName : '苹果' , maxValue : 80 , unit : '点' , shortMark : 'AGI' },
        { index : -1 , name : '智力' , type : 0 , typeName : '苹果' , maxValue : 80 , unit : '点' , shortMark : 'INT' },
        { index : -1 , name : '体魄' , type : 0 , typeName : '苹果' , maxValue : 80 , unit : '点' , shortMark : 'VIT' },
        { index : -1 , name : '精神' , type : 0 , typeName : '苹果' , maxValue : 80 , unit : '点' , shortMark : 'SPR' },
        { index : -1 , name : '意志' , type : 0 , typeName : '苹果' , maxValue : 80 , unit : '点' , shortMark : 'MND' },
        { index : -1 , name : '物理攻击' , type : 1 , typeName : '葡萄' , maxValue : 10 , unit : '%' , shortMark : 'PATK' },
        { index : -1 , name : '魔法攻击' , type : 1 , typeName : '葡萄' , maxValue : 10 , unit : '%' , shortMark : 'MATK' },
        { index : -1 , name : '速度' , type : 1 , typeName : '葡萄' , maxValue : 10 , unit : '%' , shortMark : 'SPD' },
        { index : -1 , name : '生命护盾回复效果' , type : 1 , typeName : '葡萄' , maxValue : 10 , unit : '%' , shortMark : 'REC' },
        { index : -1 , name : '最大生命值' , type : 1 , typeName : '葡萄' , maxValue : 10 , unit : '%' , shortMark : 'HP' },
        { index : -1 , name : '最大护盾值' , type : 1 , typeName : '葡萄' , maxValue : 10 , unit : '%' , shortMark : 'SLD' },
        { index : -1 , name : '固定生命偷取' , type : 2 , typeName : '樱桃' , maxValue : 10 , unit : '%' , shortMark : 'LCH' },
        { index : -1 , name : '固定反伤' , type : 2 , typeName : '樱桃' , maxValue : 10 , unit : '%' , shortMark : 'RFL' },
        { index : -1 , name : '固定暴击几率' , type : 2 , typeName : '樱桃' , maxValue : 10 , unit : '%' , shortMark : 'CRT' },
        { index : -1 , name : '固定技能几率' , type : 2 , typeName : '樱桃' , maxValue : 10 , unit : '%' , shortMark : 'SKL' },
        { index : -1 , name : '物理防御效果' , type : 2 , typeName : '樱桃' , maxValue : 10 , unit : '%' , shortMark : 'PDEF' },
        { index : -1 , name : '魔法防御效果' , type : 2 , typeName : '樱桃' , maxValue : 10 , unit : '%' , shortMark : 'MDEF' } ];

    const g_amuletBuffMap = new Map();
    g_amuletBuffs.forEach((item, index) => {
        item.index = index;
        g_amuletBuffMap.set(item.index, item);
        g_amuletBuffMap.set(item.name, item);
        g_amuletBuffMap.set(item.shortMark, item);
    });

    function Amulet() {
        this.id = -1;
        this.type = -1;
        this.level = 0;
        this.enhancement = 0;
        this.buffCode = 0;
        this.text = null;

        this.reset = (() => {
            this.id = -1;
            this.type = -1;
            this.level = 0;
            this.enhancement = 0;
            this.buffCode = 0;
            this.text = null;
        });

        this.isValid = (() => {
            return (this.type >= 0 && this.type <= 2 && this.buffCode > 0);
        });

        this.fromCode = ((code) => {
            if (!isNaN(code)) {
                this.type = Math.trunc(code / AMULET_TYPE_ID_FACTOR) % 10;
                this.level = Math.trunc(code / AMULET_LEVEL_ID_FACTOR) % 10;
                this.enhancement = Math.trunc(code / AMULET_ENHANCEMENT_FACTOR) % 10;
                this.buffCode = code % AMULET_BUFF_MAX_FACTOR;
            }
            else {
                this.reset();
            }
            return (this.isValid() ? this : null);
        });

        this.fromBuffText = ((text) => {
            if (text?.length > 0) {
                let nb = text.split(' = ');
                if (nb.length == 2) {
                    this.id = -1;
                    this.type = g_amuletTypeIds[nb[0].slice(2, 4)];
                    this.level = g_amuletLevelIds[nb[0].slice(0, 2)];
                    this.enhancement = parseInt(nb[0].match(/\d+/)[0]);
                    this.buffCode = 0;
                    nb[1].replaceAll(/(\+)|( 点)|( %)/g, '').split(',').forEach((buff) => {
                        let nv = buff.trim().split(' ');
                        this.buffCode += ((100 ** (g_amuletBuffMap.get(nv[0]).index % 6)) * parseInt(nv[1]));
                    });
                    if (this.isValid()) {
                        this.text = nb[1];
                        return this;
                    }
                }
            }
            this.reset();
            return null;
        });

        this.fromNode = ((node) => {
            if (node?.className?.endsWith('fyg_mp3') && node.innerText.indexOf('+') >= 0) {
                let id = node.getAttribute('onclick');
                let typeName = (node.getAttribute('data-original-title') ?? node.getAttribute('title'));
                let content = node.getAttribute('data-content');
                if (id != null && typeName?.length > 4 && content?.length > 0 &&
                    !isNaN(this.type = g_amuletTypeIds[typeName.slice(2, 4)]) &&
                    !isNaN(this.level = g_amuletLevelIds[typeName.slice(0, 2)]) &&
                    !isNaN(this.id = parseInt(id.match(/\d+/)[0])) &&
                    !isNaN(this.enhancement = parseInt(node.innerText.match(/\d+/)[0]))) {

                    this.buffCode = 0;
                    this.text = '';
                    let attr = null;
                    let regex = /<p[^>]*>([^<]+)<[^>]*>\+(\d+)[^<]*<\/span><\/p>/g;
                    while ((attr = regex.exec(content))?.length == 3) {
                        let buffMeta = g_amuletBuffMap.get(attr[1]);
                        this.buffCode += ((100 ** (buffMeta.index % 6)) * attr[2]);
                        this.text += `${this.text.length > 0 ? ', ' : ''}${attr[1]} +${attr[2]} ${buffMeta.unit}`;
                    }
                    if (this.isValid()) {
                        return this;
                    }
                }
            }
            this.reset();
            return null;
        });

        this.fromAmulet = ((amulet) => {
            if (amulet?.isValid()) {
                this.id = amulet.id;
                this.type = amulet.type;
                this.level = amulet.level;
                this.enhancement = amulet.enhancement;
                this.buffCode = amulet.buffCode;
                this.text = amulet.text;
            }
            else {
                this.reset();
            }
            return (this.isValid() ? this : null);
        });

        this.getCode = (() => {
            if (this.isValid()) {
                return (this.type * AMULET_TYPE_ID_FACTOR +
                        this.level * AMULET_LEVEL_ID_FACTOR +
                        this.enhancement * AMULET_ENHANCEMENT_FACTOR +
                        this.buffCode);
            }
            return -1;
        });

        this.getBuff = (() => {
            let buffs = {};
            if (this.isValid()) {
                let code = this.buffCode;
                let type = this.type * 6;
                g_amuletBuffs.slice(type, type + 6).forEach((buff) => {
                    let v = (code % 100);
                    if (v > 0) {
                        buffs[buff.name] = v;
                    }
                    code = Math.trunc(code / 100);
                });
            }
            return buffs;
        });

        this.getTotalPoints = (() => {
            let points = 0;
            if (this.isValid()) {
                let code = this.buffCode;
                for(let i = 0; i < 6; i++) {
                    points += (code % 100);
                    code = Math.trunc(code / 100);
                }
            }
            return points;
        });

        this.formatName = (() => {
            if (this.isValid()) {
                return `${g_amuletLevelNames[this.level]}${g_amuletTypeNames[this.type]} (+${this.enhancement})`;
            }
            return null;
        });

        this.formatBuff = (() => {
            if (this.isValid()) {
                if (this.text?.length > 0) {
                    return this.text;
                }
                this.text = '';
                let buffs = this.getBuff();
                for (let buff in buffs) {
                    this.text += `${this.text.length > 0 ? ', ' : ''}${buff} +${buffs[buff]} ${g_amuletBuffMap.get(buff).unit}`;
                }
            }
            return this.text;
        });

        this.formatBuffText = (() => {
            if (this.isValid()) {
                return this.formatName() + ' = ' + this.formatBuff();
            }
            return null;
        });

        this.formatShortMark = (() => {
            let text = this.formatBuff()?.replaceAll(/(\+)|( 点)|( %)/g, '');
            if (text?.length > 0) {
                for (let buff in this.getBuff()) {
                    text = text.replaceAll(buff, g_amuletBuffMap.get(buff).shortMark);
                }
                return this.formatName() + ' = ' + text;
            }
            return null;
        });

        this.compareMatch = ((other, ascType) => {
            if (!this.isValid()) {
                return 1;
            }
            else if (!other?.isValid()) {
                return -1;
            }
            else if (this.id >= 0 && this.id == other.id) {
                return 0;
            }

            let delta = other.type - this.type;
            if (delta != 0) {
                return (ascType ? -delta : delta);
            }
            return (other.buffCode - this.buffCode);
        });

        this.compareTo = ((other, ascType) => {
            if (!this.isValid()) {
                return 1;
            }
            else if (!other?.isValid()) {
                return -1;
            }
            else if (this.id >= 0 && this.id == other.id) {
                return 0;
            }

            let delta = other.type - this.type;
            if (delta != 0) {
                return (ascType ? -delta : delta);
            }

            let tbuffs = this.formatBuffText().split(' = ')[1].replaceAll(/(\+)|( 点)|( %)/g, '').split(', ');
            let obuffs = other.formatBuffText().split(' = ')[1].replaceAll(/(\+)|( 点)|( %)/g, '').split(', ');
            let bl = Math.min(tbuffs.length, obuffs.length);
            for (let i = 0; i < bl; i++) {
                let tbuff = tbuffs[i].split(' ');
                let obuff = obuffs[i].split(' ');
                if ((delta = g_amuletBuffMap.get(tbuff[0]).index - g_amuletBuffMap.get(obuff[0]).index) != 0 ||
                    (delta = parseInt(obuff[1]) - parseInt(tbuff[1])) != 0) {
                    return delta;
                }
            }
            if ((delta = obuffs.length - tbuffs.length) != 0 ||
                (delta = other.level - this.level) != 0 ||
                (delta = other.enhancement - this.enhancement) != 0) {
                return delta;
            }

            return 0;
        });
    }

    function AmuletGroup(persistenceString) {
        this.buffSummary = {
            力量 : 0,
            敏捷 : 0,
            智力 : 0,
            体魄 : 0,
            精神 : 0,
            意志 : 0,
            物理攻击 : 0,
            魔法攻击 : 0,
            速度 : 0,
            生命护盾回复效果 : 0,
            最大生命值 : 0,
            最大护盾值 : 0,
            固定生命偷取 : 0,
            固定反伤 : 0,
            固定暴击几率 : 0,
            固定技能几率 : 0,
            物理防御效果 : 0,
            魔法防御效果 : 0
        };

        this.name = null;
        this.items = [];

        this.isValid = (() => {
            return (this.items.length > 0 && amuletIsValidGroupName(this.name));
        });

        this.count = (() => {
            return this.items.length;
        });

        this.clear = (() => {
            this.items = [];
            for (let buff in this.buffSummary) {
                this.buffSummary[buff] = 0;
            }
        });

        this.add = ((amulet) => {
            if (amulet?.isValid()) {
                let buffs = amulet.getBuff();
                for (let buff in buffs) {
                    this.buffSummary[buff] += buffs[buff];
                }
                return insertElement(this.items, amulet, (a, b) => a.compareTo(b, true));
            }
            return -1;
        });

        this.remove = ((amulet) => {
            if (this.isValid() && amulet?.isValid()) {
                let i = searchElement(this.items, amulet, (a, b) => a.compareTo(b, true));
                if (i >= 0) {
                    let buffs = amulet.getBuff();
                    for (let buff in buffs) {
                        this.buffSummary[buff] -= buffs[buff];
                    }
                    this.items.splice(i, 1);
                    return true;
                }
            }
            return false;
        });

        this.removeId = ((id) => {
            if (this.isValid()) {
                let i = this.items.findIndex((a) => a.id == id);
                if (i >= 0) {
                    let amulet = this.items[i];
                    let buffs = amulet.getBuff();
                    for (let buff in buffs) {
                        this.buffSummary[buff] -= buffs[buff];
                    }
                    this.items.splice(i, 1);
                    return amulet;
                }
            }
            return null;
        });

        this.merge = ((group) => {
            group?.items?.forEach((am) => { this.add(am); });
            return this;
        });

        this.validate = ((amulets) => {
            if (this.isValid()) {
                let mismatch = 0;
                let al = this.items.length;
                let i = 0;
                if (amulets?.length > 0) {
                    amulets = amulets.slice().sort((a, b) => a.compareMatch(b));
                    for ( ; amulets.length > 0 && i < al; i++) {
                        let mi = searchElement(amulets, this.items[i], (a, b) => a.compareMatch(b));
                        if (mi >= 0) {
                            // remove a matched amulet from the amulet pool can avoid one single amulet matches all
                            // the equivalent objects in the group.
                            // let's say two (or even more) AGI +5 apples in one group is fairly normal, if we just
                            // have only one equivalent apple in the amulet pool and we don't remove it when the
                            // first match happens, then the 2nd apple will get matched later, the consequence would
                            // be we can never find the mismatch which should be encountered at the 2nd apple
                            this.items[i].fromAmulet(amulets[mi]);
                            amulets.splice(mi, 1);
                        }
                        else {
                            mismatch++;
                        }
                    }
                }
                if (i > mismatch) {
                    this.items.sort((a, b) => a.compareTo(b, true));
                }
                if (i < al) {
                    mismatch += (al - i);
                }
                return (mismatch == 0);
            }
            return false;
        });

        this.findIndices = ((amulets) => {
            let indices;
            let al;
            if (this.isValid() && (al = (amulets?.length ?? 0)) > 0) {
                let items = this.items.slice().sort((a, b) => a.compareMatch(b));
                for (let i = 0; items.length > 0 && i < al; i++) {
                    let mi;
                    if (amulets[i]?.id >= 0 && (mi = searchElement(items, amulets[i], (a, b) => a.compareMatch(b))) >= 0) {
                        // similar to the 'validate', remove the amulet from the search list when we found
                        // a match item in first time to avoid the duplicate founding, e.g. say we need only
                        // one AGI +5 apple in current group and we actually have 10 of AGI +5 apples in store,
                        // if we found the first matched itme in store and record it's index but not remove it
                        // from the temporary searching list, then we will continuously reach this kind of
                        // founding and recording until all those 10 AGI +5 apples are matched and processed,
                        // this obviously ain't the result what we expected
                        (indices ??= []).push(i);
                        items.splice(mi, 1);
                    }
                }
            }
            return indices;
        });

        this.parse = ((persistenceString) => {
            this.clear();
            if (persistenceString?.length > 0) {
                let elements = persistenceString.split(AMULET_STORAGE_GROUPNAME_SEPARATOR);
                if (elements.length == 2) {
                    let name = elements[0].trim();
                    if (amuletIsValidGroupName(name)) {
                        let items = elements[1].split(AMULET_STORAGE_AMULET_SEPARATOR);
                        let il = items.length;
                        for (let i = 0; i < il; i++) {
                            if (this.add((new Amulet()).fromCode(parseInt(items[i]))) < 0) {
                                this.clear();
                                break;
                            }
                        }
                        if (this.count() > 0) {
                            this.name = name;
                        }
                    }
                }
            }
            return (this.count() > 0);
        });

        this.formatBuffSummary = ((linePrefix, lineSuffix, lineSeparator, ignoreMaxValue) => {
            if (this.isValid()) {
                let str = '';
                let nl = '';
                g_amuletBuffs.forEach((buff) => {
                    let v = this.buffSummary[buff.name];
                    if (v > 0) {
                        str += `${nl}${linePrefix}${buff.name} +${ignoreMaxValue ? v : Math.min(v, buff.maxValue)} ${buff.unit}${lineSuffix}`;
                        nl = lineSeparator;
                    }
                });
                return str;
            }
            return '';
        });

        this.formatBuffShortMark = ((keyValueSeparator, itemSeparator, ignoreMaxValue) => {
            if (this.isValid()) {
                let str = '';
                let sp = '';
                g_amuletBuffs.forEach((buff) => {
                    let v = this.buffSummary[buff.name];
                    if (v > 0) {
                        str += `${sp}${buff.shortMark}${keyValueSeparator}${ignoreMaxValue ? v : Math.min(v, buff.maxValue)}`;
                        sp = itemSeparator;
                    }
                });
                return str;
            }
            return '';
        });

        this.formatItems = ((linePrefix, erroeLinePrefix, lineSuffix, errorLineSuffix, lineSeparator) => {
            if (this.isValid()) {
                let str = '';
                let nl = '';
                this.items.forEach((amulet) => {
                    str += `${nl}${amulet.id < 0 ? erroeLinePrefix : linePrefix}${amulet.formatBuffText()}` +
                           `${amulet.id < 0 ? errorLineSuffix : lineSuffix}`;
                    nl = lineSeparator;
                });
                return str;
            }
            return '';
        });

        this.getDisplayStringLineCount = (() => {
            if (this.isValid()) {
                let lines = 0;
                g_amuletBuffs.forEach((buff) => {
                    if (this.buffSummary[buff.name] > 0) {
                        lines++;
                    }
                });
                return lines + this.items.length;
            }
            return 0;
        });

        this.formatPersistenceString = (() => {
            if (this.isValid()) {
                let codes = [];
                this.items.forEach((amulet) => {
                    codes.push(amulet.getCode());
                });
                return `${this.name}${AMULET_STORAGE_GROUPNAME_SEPARATOR}${codes.join(AMULET_STORAGE_AMULET_SEPARATOR)}`;
            }
            return '';
        });

        this.parse(persistenceString);
    }

    function AmuletGroupCollection(persistenceString) {
        this.items = {};
        this.itemCount = 0;

        this.count = (() => {
            return this.itemCount;
        });

        this.contains = ((name) => {
            return (this.items[name] != undefined);
        });

        this.add = ((item) => {
            if (item?.isValid()) {
                if (!this.contains(item.name)) {
                    this.itemCount++;
                }
                this.items[item.name] = item;
                return true;
            }
            return false;
        });

        this.remove = ((name) => {
            if (this.contains(name)) {
                delete this.items[name];
                this.itemCount--;
                return true;
            }
            return false;
        });

        this.clear = (() => {
            for (let name in this.items) {
                delete this.items[name];
            }
            this.itemCount = 0;
        });

        this.get = ((name) => {
            return this.items[name];
        });

        this.rename = ((oldName, newName) => {
            if (amuletIsValidGroupName(newName)) {
                let group = this.items[oldName];
                if (this.remove(oldName)) {
                    group.name = newName;
                    return this.add(group);
                }
            }
            return false;
        });

        this.toArray = (() => {
            let groups = [];
            for (let name in this.items) {
                groups.push(this.items[name]);
            }
            return groups;
        });

        this.parse = ((persistenceString) => {
            this.clear();
            if (persistenceString?.length > 0) {
                let groupStrings = persistenceString.split(AMULET_STORAGE_GROUP_SEPARATOR);
                let gl = groupStrings.length;
                for (let i = 0; i < gl; i++) {
                    if (!this.add(new AmuletGroup(groupStrings[i]))) {
                        this.clear();
                        break;
                    }
                }
            }
            return (this.count() > 0);
        });

        this.formatPersistenceString = (() => {
            let str = '';
            let ns = '';
            for (let name in this.items) {
                str += (ns + this.items[name].formatPersistenceString());
                ns = AMULET_STORAGE_GROUP_SEPARATOR;
            }
            return str;
        });

        this.parse(persistenceString);
    }

    function amuletIsValidGroupName(groupName) {
        return (groupName?.length > 0 && groupName.length < 32 && groupName.search(USER_STORAGE_RESERVED_SEPARATORS) < 0);
    }

    function amuletSaveGroups(groups) {
        if (groups?.count() > 0) {
            localStorage.setItem(g_amuletGroupsStorageKey, groups.formatPersistenceString());
        }
        else {
            localStorage.removeItem(g_amuletGroupsStorageKey);
        }
    }

    function amuletLoadGroups() {
        return new AmuletGroupCollection(localStorage.getItem(g_amuletGroupsStorageKey));
    }

    function amuletClearGroups() {
        localStorage.removeItem(g_amuletGroupsStorageKey);
    }

    function amuletSaveGroup(group) {
        if (group?.isValid()) {
            let groups = amuletLoadGroups();
            if (groups.add(group)) {
                amuletSaveGroups(groups);
            }
        }
    }

    function amuletLoadGroup(groupName) {
        return amuletLoadGroups().get(groupName);
    }

    function amuletDeleteGroup(groupName) {
        let groups = amuletLoadGroups();
        if (groups.remove(groupName)) {
            amuletSaveGroups(groups);
        }
    }

    function amuletCreateGroupFromArray(groupName, amulets) {
        if (amulets?.length > 0 && amuletIsValidGroupName(groupName)) {
            let group = new AmuletGroup(null);
            for (let amulet of amulets) {
                if (group.add(amulet) < 0) {
                    group.clear();
                    break;
                }
            }
            if (group.count() > 0) {
                group.name = groupName;
                return group;
            }
        }
        return null;
    }

    function amuletNodesToArray(nodes, array, key) {
        array ??= [];
        let amulet;
        for (let node of nodes) {
            if (objectMatchTitle(node, key) && (amulet ??= new Amulet()).fromNode(node)?.isValid()) {
                array.push(amulet);
                amulet = null;
            }
        }
        return array;
    }

    function beginReadAmulets(bagAmulets, storeAmulets, key, fnFurtherProcess, fnParams) {
        function parseAmulets() {
            if (bagAmulets != null) {
                amuletNodesToArray(bagAmulets.pop(), bagAmulets, key);
            }
            if (storeAmulets != null) {
                amuletNodesToArray(storeAmulets.pop(), storeAmulets, key);
            }

            if (fnFurtherProcess != null) {
                fnFurtherProcess(fnParams);
            }
        }

        if (bagAmulets != null || storeAmulets != null) {
            beginReadObjects(bagAmulets, storeAmulets, parseAmulets, null);
        }
        else if (fnFurtherProcess != null) {
            fnFurtherProcess(fnParams);
        }
    }

    function beginMoveAmulets({ groupName, amulets, path, proc, params }) {
        let indices = amuletLoadGroup(groupName)?.findIndices(amulets)?.sort((a, b) => b - a);
        let ids;
        while (indices?.length > 0) {
            (ids ??= []).push(amulets[indices.pop()].id);
        }
        beginMoveObjects(ids, path, proc, params);
    }

    function beginLoadAmuletGroupFromStore(amulets, groupName, fnFurtherProcess, fnParams) {
        if (amulets?.length > 0) {
            let store = amuletNodesToArray(amulets);
            beginMoveAmulets({ groupName : groupName, amulets : store, path : g_object_move_path.store2bag,
                               proc : fnFurtherProcess, params : fnParams });
        }
        else {
            beginReadAmulets(null, amulets = [], null, beginMoveAmulets,
                             { groupName : groupName, amulets : amulets, path : g_object_move_path.store2bag,
                               proc : fnFurtherProcess, params : fnParams });
        }
    }

    function beginUnloadAmuletGroupFromBag(amulets, groupName, fnFurtherProcess, fnParams) {
        if (amulets?.length > 0) {
            let bag = amuletNodesToArray(amulets);
            beginMoveAmulets({ groupName : groupName, amulets : bag, path : g_object_move_path.bag2store,
                               proc : fnFurtherProcess, params : fnParams });
        }
        else {
            beginReadAmulets(amulets, null, null, beginMoveAmulets,
                             { groupName : groupName, amulets : amulets, path : g_object_move_path.bag2store,
                               proc : fnFurtherProcess, params : fnParams });
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // equipment utilities
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    function equipmentInfoParseNode(node) {
        if (node?.className?.split(' ').length > 2 && node.className.endsWith('fyg_mp3') && node.innerText.indexOf('+') < 0) {
            let title = (node.getAttribute('data-original-title') ?? node.getAttribute('title'));
            if (title?.length > 0) {
                let name = title?.substring(title.lastIndexOf('>') + 1).trim();
                name = (g_equipMap.get(name)?.shortMark ?? g_equipMap.get(name.substring(2))?.shortMark);
                if (name?.length > 0) {
                    let attr = node.getAttribute('data-content')?.match(/>\s*\d+\s?%\s*</g);
                    let lv = title.match(/>(\d+)</);
                    if (attr?.length > 0 && lv?.length > 0) {
                        let mys = (node.getAttribute('data-content')?.match(/\[神秘属性\]/) == null ? 0 : 1);
                        let id = node.getAttribute('onclick')?.match(/\d+/)[0];
                        return [ name, lv[1],
                                 attr[0].match(/\d+/)[0], attr[1].match(/\d+/)[0],
                                 attr[2].match(/\d+/)[0], attr[3].match(/\d+/)[0],
                                 mys, id ];
                    }
                }
            }
        }
        return null;
    }

    function equipmentNodesToInfoArray(nodes, array) {
        array ??= [];
        for (let i = (nodes?.length ?? 0) - 1; i >= 0; i--) {
            let e = equipmentInfoParseNode(nodes[i]);
            if (e != null) {
                array.unshift(e);
            }
        }
        return array;
    }

    const g_equipmentLevelPoints = [ 200, 321, 419, 516, 585 ];
    const g_equipmentLevelName = [ '普通', '幸运', '稀有', '史诗', '传奇' ];
    const g_equipmentLevelBGColor = [ '#f0f8f8', '#c0e0ff', '#c0ffc0', '#ffffc0', '#ffd0d0' ];
    const g_equipmentLevelTipClass = [ 'popover-primary', 'popover-info', 'popover-success', 'popover-warning', 'popover-danger' ];
    function equipmentGetLevel(e) {
        let eq = (Array.isArray(e) ? e : equipmentInfoParseNode(e));
        if (eq != null) {
            let p = parseInt(eq[2]) + parseInt(eq[3]) + parseInt(eq[4]) + parseInt(eq[5]) + (parseInt(eq[6]) * 100);
            for (var i = g_equipmentLevelPoints.length - 1; i > 0 && p < g_equipmentLevelPoints[i]; i--);
            return i;
        }
        else if ((eq = (new Amulet()).fromNode(e))?.isValid()) {
            return (eq.level + 2)
        }
        return -1;
    }

    function equipmentInfoComparer(e1, e2) {
        let delta = g_equipMap.get(e1[0]).index - g_equipMap.get(e2[0]).index;
        for (let i = 1; i < 7 && delta == 0; delta = parseInt(e1[i]) - parseInt(e2[i++]));
        return delta;
    }

    function objectNodeComparer(e1, e2) {
        let eq1 = equipmentInfoParseNode(e1);
        if (eq1 != null) {
            e1.setAttribute('data-meta-index', g_equipMap.get(eq1[0]).index);
        }

        let eq2 = equipmentInfoParseNode(e2);
        if (eq2 != null) {
            e2.setAttribute('data-meta-index', g_equipMap.get(eq2[0]).index);
        }

        if (eq1 == null && eq2 == null) {
            return ((new Amulet()).fromNode(e1)?.compareTo((new Amulet()).fromNode(e2)) ?? 1);
        }
        else if (eq1 == null) {
            return 1;
        }
        else if (eq2 == null) {
            return -1;
        }
        return equipmentInfoComparer(eq1, eq2);
    }

    function objectIsEmptyNode(node) {
        return (node?.innerText == '空');
    }

    function objectEmptyNodesCount(nodes) {
        let nl = (nodes?.length ?? 0);
        for (var i = nl - 1; i >= 0 && nodes[i].innerText == '空'; i--);
        return (nl - 1 - i);
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // bag & store utilities
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    function beginClearBag(bag, key, fnFurtherProcess, fnParams) {
        function beginClearBagObjects(objects) {
            beginMoveObjects(objects, g_object_move_path.bag2store, fnFurtherProcess, fnParams);
        }

        let objects = [];
        if (bag?.length > 0) {
            objectIdParseNodes(bag, objects, key, true);
            beginClearBagObjects(objects);
        }
        else {
            beginReadObjectIds(objects, null, key, true, beginClearBagObjects, objects);
        }
    }

    function beginRestoreObjects(store, amulets, equips, fnFurtherProcess, fnParams) {
        function readStoreCompletion() {
            beginRestoreObjects(store.pop(), amulets, equips, fnFurtherProcess, fnParams);
        }

        if (store == null) {
            beginReadObjects(null, store = [], readStoreCompletion, null);
        }
        else {
            let ids = [];
            if (amulets?.length > 0) {
                let ams = amuletNodesToArray(store);
                for (let i = ams.length - 1; i >= 0 && amulets.length > 0; i--) {
                    for (let j = amulets.length - 1; j >= 0; j--) {
                        if (ams[i].compareTo(amulets[j]) == 0) {
                            amulets.splice(j, 1);
                            ids.unshift(ams[i].id);
                            break;
                        }
                    }
                }
            }
            if (equips?.length > 0) {
                let eqs = equipmentNodesToInfoArray(store);
                for (let i = eqs.length - 1; i >= 0 && equips.length > 0; i--) {
                    for (let j = equips.length - 1; j >= 0; j--) {
                        if (equipmentInfoComparer(eqs[i], equips[j]) == 0) {
                            equips.splice(j, 1);
                            ids.unshift(parseInt(eqs[i][7]));
                            break;
                        }
                    }
                }
            }

            beginMoveObjects(ids, g_object_move_path.store2bag, fnFurtherProcess, fnParams);
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // generic popups
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    const g_genericPopupContainerId = 'generic-popup-container';
    const g_genericPopupClass = 'generic-popup';
    const g_genericPopupId = g_genericPopupClass;
    const g_genericPopupContentContainerId = 'generic-popup-content-container';
    const g_genericPopupContentClass = 'generic-popup-content';
    const g_genericPopupContentId = g_genericPopupContentClass;
    const g_genericPopupFixedContentId = 'generic-popup-content-fixed';
    const g_genericPopupInformationTipsId = 'generic-popup-information-tips';
    const g_genericPopupProgressClass = g_genericPopupClass;
    const g_genericPopupProgressId = 'generic-popup-progress';
    const g_genericPopupProgressContentClass = 'generic-popup-content-progress';
    const g_genericPopupProgressContentId = g_genericPopupProgressContentClass;
    const g_genericPopupTopLineDivClass = 'generic-popup-top-line-container';
    const g_genericPopupTitleTextClass = 'generic-popup-title-text';
    const g_genericPopupTitleTextId = g_genericPopupTitleTextClass;
    const g_genericPopupTitleButtonContainerId = 'generic-popup-title-button-container';
    const g_genericPopupFootButtonContainerId = 'generic-popup-foot-button-container';
    const g_genericPopupBackgroundColor = '#ebf2f9';
    const g_genericPopupBackgroundColorAlt = '#dbe2e9';
    const g_genericPopupBorderColor = '#3280fc';
    const g_genericPopupTitleTextColor = '#ffffff';

    const g_genericPopupStyle =
        `<style>
            .${g_genericPopupClass} {
                width: 100vw;
                height: 100vh;
                background-color: rgba(0, 0, 0, .5);
                position: fixed;
                left: 0;
                top: 0;
                bottom: 0;
                right: 0;
                z-index: 9999;
                display: none;
                justify-content: center;
                align-items: center;
            }
            .${g_genericPopupContentClass} {
                width: 100%;
                background-color: ${g_genericPopupBackgroundColor};
                box-sizing: border-box;
                padding: 0px 30px;
                color: black;
            }
            .${g_genericPopupProgressContentClass} {
                width: 400px;
                height: 200px;
                background-color: ${g_genericPopupBackgroundColor};
                box-sizing: border-box;
                border: 2px solid ${g_genericPopupBorderColor};
                border-radius: 5px;
                display: table;
            }
            #${g_genericPopupProgressContentId} {
                height: 100%;
                width: 100%;
                color: #0000c0;
                font-size: 24px;
                font-weight: bold;
                display: table-cell;
                text-align: center;
                vertical-align: middle;
            }
            .${g_genericPopupTopLineDivClass} {
                width: 100%;
                padding: 20px 0px;
                border-top: 2px groove #d0d0d0;
            }
            .generic-popup-title-foot-container {
                width: 100%;
                height: 40px;
                background-color: ${g_genericPopupBorderColor};
                padding: 0px 30px;
                display: table;
            }
            .${g_genericPopupTitleTextClass} {
                height: 100%;
                color: ${g_genericPopupTitleTextColor};
                font-size: 18px;
                display: table-cell;
                text-align: left;
                vertical-align: middle;
            }
        </style>`;

    const g_genericPopupHTML =
        `${g_genericPopupStyle}
         <div class="${g_genericPopupClass}" id="${g_genericPopupId}">
           <div style="border:2px solid ${g_genericPopupBorderColor};border-radius:5px;">
             <div class="generic-popup-title-foot-container">
               <span class="${g_genericPopupTitleTextClass}" id="${g_genericPopupTitleTextId}"></span>
               <div id="${g_genericPopupTitleButtonContainerId}" style="float:right;margin-top:6px;"></div>
             </div>
             <div id="${g_genericPopupContentContainerId}">
               <div class="${g_genericPopupContentClass}" id="${g_genericPopupFixedContentId}" style="display:none;"></div>
               <div class="${g_genericPopupContentClass}" id="${g_genericPopupContentId}"></div>
             </div>
             <div class="generic-popup-title-foot-container">
               <div id="${g_genericPopupFootButtonContainerId}" style="float:right;margin-top:8px;"></div>
             </div>
           </div>
         </div>
         <div class="${g_genericPopupProgressClass}" id="${g_genericPopupProgressId}">
           <div class="${g_genericPopupProgressContentClass}"><span id="${g_genericPopupProgressContentId}"></span></div>
         </div>`;

    var g_genericPopupContainer = null;
    function genericPopupInitialize() {
        if (g_genericPopupContainer == null) {
            g_genericPopupContainer = document.createElement('div');
            g_genericPopupContainer.id = g_genericPopupContainerId;
            document.body.appendChild(g_genericPopupContainer);
        }
        g_genericPopupContainer.innerHTML = g_genericPopupHTML;
    }

    function genericPopupReset() {
        let fixedContent = g_genericPopupContainer.querySelector('#' + g_genericPopupFixedContentId);
        fixedContent.style.display = 'none';
        fixedContent.innerHTML = '';

        g_genericPopupContainer.querySelector('#' + g_genericPopupTitleTextId).innerText = '';
        g_genericPopupContainer.querySelector('#' + g_genericPopupContentId).innerHTML = '';
        g_genericPopupContainer.querySelector('#' + g_genericPopupTitleButtonContainerId).innerHTML = '';
        g_genericPopupContainer.querySelector('#' + g_genericPopupFootButtonContainerId).innerHTML = '';
    }

    function genericPopupSetContent(title, content) {
        g_genericPopupContainer.querySelector('#' + g_genericPopupTitleTextId).innerText = title;
        g_genericPopupContainer.querySelector('#' + g_genericPopupContentId).innerHTML = content;
    }

    function genericPopupSetFixedContent(content) {
        let fixedContent = g_genericPopupContainer.querySelector('#' + g_genericPopupFixedContentId);
        fixedContent.style.display = 'block';
        fixedContent.innerHTML = content;
    }

    function genericPopupAddButton(text, width, clickProc, addToTitle) {
        let btn = document.createElement('button');
        btn.innerText = text;
        btn.onclick = clickProc;
        if (width != null && width > 0) {
            width = width.toString();
            btn.style.width = width + (width.endsWith('px') || width.endsWith('%') ? '' : 'px');
        }
        else {
            btn.style.width = 'auto';
        }

        g_genericPopupContainer.querySelector('#' + (addToTitle
                                              ? g_genericPopupTitleButtonContainerId
                                              : g_genericPopupFootButtonContainerId)).appendChild(btn);
        return btn;
    }

    function genericPopupAddCloseButton(width, text, addToTitle) {
        return genericPopupAddButton(text?.length > 0 ? text : '关闭', width, (() => { genericPopupClose(true); }), addToTitle);
    }

    function genericPopupSetContentSize(height, width, scrollable) {
        height = (height?.toString() ?? '100%');
        width = (width?.toString() ?? '100%');

        g_genericPopupContainer.querySelector('#' + g_genericPopupContentContainerId).style.width
            = width + (width.endsWith('px') || width.endsWith('%') ? '' : 'px');

        let content = g_genericPopupContainer.querySelector('#' + g_genericPopupContentId);
        content.style.height = height + (height.endsWith('px') || height.endsWith('%') ? '' : 'px');
        content.style.overflow = (scrollable ? 'auto' : 'hidden');
    }

    function genericPopupShowModal(clickOutsideToClose) {
        genericPopupClose(false);

        let popup = g_genericPopupContainer.querySelector('#' + g_genericPopupId);

        if (clickOutsideToClose) {
            popup.onclick = ((event) => {
                if (event.target == popup) {
                    genericPopupClose(true);
                }
            });
        }
        else {
            popup.onclick = null;
        }

        popup.style.display = "flex";
    }

    function genericPopupClose(reset) {
        genericPopupCloseProgressMessage();

        let popup = g_genericPopupContainer.querySelector('#' + g_genericPopupId);
        popup.style.display = "none";

        if (reset) {
            genericPopupReset();
        }

        httpRequestClearAll();
    }

    function genericPopupQuerySelector(selectString) {
        return g_genericPopupContainer.querySelector(selectString);
    }

    function genericPopupQuerySelectorAll(selectString) {
        return g_genericPopupContainer.querySelectorAll(selectString);
    }

    let g_genericPopupInformationTipsTimer = null;
    function genericPopupShowInformationTips(msg, time) {
        if (g_genericPopupInformationTipsTimer != null) {
            clearTimeout(g_genericPopupInformationTipsTimer);
            g_genericPopupInformationTipsTimer = null;
        }
        let msgContainer = g_genericPopupContainer.querySelector('#' + g_genericPopupInformationTipsId);
        if (msgContainer != null) {
            msgContainer.innerText = (msg?.length > 0 ? `[ ${msg} ]` : '');
            if ((time = parseInt(time)) > 0) {
                g_genericPopupInformationTipsTimer = setTimeout(() => {
                    g_genericPopupInformationTipsTimer = null;
                    msgContainer.innerText = '';
                }, time);
            }
        }
    }

    function genericPopupShowProgressMessage(progressMessage) {
        genericPopupClose(false);

        g_genericPopupContainer.querySelector('#' + g_genericPopupProgressContentId).innerText
            = (progressMessage?.length > 0 ? progressMessage : '请稍候...');
        g_genericPopupContainer.querySelector('#' + g_genericPopupProgressId).style.display = "flex";
    }

    function genericPopupCloseProgressMessage() {
        g_genericPopupContainer.querySelector('#' + g_genericPopupProgressId).style.display = "none";
    }

    //
    // generic task-list based progress popup
    //
    const g_genericPopupTaskListId = 'generic-popup-task-list';
    const g_genericPopupTaskItemId = 'generic-popup-task-item-';
    const g_genericPopupTaskWaiting = '×';
    const g_genericPopupTaskCompleted = '√';
    const g_genericPopupTaskCompletedWithError = '!';
    const g_genericPopupColorTaskIncompleted = '#c00000';
    const g_genericPopupColorTaskCompleted = '#0000c0';
    const g_genericPopupColorTaskCompletedWithError = 'red';

    var g_genericPopupIncompletedTaskCount = 0;
    function genericPopupTaskListPopupSetup(title, popupWidth, tasks, fnCancelRoutine, cancelButtonText, cancelButtonWidth) {
        g_genericPopupIncompletedTaskCount = tasks.length;

        genericPopupSetContent(title, `<div style="padding:15px 0px 15px 0px;"><ul id="${g_genericPopupTaskListId}"></ul></div>`);
        let indicatorList = g_genericPopupContainer.querySelector('#' + g_genericPopupTaskListId);
        for (let i = 0; i < g_genericPopupIncompletedTaskCount; i++) {
            let li = document.createElement('li');
            li.id = g_genericPopupTaskItemId + i;
            li.style.color = g_genericPopupColorTaskIncompleted;
            li.innerHTML = `<span>${g_genericPopupTaskWaiting}</span><span>&nbsp;${tasks[i]}&nbsp;</span><span></span>`;
            indicatorList.appendChild(li);
        }

        if (fnCancelRoutine != null) {
            genericPopupAddButton(cancelButtonText?.length > 0 ? cancelButtonText : '取消', cancelButtonWidth, fnCancelRoutine, false);
        }

        genericPopupSetContentSize(Math.min(g_genericPopupIncompletedTaskCount * 20 + 30, window.innerHeight - 400), popupWidth, true);
    }

    function genericPopupTaskSetState(index, state) {
        let item = g_genericPopupContainer.querySelector('#' + g_genericPopupTaskItemId + index)?.lastChild;
        if (item != undefined) {
            item.innerText = (state ?? '');
        }
    }

    function genericPopupTaskComplete(index, error) {
        let li = g_genericPopupContainer.querySelector('#' + g_genericPopupTaskItemId + index);
        if (li?.firstChild?.innerText == g_genericPopupTaskWaiting) {
            li.firstChild.innerText = (error ? g_genericPopupTaskCompletedWithError : g_genericPopupTaskCompleted);
            li.style.color = (error ? g_genericPopupColorTaskCompletedWithError : g_genericPopupColorTaskCompleted);
            g_genericPopupIncompletedTaskCount--;
        }
    }

    function genericPopupTaskCheckCompletion() {
        return (g_genericPopupIncompletedTaskCount == 0);
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // constants
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    const g_roles = [
        { index : -1 , id : 3000 , name : '舞' , shortMark : 'WU' },
        { index : -1 , id : 3001 , name : '默' , shortMark : 'MO' },
        { index : -1 , id : 3002 , name : '琳' , shortMark : 'LIN' },
        { index : -1 , id : 3003 , name : '艾' , shortMark : 'AI' },
        { index : -1 , id : 3004 , name : '梦' , shortMark : 'MENG' },
        { index : -1 , id : 3005 , name : '薇' , shortMark : 'WEI' },
        { index : -1 , id : 3006 , name : '伊' , shortMark : 'YI' },
        { index : -1 , id : 3007 , name : '冥' , shortMark : 'MING' },
        { index : -1 , id : 3008 , name : '命' , shortMark : 'MIN' },
        { index : -1 , id : 3009 , name : '希' , shortMark : 'XI' } ];

    const g_roleMap = new Map();
    g_roles.forEach((item, index) => {
        item.index = index;
        g_roleMap.set(item.id, item);
        g_roleMap.set(item.id.toString(), item);
        g_roleMap.set(item.name, item);
        g_roleMap.set(item.shortMark, item);
    });

    const g_equipAttributes = [
        { index : 0 , type : 0 , name : '物理攻击' },
        { index : 1 , type : 0 , name : '魔法攻击' },
        { index : 2 , type : 0 , name : '攻击速度' },
        { index : 3 , type : 0 , name : '最大生命' },
        { index : 4 , type : 0 , name : '最大护盾' },
        { index : 5 , type : 1 , name : '附加物伤' },
        { index : 6 , type : 1 , name : '附加魔伤' },
        { index : 7 , type : 1 , name : '附加攻速' },
        { index : 8 , type : 1 , name : '附加生命' },
        { index : 9 , type : 1 , name : '附加护盾' },
        { index : 10 , type : 1 , name : '附加回血' },
        { index : 11 , type : 1 , name : '附加回盾' },
        { index : 12 , type : 0 , name : '护盾回复' },
        { index : 13 , type : 0 , name : '物理穿透' },
        { index : 14 , type : 0 , name : '魔法穿透' },
        { index : 15 , type : 0 , name : '暴击穿透' },
        { index : 16 , type : 1 , name : '附加物穿' },
        { index : 17 , type : 1 , name : '附加物防' },
        { index : 18 , type : 1 , name : '附加魔防' },
        { index : 19 , type : 1 , name : '物理减伤' },
        { index : 20 , type : 1 , name : '魔法减伤' },
        { index : 21 , type : 0 , name : '生命偷取' },
        { index : 22 , type : 0 , name : '伤害反弹' },
        { index : 23 , type : 1 , name : '附加魔穿' } ];

    // g_equipAttributes.forEach((item, index) => {
    //     item.index = index;
    // });

    const g_equipments = [
        {
            index : -1,
            name : '反叛者的刺杀弓',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[0] , factor : 1 / 5 , additive : 30 },
                           { attribute : g_equipAttributes[15] , factor : 1 / 20 , additive : 10 },
                           { attribute : g_equipAttributes[13] , factor : 1 / 20 , additive : 10 },
                           { attribute : g_equipAttributes[16] , factor : 1 , additive : 0 } ],
            merge : null,
            shortMark : 'ASSBOW'
        },
        {
            index : -1,
            name : '狂信者的荣誉之刃',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[0] , factor : 1 / 5 , additive : 20 },
                           { attribute : g_equipAttributes[2] , factor : 1 / 5 , additive : 20 },
                           { attribute : g_equipAttributes[15] , factor : 1 / 20 , additive : 10 },
                           { attribute : g_equipAttributes[13] , factor : 1 / 20 , additive : 10 } ],
            merge : null,
            shortMark : 'BLADE'
        },
        {
            index : -1,
            name : '陨铁重剑',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[5] , factor : 20 , additive : 0 },
                           { attribute : g_equipAttributes[5] , factor : 20 , additive : 0 },
                           { attribute : g_equipAttributes[0] , factor : 1 / 5 , additive : 30 },
                           { attribute : g_equipAttributes[15] , factor : 1 / 20 , additive : 1 } ],
            merge : [ [ 0, 1 ], [ 2 ], [ 3 ] ],
            shortMark : 'CLAYMORE'
        },
        {
            index : -1,
            name : '幽梦匕首',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[0] , factor : 1 / 5 , additive : 0 },
                           { attribute : g_equipAttributes[1] , factor : 1 / 5 , additive : 0 },
                           { attribute : g_equipAttributes[7] , factor : 4 , additive : 0 },
                           { attribute : g_equipAttributes[2] , factor : 1 / 5 , additive : 25 } ],
            merge : null,
            shortMark : 'DAGGER'
        },
        {
            index : -1,
            name : '荆棘剑盾',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 10 },
                           { attribute : g_equipAttributes[22] , factor : 1 / 15 , additive : 0 },
                           { attribute : g_equipAttributes[17] , factor : 1 , additive : 0 },
                           { attribute : g_equipAttributes[18] , factor : 1 , additive : 0 } ],
            merge : null,
            shortMark : 'SHIELD'
        },
        {
            index : -1,
            name : '饮血长枪',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[0] , factor : 1 / 5 , additive : 50 },
                           { attribute : g_equipAttributes[13] , factor : 1 / 20 , additive : 10 },
                           { attribute : g_equipAttributes[23] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[21] , factor : 1 / 15, additive : 10 } ],
            merge : null,
            shortMark : 'SPEAR'
        },
        {
            index : -1,
            name : '光辉法杖',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[1] , factor : 1 / 5 , additive : 0 },
                           { attribute : g_equipAttributes[1] , factor : 1 / 5 , additive : 0 },
                           { attribute : g_equipAttributes[1] , factor : 1 / 5 , additive : 0 },
                           { attribute : g_equipAttributes[14] , factor : 1 / 20 , additive : 0 } ],
            merge : [ [ 0, 1, 2 ], [ 3 ] ],
            shortMark : 'WAND'
        },
        {
            index : -1,
            name : '探险者短弓',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[5] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[6] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[7] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 10 } ],
            merge : null,
            shortMark : 'BOW'
        },
        {
            index : -1,
            name : '探险者短杖',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[5] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[6] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[14] , factor : 1 / 20 , additive : 5 },
                           { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 10 } ],
            merge : null,
            shortMark : 'STAFF'
        },
        {
            index : -1,
            name : '探险者之剑',
            type : 0,
            attributes : [ { attribute : g_equipAttributes[5] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[6] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[16] , factor : 1 , additive : 0 },
                           { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 10 } ],
            merge : null,
            shortMark : 'SWORD'
        },
        {
            index : -1,
            name : '命师的传承手环',
            type : 1,
            attributes : [ { attribute : g_equipAttributes[1] , factor : 1 / 5 , additive : 1 },
                           { attribute : g_equipAttributes[14] , factor : 1 / 20 , additive : 1 },
                           { attribute : g_equipAttributes[9] , factor : 20 , additive : 0 },
                           { attribute : g_equipAttributes[18] , factor : 1 , additive : 0 } ],
            merge : null,
            shortMark : 'BRACELET'
        },
        {
            index : -1,
            name : '秃鹫手套',
            type : 1,
            attributes : [ { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 5 },
                           { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 5 },
                           { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 5 },
                           { attribute : g_equipAttributes[7] , factor : 2 , additive : 0 } ],
            merge : [ [ 0, 1, 2 ], [ 3 ] ],
            shortMark : 'VULTURE'
        },
        {
            index : -1,
            name : '探险者手套',
            type : 1,
            attributes : [ { attribute : g_equipAttributes[5] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[6] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[7] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[8] , factor : 10 , additive : 0 } ],
            merge : null,
            shortMark : 'GLOVES'
        },
        {
            index : -1,
            name : '旅法师的灵光袍',
            type : 2,
            attributes : [ { attribute : g_equipAttributes[8] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[11] , factor : 60 , additive : 0 },
                           { attribute : g_equipAttributes[4] , factor : 1 / 5 , additive : 25 },
                           { attribute : g_equipAttributes[9] , factor : 50 , additive : 0 } ],
            merge : null,
            shortMark : 'CLOAK'
        },
        {
            index : -1,
            name : '挑战斗篷',
            type : 2,
            attributes : [ { attribute : g_equipAttributes[4] , factor : 1 / 5 , additive : 50 },
                           { attribute : g_equipAttributes[9] , factor : 100 , additive : 0 },
                           { attribute : g_equipAttributes[18] , factor : 1 , additive : 0 },
                           { attribute : g_equipAttributes[20] , factor : 5 , additive : 0 } ],
            merge : null,
            shortMark : 'CAPE'
        },
        {
            index : -1,
            name : '战线支撑者的荆棘重甲',
            type : 2,
            attributes : [ { attribute : g_equipAttributes[3] , factor : 1 / 5 , additive : 20 },
                           { attribute : g_equipAttributes[17] , factor : 1 , additive : 0 },
                           { attribute : g_equipAttributes[18] , factor : 1 , additive : 0 },
                           { attribute : g_equipAttributes[22] , factor : 1 / 15 , additive : 10 } ],
            merge : null,
            shortMark : 'THORN'
        },
        {
            index : -1,
            name : '复苏木甲',
            type : 2,
            attributes : [ { attribute : g_equipAttributes[3] , factor : 1 / 5 , additive : 50 },
                           { attribute : g_equipAttributes[19] , factor : 5 , additive : 0 },
                           { attribute : g_equipAttributes[20] , factor : 5 , additive : 0 },
                           { attribute : g_equipAttributes[10] , factor : 20 , additive : 0 } ],
            merge : null,
            shortMark : 'WOOD'
        },
        {
            index : -1,
            name : '探险者铁甲',
            type : 2,
            attributes : [ { attribute : g_equipAttributes[8] , factor : 20 , additive : 0 },
                           { attribute : g_equipAttributes[17] , factor : 1 , additive : 0 },
                           { attribute : g_equipAttributes[18] , factor : 1 , additive : 0 },
                           { attribute : g_equipAttributes[10] , factor : 10 , additive : 0 } ],
            merge : null,
            shortMark : 'PLATE'
        },
        {
            index : -1,
            name : '探险者皮甲',
            type : 2,
            attributes : [ { attribute : g_equipAttributes[8] , factor : 25 , additive : 0 },
                           { attribute : g_equipAttributes[19] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[20] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[10] , factor : 6 , additive : 0 } ],
            merge : null,
            shortMark : 'LEATHER'
        },
        {
            index : -1,
            name : '探险者布甲',
            type : 2,
            attributes : [ { attribute : g_equipAttributes[8] , factor : 25 , additive : 0 },
                           { attribute : g_equipAttributes[19] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[20] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[10] , factor : 6 , additive : 0 } ],
            merge : null,
            shortMark : 'CLOTH'
        },
        {
            index : -1,
            name : '天使缎带',
            type : 3,
            attributes : [ { attribute : g_equipAttributes[8] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[9] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[10] , factor : 5 , additive : 0 },
                           { attribute : g_equipAttributes[12] , factor : 1 / 30 , additive : 0 } ],
            merge : null,
            shortMark : 'RIBBON'
        },
        {
            index : -1,
            name : '占星师的发饰',
            type : 3,
            attributes : [ { attribute : g_equipAttributes[8] , factor : 5 , additive : 0 },
                           { attribute : g_equipAttributes[4] , factor : 1 / 5 , additive : 0 },
                           { attribute : g_equipAttributes[9] , factor : 20 , additive : 0 },
                           { attribute : g_equipAttributes[19] , factor : 2 , additive : 0 } ],
            merge : null,
            shortMark : 'TIARA'
        },
        {
            index : -1,
            name : '探险者头巾',
            type : 3,
            attributes : [ { attribute : g_equipAttributes[8] , factor : 10 , additive : 0 },
                           { attribute : g_equipAttributes[19] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[20] , factor : 2 , additive : 0 },
                           { attribute : g_equipAttributes[10] , factor : 4 , additive : 0 } ],
            merge : null,
            shortMark : 'SCARF'
        }];

    const g_defaultEquipAttributeMerge = [ [0], [1], [2], [3] ];
    function defaultEquipmentNodeComparer(setting, eqKey, eq1, eq2) {
        let eqMeta = g_equipMap.get(eqKey);
        let delta = [];
        let minorAdv = 0;
        let majorDis = 0;

        eqMeta.attributes.forEach((attr, index) => {
            let d = Math.trunc((eq1[0] * attr.factor + attr.additive) * eq1[index + 1]) -
                    Math.trunc((eq2[0] * attr.factor + attr.additive) * eq2[index + 1]);
            if (setting[index + 1]) {
                delta.push(0);
                if (d > 0) {
                    minorAdv++;
                }
            }
            else {
                delta.push(d);
            }
        });

        let merge = (eqMeta.merge?.length > 1 ? eqMeta.merge : g_defaultEquipAttributeMerge);
        for (let indices of merge) {
            let sum = 0;
            indices.forEach((index) => { sum += delta[index]; });
            if (sum > 0) {
                return true;
            }
            else if (sum < 0) {
                majorDis++;
            }
        };

        return (majorDis == 0 && minorAdv > 0);
    }

    function formatEquipmentAttributes(e, itemSeparator) {
        let text = '';
        if (e?.length > 5) {
            itemSeparator ??= ', ';
            let sp = '';
            g_equipMap.get(e[0])?.attributes.forEach((attr, index) => {
                text += `${sp}${attr.attribute.name} +${Math.trunc((e[1] * attr.factor + attr.additive) *
                                                                    e[index + 2] / 100)}${attr.attribute.type == 0 ? '%' : ''}`;
                sp = itemSeparator;
            });
        }
        return text;
    }

    const g_equipMap = new Map();
    g_equipments.forEach((item, index) => {
        item.index = index;
        g_equipMap.set(item.name, item);
        g_equipMap.set(item.shortMark, item);
    });

    const g_halos = [
        { index : -1 , id : 101 , name : '启程之誓' , points : 10 , shortMark : 'SHI' },
        { index : -1 , id : 102 , name : '启程之心' , points : 10 , shortMark : 'XIN' },
        { index : -1 , id : 103 , name : '启程之风' , points : 10 , shortMark : 'FENG' },
        { index : -1 , id : 201 , name : '破壁之心' , points : 30 , shortMark : 'BI' },
        { index : -1 , id : 202 , name : '破魔之心' , points : 30 , shortMark : 'MO' },
        { index : -1 , id : 203 , name : '复合护盾' , points : 30 , shortMark : 'DUN' },
        { index : -1 , id : 204 , name : '鲜血渴望' , points : 30 , shortMark : 'XUE' },
        { index : -1 , id : 205 , name : '削骨之痛' , points : 30 , shortMark : 'XIAO' },
        { index : -1 , id : 206 , name : '圣盾祝福' , points : 30 , shortMark : 'SHENG' },
        { index : -1 , id : 301 , name : '伤口恶化' , points : 50 , shortMark : 'SHANG' },
        { index : -1 , id : 302 , name : '精神创伤' , points : 50 , shortMark : 'SHEN' },
        { index : -1 , id : 303 , name : '铁甲尖刺' , points : 50 , shortMark : 'CI' },
        { index : -1 , id : 304 , name : '忍无可忍' , points : 50 , shortMark : 'REN' },
        { index : -1 , id : 305 , name : '热血战魂' , points : 50 , shortMark : 'RE' },
        { index : -1 , id : 306 , name : '点到为止' , points : 50 , shortMark : 'DIAN' },
        { index : -1 , id : 401 , name : '沸血之志' , points : 100 , shortMark : 'FEI' },
        { index : -1 , id : 402 , name : '波澜不惊' , points : 100 , shortMark : 'BO' },
        { index : -1 , id : 403 , name : '飓风之力' , points : 100 , shortMark : 'JU' },
        { index : -1 , id : 404 , name : '红蓝双刺' , points : 100 , shortMark : 'HONG' },
        { index : -1 , id : 405 , name : '荧光护盾' , points : 100 , shortMark : 'JUE' },
        { index : -1 , id : 406 , name : '后发制人' , points : 100 , shortMark : 'HOU' } ];

    const g_haloMap = new Map();
    g_halos.forEach((item, index) => {
        item.index = index;
        g_haloMap.set(item.id, item);
        g_haloMap.set(item.id.toString(), item);
        g_haloMap.set(item.name, item);
        g_haloMap.set(item.shortMark, item);
    });

    const g_configs = [
        {
            index : -1,
            id : 'maxConcurrentRequests',
            name : `最大并发网络请求(${g_ConcurrentRequestCount.min} - ${g_ConcurrentRequestCount.max})`,
            defaultValue : g_ConcurrentRequestCount.default,
            value : g_ConcurrentRequestCount.default,
            tips : '同时向服务器提交的请求的最大数量。过高的设置容易引起服务阻塞或被认定为DDOS攻击从而导致服务器停止服务(HTTP 503)。',
            validate : ((value) => {
                return (!isNaN(value = parseInt(value)) &&
                        value >= g_ConcurrentRequestCount.min &&
                        value <= g_ConcurrentRequestCount.max);
            }),
            onchange : ((value) => {
                if (!isNaN(value = parseInt(value)) &&
                    value >= g_ConcurrentRequestCount.min &&
                    value <= g_ConcurrentRequestCount.max) {

                    return (g_maxConcurrentRequests = value);
                }
                return (g_maxConcurrentRequests = g_ConcurrentRequestCount.default);
            })
        },
        {
            index : -1,
            id : 'minBeachEquipLevelToAmulet',
            name : `海滩稀有装备转护符最小等级`,
            defaultValue : 1,
            value : 1,
            tips : '海滩稀有装备批量转换护符时所需达到的最小等级,小于此等级的装备不会被转换,但玩家依然可以选择手动熔炼。史诗和传奇装备则肯定会被自动转换。',
            validate : ((value) => {
                return /^\s*\d{1,3}\s*$/.test(value);
            }),
            onchange : ((value) => {
                if (/^\s*\d{1,3}\s*$/.test(value)) {
                    return parseInt(value);
                }
                return 1;
            })
        },
        {
            index : -1,
            id : 'minBeachAmuletPointsToStore',
            name : `海滩转护符默认入仓最小加成(苹果,葡萄,樱桃)`,
            defaultValue : '1, 1%, 1%',
            value : '1, 1%, 1%',
            tips : '海滩装备批量转换护符时默认处于入仓列表的最小加成(‘%’可省略)。此设置仅为程序产生分类列表时作为参考,玩家可通过双击特定护符移动它的位置。',
            validate : ((value) => {
                return /^\s*\d+\s*,\s*\d+\s*%?\s*,\s*\d+\s*%?\s*$/.test(value);
            }),
            onchange : ((value) => {
                if (/^\s*\d+\s*,\s*\d+\s*%?\s*,\s*\d+\s*%?\s*$/.test(value)) {
                    return value;
                }
                return '1, 1%, 1%';
            })
        } ];

    const g_configMap = new Map();
    g_configs.forEach((item, index) => {
        item.index = index;
        g_configMap.set(item.id, item);
    });

    function initiatizeConfig() {
        let udata = loadUserConfigData();
        if (udata == null) {
            udata = {
                dataIndex : { battleInfoNow : '' , battleInfoBefore : '' , battleInfoBack : '' },
                dataBeachSift : {},
                dataBind : {},
                config : {}
            };
        }
        if (udata.dataIndex == null) {
            udata.dataIndex = { battleInfoNow : '' , battleInfoBefore : '' , battleInfoBack : '' };
        }
        if (udata.dataBeachSift == null) {
            udata.dataBeachSift = {};
        }
        if (udata.dataBind == null) {
            udata.dataBind = {};
        }
        if (udata.config == null) {
            udata.config = {};
        }
        for (let key in udata.dataBeachSift) {
            if (g_equipMap.get(key) == undefined) {
                delete udata.dataBeachSift[key];
            }
        }
        for (let key in udata.dataBind) {
            if (g_roleMap.get(key) == undefined) {
                delete udata.dataBind[key];
            }
        }
        for (let key in udata.config) {
            if (g_configMap.get(key) == undefined) {
                delete udata.config[key];
            }
        }

        g_configs.forEach((item) => {
            item.value = (item.onchange?.call(null, udata.config[item.id] ?? item.defaultValue));
        });

        saveUserConfigData(udata);
    }

    function wishExpireTip() {
        if (localStorage.getItem(g_ignoreWishpoolExpirationStorageKey) != 'true') {
            let misc;
            beginReadWishpool(
                null,
                misc = [],
                () => {
                    if (parseInt(misc[1]) < 2) {
                        let navButtons = document.querySelectorAll(
                            'div.btn-toolbar > div.row > div.col-md-10 > div > div.btn-group > button.btn.btn-lg');
                        for (let btn of navButtons) {
                            if (btn.innerText == '许愿池') {
                                btn.innerText = '许愿池(已过期)';
                                btn.className += ' btn-danger';
                                break;
                            }
                        }
                    }
                },
                null);
        }
    }

    initiatizeConfig();
    wishExpireTip();

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // page add-ins
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    if (window.location.pathname == '/fyg_index.php') {
        function doConfig() {
            let fixedContent =
                '<div style="padding:20px 10px 10px 0px;color:blue;font-size:15px;"><b>请勿随意修改配置项,' +
                `除非您知道它的准确用途并且设置为正确的值,否则可能会导致插件工作异常。<span id="${g_genericPopupInformationTipsId}" ` +
                'style="float:right;color:red;"></span></b></div>';
            let mainContent =
                `<style> #config-table { width:100%; }
                         #config-table tr.config-tr { }
                         #config-table tr.config-tr-alt { background-color:${g_genericPopupBackgroundColorAlt}; }
                         #config-table th { width:20%; }
                         #config-table th.config-th-name { width:60%; }
                         #config-table th.config-th-button { width:20%; }
                         #config-table button.config-restore-value { width:50%; }
                 </style>
                 <div class="${g_genericPopupTopLineDivClass}"><table id="config-table">
                 <tr><th class="config-th-name">配置项</th><th>值</th><th class="config-th-button"></th></tr></table><div>`;

            genericPopupSetFixedContent(fixedContent);
            genericPopupSetContent('插件设置', mainContent);

            let configTable = genericPopupQuerySelector('#config-table');
            g_configs.forEach((item, index) => {
                let tr = document.createElement('tr');
                tr.className = 'config-tr' + ((index & 1) == 0 ? ' config-tr-alt' : '');
                tr.setAttribute('config-item', item.id);
                tr.innerHTML =
                    `<td><div data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="${item.tips}">${item.name}<div></td>
                     <td><div data-toggle="popover" data-placement="bottom" data-trigger="hover" data-content="${item.tips}">
                         <input type="text" style="display:inline-block;width:100%;" value="${item.value}" /><div></td>
                     <td><button type="button" class="config-restore-value" title="重置为当前配置" value="${item.value}">当前</button>` +
                        `<button type="button" class="config-restore-value" title="重置为默认配置" value="${item.defaultValue}">默认</button></td>`;
                tr.children[1].children[0].children[0].oninput = tr.children[1].children[0].children[0].onchange = validateInput;
                configTable.appendChild(tr);
            });
            function validateInput(e) {
                let tr = e.target.parentNode.parentNode.parentNode;
                let cfg = g_configMap.get(tr.getAttribute('config-item'));
                tr.style.color = ((cfg.validate?.call(null, e.target.value) ?? true) ? 'black' : 'red');
            }

            configTable.querySelectorAll('button.config-restore-value').forEach((btn) => { btn.onclick = restoreValue; });
            function restoreValue(e) {
                let input = e.target.parentNode.parentNode.children[1].children[0].children[0];
                input.value = e.target.value;
                input.oninput({ target : input });
                genericPopupShowInformationTips('配置项已' + e.target.title, 5000);
            }

            $('#config-table div[data-toggle="popover"]').popover();

            genericPopupAddButton('重置为当前配置', 0, restoreValueAll, true).setAttribute('config-restore-default-all', 0);
            genericPopupAddButton('重置为默认配置', 0, restoreValueAll, true).setAttribute('config-restore-default-all', 1);
            function restoreValueAll(e) {
                let defaultValue = (e.target.getAttribute('config-restore-default-all') == '1');
                configTable.querySelectorAll('tr.config-tr').forEach((row) => {
                    let id = row.getAttribute('config-item');
                    let cfg = g_configMap.get(id);
                    let input = row.children[1].children[0].children[0];
                    input.value = (defaultValue ? cfg.defaultValue : (cfg.value ?? cfg.defaultValue));
                    input.oninput({ target : input });
                });
                genericPopupShowInformationTips('全部配置项已' + e.target.innerText, 5000);
            }

            genericPopupAddButton('保存', 80, saveConfig, false).setAttribute('config-save-config', 1);
            genericPopupAddButton('确认', 80, saveConfig, false).setAttribute('config-save-config', 0);
            function saveConfig(e) {
                let close = (e.target.getAttribute('config-save-config') == '0');
                let udata = loadUserConfigData();
                let config = (udata?.config ?? {});
                let error = [];
                configTable.querySelectorAll('tr.config-tr').forEach((row) => {
                    let id = row.getAttribute('config-item');
                    let cfg = g_configMap.get(id);
                    let value = row.children[1].children[0].children[0].value;
                    if (cfg.validate?.call(null, value) ?? true) {
                        config[id] = cfg.value = row.children[2].children[0].value = (cfg.onchange?.call(null, value) ?? value);
                    }
                    else {
                        error.push(cfg.name);
                    }
                });

                udata.config = config;
                saveUserConfigData(udata);

                if (error.length > 0) {
                    alert('以下配置项输入内容有误,如有必要请重新设置:\n\n    [ ' + error.join(' ]\n    [ ') + ' ]');
                }
                else if (close) {
                    genericPopupClose(true);
                }
                else {
                    genericPopupShowInformationTips('配置已保存', 5000);
                }
            }
            genericPopupAddCloseButton(80);

            genericPopupSetContentSize(Math.min(g_configs.length * 28 + 60, Math.max(window.innerHeight - 200, 400)),
                                       Math.min(600, Math.max(window.innerWidth - 100, 600)),
                                       true);
            genericPopupShowModal(true);
        }

        const USER_DATA_xPORT_SEPARATOR = '\n';

        function importUserConfigData() {
            genericPopupSetContent(
                '导入内容',
                `<b><div style="color:#0000c0;padding:15px 0px 10px;">
                 请将从其它系统中使用同一帐号导出的内容填入文本框中并执行导入操作</div></b>
                 <div style="height:330px;"><textarea id="user_data_persistence_string"
                 style="height:100%;width:100%;resize:none;"></textarea></div>`);

            genericPopupAddButton(
                '执行导入',
                0,
                (() => {
                    let userData = genericPopupQuerySelector('#user_data_persistence_string').value.split(USER_DATA_xPORT_SEPARATOR);
                    if (userData.length > 0) {
                        if (confirm('导入操作会覆盖已有的用户配置(护符组定义、卡片装备光环护符绑定、海滩装备筛选配置等等),要继续吗?')) {
                            let backup = [];
                            let importedItems = [];
                            let illegalItems = [];
                            g_userDataStorageKeyConfig.forEach((item, index) => {
                                backup[index] = localStorage.getItem(item);
                            });
                            userData.forEach((item) => {
                                if ((item = item.trim()).length > 0) {
                                    let key = item.slice(0, item.indexOf(USER_STORAGE_KEY_VALUE_SEPARATOR));
                                    if (g_userDataStorageKeyConfig.indexOf(key) >= 0) {
                                        if (illegalItems.length == 0) {
                                            localStorage.setItem(key, item.substring(key.length + 1));
                                            importedItems.push(key);
                                        }
                                    }
                                    else {
                                        illegalItems.push(key);
                                    }
                                }
                            });
                            if (illegalItems.length > 0) {
                                importedItems.forEach((item) => {
                                    let index = g_userDataStorageKeyConfig.indexOf(item);
                                    if (index >= 0 && backup[index] != null) {
                                        localStorage.setItem(item, backup[index]);
                                    }
                                    else {
                                        localStorage.removeItem(item);
                                    }
                                });
                                alert('输入内容格式有误,有非法项目导致导入失败,请检查:\n\n    [ ' + illegalItems.join(' ]\n    [ ') + ' ]');
                            }
                            else if (importedItems.length > 0) {
                                alert('导入已完成:\n\n    [ ' + importedItems.join(' ]\n    [ ') + ' ]');
                                genericPopupClose(true);
                                window.location.reload();
                            }
                            else {
                                alert('输入内容格式有误,导入失败,请检查!');
                            }
                        }
                    }
                    else {
                        alert('输入内容格式有误,导入失败,请检查!');
                    }
                }),
                true);
            genericPopupAddCloseButton(80);

            genericPopupSetContentSize(400, 600, false);
            genericPopupShowModal(true);
        }

        function exportUserConfigData() {
            genericPopupSetContent(
                '导出内容',
                `<b><div id="user_data_export_tip" style="color:#0000c0;padding:15px 0px 10px;">
                 请勿修改任何导出内容,将其保存为纯文本在其它系统中使用相同的帐号执行导入操作</div></b>
                 <div style="height:330px;"><textarea id="user_data_persistence_string" readonly="true"
                 style="height:100%;width:100%;resize:none;"></textarea></div>`);

            genericPopupAddButton(
                '复制导出内容至剪贴板',
                0,
                ((e) => {
                    e.target.disabled = 'disabled';
                    let tipContainer = genericPopupQuerySelector('#user_data_export_tip');
                    let tipColor = tipContainer.style.color;
                    let tipString = tipContainer.innerText;
                    tipContainer.style.color = '#ff0000';
                    genericPopupQuerySelector('#user_data_persistence_string').select();
                    if (document.execCommand('copy')) {
                        tipContainer.innerText = '导出内容已复制到剪贴板';
                    }
                    else {
                        tipContainer.innerText = '复制失败,这可能是因为浏览器没有剪贴板访问权限,请进行手工复制(CTRL+A, CTRL+C)';
                    }
                    setTimeout((() => {
                        tipContainer.style.color = tipColor;
                        tipContainer.innerText = tipString;
                        e.target.disabled = '';
                    }), 3000);
                }),
                true);
            genericPopupAddCloseButton(80);

            let userData = [];
            g_userDataStorageKeyConfig.forEach((item) => {
                let value = localStorage.getItem(item);
                if (value != null) {
                    userData.push(`${item}${USER_STORAGE_KEY_VALUE_SEPARATOR}${value}`);
                }
            });
            genericPopupQuerySelector('#user_data_persistence_string').value = userData.join(USER_DATA_xPORT_SEPARATOR);

            genericPopupSetContentSize(400, 600, false);
            genericPopupShowModal(true);
        }

        function clearUserData() {
            if (confirm('这将清除所有用户配置(护符组定义、卡片装备光环护符绑定、海滩装备筛选配置等等)和数据,要继续吗?')) {
                g_userDataStorageKeyConfig.concat(g_userDataStorageKeyExtra).forEach((item) => {
                    localStorage.removeItem(item);
                });
                alert('用户配置和数据已全部清除!');
                window.location.reload();
            }
        }

        let waitForLoad = setInterval(() => {
            let panel = document.querySelector('div.col-md-3 > div.panel > div.panel-body');
            if (panel?.children?.length > 4) {
                clearInterval(waitForLoad);
                genericPopupInitialize();

                let userData = loadUserConfigData();
                let dataIndex = userData.dataIndex;

                let px = panel.children[3];
                let p0 = document.createElement(px.tagName);
                p0.className = px.className;
                p0.innerText = '对玩家战斗(上次查看)';

                let sp = document.createElement(px.children[0].tagName);
                sp.className = px.children[0].className;

                dataIndex.battleInfoNow = px.children[0].innerText;
                if (dataIndex.battleInfoNow == dataIndex.battleInfoBefore) {
                    sp.innerText = dataIndex.battleInfoBack;
                }
                else {
                    sp.innerText = dataIndex.battleInfoBefore;
                    dataIndex.battleInfoBack = dataIndex.battleInfoBefore;
                    dataIndex.battleInfoBefore = dataIndex.battleInfoNow
                    saveUserConfigData(userData);
                }

                p0.appendChild(sp);
                px.parentNode.insertBefore(p0, px.nextSibling);

                let globalDataBtnContainer = document.createElement('div');
                globalDataBtnContainer.id = 'global-data-button-container';
                globalDataBtnContainer.style.borderTop = '1px solid #d0d0d0';
                globalDataBtnContainer.style.padding = '10px 0px 0px';

                let versionLabel = document.createElement(px.tagName);
                versionLabel.innerText = '插件版本:';
                versionLabel.className = px.className;

                let versionText = document.createElement(px.children[0].tagName);
                versionText.className = px.children[0].className;
                versionText.innerText = g_modificationVersion;
                versionLabel.appendChild(versionText);
                globalDataBtnContainer.appendChild(versionLabel);

                let configBtn = document.createElement('button');
                configBtn.innerHTML = '设置';
                configBtn.style.height = '30px';
                configBtn.style.width = '100%';
                configBtn.style.marginBottom = '1px';
                configBtn.onclick = (() => {
                    doConfig();
                });
                globalDataBtnContainer.appendChild(configBtn);

                let importBtn = document.createElement('button');
                importBtn.innerHTML = '导入用户配置数据';
                importBtn.style.height = '30px';
                importBtn.style.width = '100%';
                importBtn.style.marginBottom = '1px';
                importBtn.onclick = (() => {
                    importUserConfigData();
                });
                globalDataBtnContainer.appendChild(importBtn);

                let exportBtn = document.createElement('button');
                exportBtn.innerHTML = '导出用户配置数据';
                exportBtn.style.height = '30px';
                exportBtn.style.width = '100%';
                exportBtn.style.marginBottom = '1px';
                exportBtn.onclick = (() => {
                    exportUserConfigData();
                });
                globalDataBtnContainer.appendChild(exportBtn);

                let eraseBtn = document.createElement('button');
                eraseBtn.innerHTML = '清除用户数据';
                eraseBtn.style.height = '30px';
                eraseBtn.style.width = '100%';
                eraseBtn.style.marginBottom = '1px';
                eraseBtn.onclick = (() => {
                    clearUserData();
                });
                globalDataBtnContainer.appendChild(eraseBtn);

                px.parentNode.appendChild(globalDataBtnContainer);
            }
        }, 200);
    }
    else if (window.location.pathname == '/fyg_equip.php') {
        genericPopupInitialize();

        let waitForBackpacks = setInterval(() => {
            if (document.getElementById('backpacks')?.children?.length > 0) {
                clearInterval(waitForBackpacks);

                let panel = document.getElementsByClassName('panel panel-primary')[1];
                let calcBtn = document.createElement('button');
                let calcDiv = document.createElement('div');

                calcBtn.innerText = '导出计算器';
                calcBtn.onclick = (() => {});

                panel.insertBefore(calcBtn, panel.children[0]);
                panel.insertBefore(calcDiv, calcBtn);

                const cardingObjectsQueryString = '#carding > div.row > div.fyg_tc > button.fyg_mp3';
                const bagObjectsQueryString = '#backpacks > div.alert-danger > div.content > button.fyg_mp3';
                const storeObjectsQueryString = '#backpacks > div.alert-success > div.content > button.fyg_mp3';
                const storeQueryString = '#backpacks > div.alert.alert-success.with-icon';
                const storeButtonId = 'collapse-backpacks-store';

                let equipmentDiv = document.createElement('div');
                equipmentDiv.id = 'equipmentDiv';
                equipmentDiv.innerHTML =
                    `<p><div style="padding:0px 0px 10px 30px;float:right;">
                        <label for="equipment_BG" style="margin-right:5px;cursor:pointer;">使用深色背景</label>
                        <input type="checkbox" id="equipment_BG" style="margin-right:15px;" />
                        <label for="equipment_Expand" style="margin-right:5px;cursor:pointer;">全部展开</label>
                        <input type="checkbox" id="equipment_Expand" /></div></p>
                     <p><button type="button" class="btn btn-block collapsed" data-toggle="collapse" data-target="#eq4">护符 ▼</button></p>
                        <div class="in" id="eq4"></div>
                     <p><button type="button" class="btn btn-block collapsed" data-toggle="collapse" data-target="#eq0">武器装备 ▼</button></p>
                        <div class="in" id="eq0"></div>
                     <p><button type="button" class="btn btn-block collapsed" data-toggle="collapse" data-target="#eq1">手臂装备 ▼</button></p>
                        <div class="in" id="eq1"></div>
                     <p><button type="button" class="btn btn-block collapsed" data-toggle="collapse" data-target="#eq2">身体装备 ▼</button></p>
                        <div class="in" id="eq2"></div>
                     <p><button type="button" class="btn btn-block collapsed" data-toggle="collapse" data-target="#eq3">头部装备 ▼</button></p>
                        <div class="in" id="eq3"></div>
                     <p><button type="button" class="btn btn-block collapsed" id="${storeButtonId}">仓库 ▼</button></p>`;

                let forceEquipDivOperation = true;
                let equipDivExpanded = {};

                equipmentDiv.querySelectorAll('.btn.btn-block.collapsed').forEach((btn) => { btn.onclick = backupEquipmentDivState; });
                function backupEquipmentDivState(e) {
                    let targetDiv = equipmentDiv.querySelector(e.target.getAttribute('data-target'));
                    if (targetDiv != null) {
                        equipDivExpanded[targetDiv.id] = !equipDivExpanded[targetDiv.id];
                    }
                    else {
                        equipDivExpanded[e.target.id] = !equipDivExpanded[e.target.id];
                    }
                };

                function collapseEquipmentDiv(expand, force) {
                    let targetDiv;
                    equipmentDiv.querySelectorAll('.btn.btn-block').forEach((btn) => {
                        if (btn.getAttribute('data-toggle') == 'collapse' &&
                            (targetDiv = equipmentDiv.querySelector(btn.getAttribute('data-target'))) != null) {

                            let exp = expand;
                            if (equipDivExpanded[targetDiv.id] == undefined || force) {
                                equipDivExpanded[targetDiv.id] = exp;
                            }
                            else {
                                exp = equipDivExpanded[targetDiv.id];
                            }

                            targetDiv.className = (exp ? 'in' : 'collapse');
                            targetDiv.style.height = (exp ? 'auto' : '0px');
                        }
                    });
                    if (equipDivExpanded[storeButtonId] == undefined || force) {
                        equipDivExpanded[storeButtonId] = expand;
                    }
                    if (equipDivExpanded[storeButtonId]) {
                        $(storeQueryString).show();
                    }
                    else {
                        $(storeQueryString).hide();
                    }
                }

                function changeEquipmentDivStyle(bg) {
                    $('#equipmentDiv .backpackDiv').css({
                        'background-color': bg ? 'black' : '#ffe5e0'
                    });
                    $('#equipmentDiv .storeDiv').css({
                        'background-color': bg ? 'black' : '#ddf4df'
                    });
                    $('#equipmentDiv .btn-light').css({
                        'background-color': bg ? 'black' : 'white'
                    });
                    $('#equipmentDiv .popover-content-show').css({
                        'background-color': bg ? 'black' : 'white'
                    });
                    $('#equipmentDiv .popover-title').css({
                        'color': bg ? 'black' : 'white'
                    });
                    $('#equipmentDiv .bg-special').css({
                        'background-color': bg ? 'black' : '#8666b8',
                        'color': bg ? '#c0c0c0' : 'white',
                        'border-bottom': bg ? '1px solid grey' : 'none'
                    });
                    $('#equipmentDiv .btn-equipment .pull-right').css({
                        'color': bg ? 'black' : 'white'
                    });
                    $('#equipmentDiv .btn-equipment .bg-danger.with-padding').css({
                        'color': bg ? 'black' : 'white'
                    });
                }

                let equipmentExpand = equipmentDiv.querySelector('#equipment_Expand').checked =
                    (localStorage.getItem(g_equipmentExpandStorageKey) == 'true');
                equipmentDiv.querySelector('#equipment_Expand').onchange = (() => {
                    localStorage.setItem(g_equipmentExpandStorageKey,
                                         equipmentExpand = document.querySelector('#equipment_Expand').checked);
                    collapseEquipmentDiv(equipmentExpand, true);
                });

                let equipmentBG = equipmentDiv.querySelector('#equipment_BG').checked =
                    (localStorage.getItem(g_equipmentBGStorageKey) == 'true');
                equipmentDiv.querySelector('#equipment_BG').onchange = (() => {
                    localStorage.setItem(g_equipmentBGStorageKey,
                                         equipmentBG = document.querySelector('#equipment_BG').checked);
                    changeEquipmentDivStyle(equipmentBG);
                });

                function addCollapse() {
                    let waitForBtn = setInterval(() => {
                        if (document.getElementById('carding')?.innerText?.indexOf('读取中') < 0 &&
                            document.getElementById('backpacks')?.innerText?.indexOf('读取中') < 0) {

                            let eqbtns = document.querySelectorAll(cardingObjectsQueryString);
                            if (eqbtns?.length > 0) {
                                clearInterval(waitForBtn);

                                let eqstore = document.querySelectorAll(storeObjectsQueryString);
                                eqstore.forEach((item) => {
                                    if (item.className?.split(' ').length > 2 && item.className.endsWith('fyg_mp3')) {
                                        item.dataset.instore = 1;
                                    }
                                });

                                eqbtns =
                                    Array.from(eqbtns).concat(
                                    Array.from(document.querySelectorAll(bagObjectsQueryString))
                                         .sort(objectNodeComparer)).concat(
                                    Array.from(eqstore).sort(objectNodeComparer));

                                for (let i = eqbtns.length - 1; i >= 0; i--) {
                                    if (!(eqbtns.className?.split(' ').length > 2 || eqbtns[i].className.endsWith('fyg_mp3'))) {
                                        eqbtns.splice(i, 1);
                                    }
                                }
                                if (!(document.getElementsByClassName('collapsed')?.length > 0)) {
                                    document.getElementById('backpacks')
                                            .insertBefore(equipmentDiv, document.getElementById('backpacks').firstChild.nextSibling);
                                }
                                for (let i = eqbtns.length - 1; i >= 0; i--) {
                                    if (eqbtns[i].className.split(' ')[0] == 'popover') {
                                        eqbtns.splice(i, 1);
                                        break;
                                    }
                                }

                                let ineqBackpackDiv =
                                    `<div class="backpackDiv" style="padding:10px;margin-bottom:10px;"></div>` +
                                    `<div class="storeDiv" style="padding:10px;"></div>`;
                                let eqDivs = [ equipmentDiv.querySelector('#eq0'),
                                               equipmentDiv.querySelector('#eq1'),
                                               equipmentDiv.querySelector('#eq2'),
                                               equipmentDiv.querySelector('#eq3'),
                                               equipmentDiv.querySelector('#eq4') ];
                                eqDivs.forEach((item) => { item.innerHTML = ineqBackpackDiv; });
                                let ineq = 0;

                                eqbtns.forEach((btn) => {
                                    if (objectIsEmptyNode(btn)) {
                                        return;
                                    }

                                    let btn0 = document.createElement('button');
                                    btn0.className = `btn btn-light ${g_equipmentLevelTipClass[equipmentGetLevel(btn)]}`;
                                    btn0.style.minWidth = '200px';
                                    btn0.style.marginRight = '5px';
                                    btn0.style.marginBottom = '5px';
                                    btn0.style.padding = '0px';
                                    btn0.style.textAlign = 'left';
                                    btn0.style.boxShadow = 'none';
                                    btn0.style.lineHeight = '150%';
                                    btn0.setAttribute('onclick', btn.getAttribute('onclick'));

                                    let storeText = '';
                                    if (btn.dataset.instore == 1) {
                                        storeText = '【仓】';
                                    }

                                    let enhancements = btn.innerText;
                                    if (enhancements.indexOf('+') < 0) {
                                        enhancements = '';
                                    }

                                    btn0.innerHTML =
                                        `<h3 class="popover-title" style="color:white;background-color: ${btn0.style.borderColor}">
                                         ${storeText}${btn.dataset.originalTitle}${enhancements}</h3>
                                         <div class="popover-content-show" style="padding:10px 10px 0px 10px;">${btn.dataset.content}</div>`;

                                    if (btn0.children[1].lastChild.nodeType == 3) { //清除背景介绍文本
                                        btn0.children[1].lastChild.remove();
                                    }

                                    if (btn.innerText.indexOf('+') >= 0) {
                                        ineq = 4;
                                    }
                                    else {
                                        let a = g_equipments[parseInt(btn.dataset.metaIndex)];
                                        if (a == null) {
                                            let e = equipmentInfoParseNode(btn);
                                            a = (e != null ? g_equipMap.get(e[0]) : null);
                                        }
                                        if ((ineq = (a?.type ?? 4)) < 4) {
                                            btn0.className += ' btn-equipment';
                                        }
                                    }

                                    (storeText == '' ? eqDivs[ineq].firstChild : eqDivs[ineq].firstChild.nextSibling).appendChild(btn0);
                                });

                                function inputAmuletGroupName(defaultGroupName) {
                                    let groupName = prompt('请输入护符组名称(不超过31个字符,请仅使用大、小写英文字母、数字、连字符、下划线及中文字符):',
                                                           defaultGroupName);
                                    if (amuletIsValidGroupName(groupName)) {
                                        return groupName;
                                    }
                                    else if (groupName != null) {
                                        alert('名称不符合命名规则,信息未保存。');
                                    }
                                    return null;
                                }

                                function refreshEquipmentPage(fnFurtherProcess) {
                                    let asyncOperations = 1;
                                    let asyncObserver = new MutationObserver(() => { asyncObserver.disconnect(); asyncOperations = 0; });
                                    asyncObserver.observe(document.getElementById('backpacks'), { childList : true , subtree : true });

                                    // refresh #carding & #backpacks
                                    cding();
                                    eqbp(1);

                                    let timer = setInterval(() => {
                                        if (asyncOperations == 0) {
                                            clearInterval(timer);
                                            genericPopupClose(true);
                                            if (fnFurtherProcess != null) {
                                                fnFurtherProcess();
                                            }
                                        }
                                    }, 200);
                                }

                                function queryAmulets(bag, store, key) {
                                    let count = 0;
                                    if (bag != null) {
                                        amuletNodesToArray(
                                            document.querySelectorAll(bagObjectsQueryString), bag, key);
                                        count += bag.length;
                                    }
                                    if (store != null) {
                                        amuletNodesToArray(
                                            document.querySelectorAll(storeObjectsQueryString), store, key);
                                        count += store.length;
                                    }
                                    return count;
                                }

                                function showAmuletGroupsPopup() {
                                    function beginSaveBagAsGroup(groupName, update) {
                                        let amulets = [];
                                        queryAmulets(amulets, null);
                                        createAmuletGroup(groupName, amulets, update);
                                        showAmuletGroupsPopup();
                                    }

                                    genericPopupClose(true);

                                    let bag = [];
                                    let store = [];
                                    if (queryAmulets(bag, store) == 0) {
                                        alert('护符信息加载异常,请检查!');
                                        refreshEquipmentPage(null);
                                        return;
                                    }

                                    let amulets = bag.concat(store);
                                    let bagGroup = amuletCreateGroupFromArray('当前背包', bag);
                                    let groups = amuletLoadGroups();
                                    if (bagGroup == null && groups.count() == 0) {
                                        alert('背包为空,且未找到预保存的护符组信息!');
                                        return;
                                    }

                                    genericPopupSetContent(
                                        '护符组管理',
                                        '<style> .group-menu { position:relative; display:inline-block; } ' +
                                                '.group-menu-items { display:none; ' +
                                                                    'position:absolute; ' +
                                                                    'font-size:15px; ' +
                                                                    'background-color:white; ' +
                                                                    'min-width:280px; ' +
                                                                    'box-shadow:0px 8px 16px 0px rgba(0, 0, 0, 0.2); ' +
                                                                    'padding:10px 20px; } '+
                                                '.group-menu-item { } ' +
                                                '.group-menu:hover .group-menu-items { display:block; } ' +
                                                '.group-menu-items .group-menu-item:hover { background-color:#bbddff; } ' +
                                        '</style>' +
                                        '<div id="popup_amulet_groups" style="margin-top:15px;"></div>');
                                    let amuletContainer = genericPopupQuerySelector('#popup_amulet_groups');
                                    let groupMenu = document.createElement('div');
                                    groupMenu.className = 'group-menu-items';
                                    groupMenu.style.zIndex = amuletContainer.style.zIndex + 1;

                                    if (bagGroup != null) {
                                        let err = !bagGroup.validate(bag);

                                        let groupDiv = document.createElement('div');
                                        groupDiv.className = g_genericPopupTopLineDivClass;
                                        groupDiv.id = 'popup_amulet_group_bag';
                                        groupDiv.innerHTML =
                                            `<b class="group-menu" id="popup_amulet_group_bag_name" style="color:${err ? "red" : "blue"};
                                             font-size:20px;cursor:pointer;">当前背包内容 [${bagGroup.count()}] ▼</b>`;

                                        let mitem = document.createElement('li');
                                        mitem.className = 'group-menu-item';
                                        mitem.innerHTML = '<a href="#popup_amulet_group_bag">当前背包内容</a>';
                                        groupMenu.appendChild(mitem);

                                        g_amuletTypeNames.slice().reverse().forEach((item) => {
                                            let btn = document.createElement('button');
                                            btn.innerText = '清空' + item;
                                            btn.style.float = 'right';
                                            btn.setAttribute('amulet-key', item);
                                            btn.onclick = clearSpecAmulet;
                                            groupDiv.appendChild(btn);
                                        });

                                        function clearSpecAmulet(e) {
                                            genericPopupShowProgressMessage('处理中,请稍候...');
                                            beginClearBag(
                                                document.querySelectorAll(bagObjectsQueryString),
                                                e.target.getAttribute('amulet-key'), refreshEquipmentPage, showAmuletGroupsPopup);
                                        }

                                        let saveBagGroupBtn = document.createElement('button');
                                        saveBagGroupBtn.innerText = '保存为护符组';
                                        saveBagGroupBtn.style.float = 'right';
                                        saveBagGroupBtn.onclick = (() => {
                                            let groupName = inputAmuletGroupName('');
                                            if (groupName != null) {
                                                beginSaveBagAsGroup(groupName, false);
                                            }
                                        });
                                        groupDiv.appendChild(saveBagGroupBtn);

                                        let groupInfoDiv = document.createElement('div');
                                        groupInfoDiv.innerHTML =
                                            `<hr><ul style="color:#000080;">${bagGroup.formatBuffSummary('<li>', '</li>', '', true)}</ul>
                                             <hr><ul>${bagGroup.formatItems('<li>', '<li style="color:red;">', '</li>', '</li>', '')}</ul>
                                             <hr><ul><li>AMULET ${bagGroup.formatBuffShortMark(' ', ' ', false)} ENDAMULET</li></ul>`;
                                        groupDiv.appendChild(groupInfoDiv);

                                        amuletContainer.appendChild(groupDiv);
                                    }

                                    let li = 0
                                    let groupArray = groups.toArray();
                                    let gl = (groupArray?.length ?? 0);
                                    if (gl > 0) {
                                        groupArray = groupArray.sort((a, b) => a.name < b.name ? -1 : 1);
                                        for (let i = 0; i < gl; i++) {
                                            let err = !groupArray[i].validate(amulets);

                                            let groupDiv = document.createElement('div');
                                            groupDiv.className = g_genericPopupTopLineDivClass;
                                            groupDiv.id = 'popup_amulet_group_' + i;
                                            groupDiv.innerHTML =
                                                `<b class="group-menu" id="popup_amulet_group_${i}_name" style="color:${err ? "red" : "blue"};
                                                 font-size:20px;">${groupArray[i].name} [${groupArray[i].count()}] ▼</b>`;

                                            let mitem = document.createElement('li');
                                            mitem.className = 'group-menu-item';
                                            mitem.innerHTML = `<a href="#popup_amulet_group_${i}">${groupArray[i].name}</a>`;
                                            groupMenu.appendChild(mitem);

                                            let amuletDeleteGroupBtn = document.createElement('button');
                                            amuletDeleteGroupBtn.innerText = '删除';
                                            amuletDeleteGroupBtn.style.float = 'right';
                                            amuletDeleteGroupBtn.onclick = (() => {
                                                let groupName = genericPopupQuerySelector(`#popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                if (confirm(`删除护符组 "${groupName}" 吗?`)) {
                                                    amuletDeleteGroup(groupName);
                                                    showAmuletGroupsPopup();
                                                }
                                            });
                                            groupDiv.appendChild(amuletDeleteGroupBtn);

                                            let amuletModifyGroupBtn = document.createElement('button');
                                            amuletModifyGroupBtn.innerText = '编辑';
                                            amuletModifyGroupBtn.style.float = 'right';
                                            amuletModifyGroupBtn.onclick = (() => {
                                                let groupName = genericPopupQuerySelector(`#popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                modifyAmuletGroup(groupName);
                                            });
                                            groupDiv.appendChild(amuletModifyGroupBtn);

                                            let importAmuletGroupBtn = document.createElement('button');
                                            importAmuletGroupBtn.innerText = '导入';
                                            importAmuletGroupBtn.style.float = 'right';
                                            importAmuletGroupBtn.onclick = (() => {
                                                let groupName = genericPopupQuerySelector(`#popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                let persistenceString = prompt('请输入护符组编码(一般由工具软件自动生成,表现形式为一组由逗号分隔的数字序列)');
                                                if (persistenceString != null) {
                                                    let group = new AmuletGroup(`${groupName}${AMULET_STORAGE_GROUPNAME_SEPARATOR}${persistenceString}`);
                                                    if (group.isValid()) {
                                                        let groups = amuletLoadGroups();
                                                        if (groups.add(group)) {
                                                            amuletSaveGroups(groups);
                                                            showAmuletGroupsPopup();
                                                        }
                                                        else {
                                                            alert('保存失败!');
                                                        }
                                                    }
                                                    else {
                                                        alert('输入的护符组编码无效,请检查!');
                                                    }
                                                }
                                            });
                                            groupDiv.appendChild(importAmuletGroupBtn);

                                            let renameAmuletGroupBtn = document.createElement('button');
                                            renameAmuletGroupBtn.innerText = '更名';
                                            renameAmuletGroupBtn.style.float = 'right';
                                            renameAmuletGroupBtn.onclick = (() => {
                                                let oldName = genericPopupQuerySelector(`#popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                let groupName = inputAmuletGroupName(oldName);
                                                if (groupName != null && groupName != oldName) {
                                                    let groups = amuletLoadGroups();
                                                    if (!groups.contains(groupName) || confirm(`护符组 "${groupName}" 已存在,要覆盖吗?`)) {
                                                        if (groups.rename(oldName, groupName)) {
                                                            amuletSaveGroups(groups);
                                                            showAmuletGroupsPopup();
                                                        }
                                                        else {
                                                            alert('更名失败!');
                                                        }
                                                    }
                                                }
                                            });
                                            groupDiv.appendChild(renameAmuletGroupBtn);

                                            let updateAmuletGroupBtn = document.createElement('button');
                                            updateAmuletGroupBtn.innerText = '更新';
                                            updateAmuletGroupBtn.style.float = 'right';
                                            updateAmuletGroupBtn.onclick = (() => {
                                                let groupName = genericPopupQuerySelector(`#popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                if (confirm(`用当前背包内容替换 "${groupName}" 护符组预定内容吗?`)) {
                                                    beginSaveBagAsGroup(groupName, true);
                                                }
                                            });
                                            groupDiv.appendChild(updateAmuletGroupBtn);

                                            let unamuletLoadGroupBtn = document.createElement('button');
                                            unamuletLoadGroupBtn.innerText = '入仓';
                                            unamuletLoadGroupBtn.style.float = 'right';
                                            unamuletLoadGroupBtn.onclick = (() => {
                                                let groupName = genericPopupQuerySelector(`#popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                genericPopupShowProgressMessage('卸载中,请稍候...');
                                                beginUnloadAmuletGroupFromBag(
                                                    document.querySelectorAll(bagObjectsQueryString),
                                                    groupName, refreshEquipmentPage, showAmuletGroupsPopup);
                                            });
                                            groupDiv.appendChild(unamuletLoadGroupBtn);

                                            let amuletLoadGroupBtn = document.createElement('button');
                                            amuletLoadGroupBtn.innerText = '装备';
                                            amuletLoadGroupBtn.style.float = 'right';
                                            amuletLoadGroupBtn.onclick = (() => {
                                                let groupName = genericPopupQuerySelector(`#popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                genericPopupShowProgressMessage('加载中,请稍候...');
                                                beginLoadAmuletGroupFromStore(
                                                    document.querySelectorAll(storeObjectsQueryString),
                                                    groupName, refreshEquipmentPage, showAmuletGroupsPopup);
                                            });
                                            groupDiv.appendChild(amuletLoadGroupBtn);

                                            let groupInfoDiv = document.createElement('div');
                                            groupInfoDiv.innerHTML =
                                                `<hr><ul style="color:#000080;">${groupArray[i].formatBuffSummary('<li>', '</li>', '', true)}</ul>
                                                 <hr><ul>${groupArray[i].formatItems('<li>', '<li style="color:red;">', '</li>', '</li>', '')}</ul>
                                                 <hr><ul><li>AMULET ${groupArray[i].formatBuffShortMark(' ', ' ', false)} ENDAMULET</li></ul>`;
                                            groupDiv.appendChild(groupInfoDiv);

                                            amuletContainer.appendChild(groupDiv);
                                            li += groupArray[i].getDisplayStringLineCount();
                                        }
                                    }

                                    genericPopupQuerySelectorAll('.group-menu')?.forEach((e) => {
                                        e.appendChild(groupMenu.cloneNode(true));
                                    });

                                    if (bagGroup != null) {
                                        gl++;
                                        li += bagGroup.getDisplayStringLineCount();
                                    }

                                    genericPopupAddButton('新建护符组', 0, modifyAmuletGroup, true);
                                    genericPopupAddButton(
                                        '导入新护符组',
                                        0,
                                        (() => {
                                            let groupName = inputAmuletGroupName('');
                                            if (groupName != null) {
                                                let persistenceString = prompt('请输入护符组编码(一般由工具软件自动生成,表现形式为一组由逗号分隔的数字序列)');
                                                if (persistenceString != null) {
                                                    let group = new AmuletGroup(`${groupName}${AMULET_STORAGE_GROUPNAME_SEPARATOR}${persistenceString}`);
                                                    if (group.isValid()) {
                                                        let groups = amuletLoadGroups();
                                                        if (!groups.contains(groupName) || confirm(`护符组 "${groupName}" 已存在,要覆盖吗?`)) {
                                                            if (groups.add(group)) {
                                                                amuletSaveGroups(groups);
                                                                showAmuletGroupsPopup();
                                                            }
                                                            else {
                                                                alert('保存失败!');
                                                            }
                                                        }
                                                    }
                                                    else {
                                                        alert('输入的护符组编码无效,请检查!');
                                                    }
                                                }
                                            }
                                        }),
                                        true);
                                    genericPopupAddButton(
                                        '清空背包',
                                        0,
                                        (() => {
                                            genericPopupShowProgressMessage('处理中,请稍候...');
                                            beginClearBag(document.querySelectorAll(bagObjectsQueryString),
                                                          null, refreshEquipmentPage, showAmuletGroupsPopup);
                                        }),
                                        true);
                                    genericPopupAddCloseButton(80);

                                    genericPopupSetContentSize(Math.min((li * 20) + (gl * 160) + 60, Math.max(window.innerHeight - 200, 400)),
                                                               Math.min(1000, Math.max(window.innerWidth - 100, 600)),
                                                               true);
                                    genericPopupShowModal(true);

                                    if (window.getSelection) {
                                        window.getSelection().removeAllRanges();
                                    }
                                    else if (document.getSelection) {
                                        document.getSelection().removeAllRanges();
                                    }
                                }

                                function modifyAmuletGroup(groupName) {
                                    function divHeightAdjustment(div) {
                                        div.style.height = (div.parentNode.offsetHeight - div.offsetTop - 3) + 'px';
                                    }

                                    function refreshAmuletList() {
                                        let type = amuletFilterList.value;
                                        amuletList.innerHTML = '';
                                        amulets.forEach((am) => {
                                            if (type == -1 || am.type == type) {
                                                let item = document.createElement('li');
                                                item.setAttribute('original-id', am.id);
                                                item.innerText = am.formatBuffText();
                                                amuletList.appendChild(item);
                                            }
                                        });
                                    }

                                    function refreshGroupAmuletSummary() {
                                        let count = group.count();
                                        if (count > 0) {
                                            groupSummary.innerHTML = group.formatBuffSummary('<li>', '</li>', '', true);
                                            groupSummary.style.display = 'block';
                                        }
                                        else {
                                            groupSummary.style.display = 'none';
                                            groupSummary.innerHTML = '';
                                        }
                                        divHeightAdjustment(groupAmuletList.parentNode);
                                        amuletCount.innerText = count;
                                    }

                                    function refreshGroupAmuletList() {
                                        groupAmuletList.innerHTML = '';
                                        group.items.forEach((am) => {
                                            if (am.id >= 0) {
                                                let item = document.createElement('li');
                                                item.setAttribute('original-id', am.id);
                                                item.innerText = am.formatBuffText();
                                                groupAmuletList.appendChild(item);
                                            }
                                        });
                                    }

                                    function refreshGroupAmuletDiv() {
                                        refreshGroupAmuletSummary();
                                        refreshGroupAmuletList();
                                    }

                                    function moveAmuletItem(e) {
                                        let li = e.target;
                                        if (li.tagName == 'LI') {
                                            let from = li.parentNode;
                                            let id = li.getAttribute('original-id');
                                            from.removeChild(li);
                                            if (from == amuletList) {
                                                let i = searchElement(amulets, id, (a, b) => a - b.id);
                                                let am = amulets[i];
                                                amulets.splice(i, 1);
                                                groupAmuletList.insertBefore(li, groupAmuletList.children.item(group.add(am)));
                                            }
                                            else {
                                                let am = group.removeId(id);
                                                insertElement(amulets, am, (a, b) => a.id - b.id);
                                                let type = amuletFilterList.value;
                                                if (type < 0 || am.type == type) {
                                                    for (var item = amuletList.firstChild;
                                                         parseInt(item?.getAttribute('original-id')) <= am.id;
                                                         item = item.nextSibling);
                                                    amuletList.insertBefore(li, item);
                                                }
                                            }
                                            refreshGroupAmuletSummary();
                                            groupChanged = true;
                                        }
                                    }

                                    let bag = [];
                                    let store = [];
                                    if (queryAmulets(bag, store) == 0) {
                                        alert('获取护符信息失败,请检查!');
                                        return;
                                    }
                                    let amulets = bag.concat(store).sort((a, b) => a.compareTo(b));
                                    amulets.forEach((item, index) => { item.id = index; });

                                    let displayName = groupName;
                                    if (!amuletIsValidGroupName(displayName)) {
                                        displayName = '(未命名)';
                                        groupName = null;
                                    }
                                    else if (displayName.length > 20) {
                                        displayName = displayName.slice(0, 19) + '...';
                                    }

                                    let groupChanged = false;
                                    let group = amuletLoadGroup(groupName);
                                    if (!group?.isValid()) {
                                        group = new AmuletGroup(null);
                                        group.name = '(未命名)';
                                        groupName = null;
                                    }
                                    else {
                                        group.validate(amulets);
                                        while (group.removeId(-1) != null) {
                                            groupChanged = true;
                                        }
                                        group.items.forEach((am) => {
                                            let i = searchElement(amulets, am, (a, b) => a.id - b.id);
                                            if (i >= 0) {
                                                amulets.splice(i, 1);
                                            }
                                        });
                                    }

                                    genericPopupClose(true);

                                    let fixedContent =
                                        '<div style="padding:20px 0px 5px 0px;font-size:18px;color:blue;"><b>' +
                                        '<span>双击护符条目以进行添加或移除操作</span><span style="float:right;">共 ' +
                                        '<span id="amulet_count" style="color:#800020;">0</span> 个护符</span></b></div>';
                                    let mainContent =
                                        '<style> ul > li:hover { background-color:#bbddff; } </style>' +
                                        '<div style="display:block;height:100%;width:100%;">' +
                                          '<div style="position:relative;display:block;float:left;height:96%;width:49%;' +
                                               'margin-top:10px;border:1px solid #000000;">' +
                                            '<div style="display:block;width:100%;padding:10px 10px;border-bottom:2px groove #d0d0d0;' +
                                                 'margin-bottom:10px;">' +
                                              '<select id="amulet_filter" style="display:inline;width:100%;color:blue;text-align:center;">' +
                                              '</select>' +
                                            '</div>' +
                                            '<div style="position:absolute;display:block;height:1px;width:100%;overflow:scroll;">' +
                                              '<ul id="amulet_list" style="cursor:pointer;"></ul>' +
                                            '</div>' +
                                          '</div>' +
                                          '<div style="position:relative;display:block;float:right;height:96%;width:49%;' +
                                               'margin-top:10px;border:1px solid #000000;">' +
                                            '<div id="group_summary" style="display:block;width:100%;padding:10px 5px;' +
                                                 'border-bottom:2px groove #d0d0d0;color:#000080;margin-bottom:10px;"></div>' +
                                            '<div style="position:absolute;display:block;height:1px;width:100%;overflow:scroll;">' +
                                              '<ul id="group_amulet_list" style="cursor:pointer;"></ul>' +
                                            '</div>' +
                                          '</div>' +
                                        '</div>';

                                    genericPopupSetFixedContent(fixedContent);
                                    genericPopupSetContent('编辑护符组 - ' + displayName, mainContent);

                                    let amuletCount = genericPopupQuerySelector('#amulet_count');
                                    let amuletFilterList = genericPopupQuerySelector('#amulet_filter');
                                    let amuletList = genericPopupQuerySelector('#amulet_list');
                                    let groupSummary = genericPopupQuerySelector('#group_summary');
                                    let groupAmuletList = genericPopupQuerySelector('#group_amulet_list');

                                    let op = document.createElement('option');
                                    op.value = -1;
                                    op.innerText = '全部护符类型';
                                    amuletFilterList.appendChild(op);
                                    for (let amuletType in g_amuletTypeIds) {
                                        op = document.createElement('option');
                                        op.value = g_amuletTypeIds[amuletType];
                                        op.innerText = amuletType;
                                        amuletFilterList.appendChild(op);
                                    }

                                    refreshAmuletList();
                                    refreshGroupAmuletDiv();

                                    amuletFilterList.onchange = refreshAmuletList;
                                    amuletList.ondblclick = groupAmuletList.ondblclick = moveAmuletItem;

                                    genericPopupAddButton(
                                        '清空护符组',
                                        0,
                                        (() => {
                                            if (group.count() > 0) {
                                                group.items.forEach((am) => { insertElement(amulets, am, (a, b) => a.id - b.id); });
                                                group.clear();

                                                refreshAmuletList();
                                                refreshGroupAmuletDiv();

                                                groupChanged = true;
                                            }
                                        }),
                                        true);

                                    if (amuletIsValidGroupName(groupName)) {
                                        genericPopupAddButton(
                                            '另存为',
                                            80,
                                            (() => {
                                                if (!group.isValid()) {
                                                    alert('护符组内容存在错误,请检查!');
                                                    return;
                                                }

                                                let gn = inputAmuletGroupName(groupName);
                                                if (gn == null) {
                                                    return;
                                                }

                                                let groups = amuletLoadGroups();
                                                if (groups.contains(gn) && !confirm(`护符组 "${gn}" 已存在,要覆盖吗?`)) {
                                                    return;
                                                }

                                                group.name = gn;
                                                if (groups.add(group)) {
                                                    amuletSaveGroups(groups);
                                                    showAmuletGroupsPopup();
                                                }
                                                else {
                                                    alert('保存失败!');
                                                }
                                            }),
                                            false);
                                    }

                                    genericPopupAddButton(
                                        '确认',
                                        80,
                                        (() => {
                                            if (!groupChanged && group.isValid()) {
                                                showAmuletGroupsPopup();
                                                return;
                                            }
                                            else if (!group.isValid()) {
                                                alert('护符组内容存在错误,请检查!');
                                                return;
                                            }

                                            let groups = amuletLoadGroups();
                                            if (!amuletIsValidGroupName(groupName)) {
                                                let gn = inputAmuletGroupName(displayName);
                                                if (gn == null || (groups.contains(gn) && !confirm(`护符组 "${gn}" 已存在,要覆盖吗?`))) {
                                                    return;
                                                }
                                                group.name = gn;
                                            }

                                            if (groups.add(group)) {
                                                amuletSaveGroups(groups);
                                                showAmuletGroupsPopup();
                                            }
                                            else {
                                                alert('保存失败!');
                                            }
                                        }),
                                        false);

                                    genericPopupAddButton(
                                        '取消',
                                        80,
                                        (() => {
                                            if (!groupChanged || confirm('护符组内容已修改,不保存吗?')) {
                                                showAmuletGroupsPopup();
                                            }
                                        }),
                                        false);

                                    genericPopupSetContentSize(Math.min(800, Math.max(window.innerHeight - 200, 500)),
                                                               Math.min(1000, Math.max(window.innerWidth - 100, 600)),
                                                               false);
                                    genericPopupShowModal(false);

                                    divHeightAdjustment(amuletList.parentNode);
                                    divHeightAdjustment(groupAmuletList.parentNode);
                                }

                                function createAmuletGroup(groupName, amulets, update) {
                                    let group = amuletCreateGroupFromArray(groupName, amulets);
                                    if (group != null) {
                                        let groups = amuletLoadGroups();
                                        if (update || !groups.contains(groupName) || confirm(`护符组 "${groupName}" 已存在,要覆盖吗?`)) {
                                            if (groups.add(group)) {
                                                amuletSaveGroups(groups);
                                                genericPopupClose(true);
                                                return true;
                                            }
                                            else {
                                                alert('保存失败!');
                                            }
                                        }
                                    }
                                    else {
                                        alert('保存异常,请检查!');
                                    }
                                    genericPopupClose(true);
                                    return false;
                                }

                                function formatAmuletsString() {
                                    let bag = [];
                                    let store = [];
                                    let exportLines = [];
                                    if (queryAmulets(bag, store) > 0) {
                                        let amulets = bag.concat(store).sort((a, b) => a.compareTo(b));
                                        let amuletIndex = 1;
                                        amulets.forEach((am) => {
                                            exportLines.push(`${('00' + amuletIndex).slice(-3)} - ${am.formatShortMark()}`);
                                            amuletIndex++;
                                        });
                                    }
                                    return (exportLines.length > 0 ? exportLines.join('\n') : '');
                                }

                                function exportAmulets() {
                                    genericPopupSetContent(
                                        '护符导出',
                                        `<b><div id="amulet_export_tip" style="color:#0000c0;padding:15px 0px 10px;">
                                         请勿修改任何导出内容,将其保存为纯文本在其它相应工具中使用</div></b>
                                         <div style="height:330px;"><textarea id="amulet_persistence_string" readonly="true"
                                         style="height:100%;width:100%;resize:none;"></textarea></div>`);

                                    genericPopupAddButton(
                                        '复制导出内容至剪贴板',
                                        0,
                                        ((e) => {
                                            e.target.disabled = 'disabled';
                                            let tipContainer = genericPopupQuerySelector('#amulet_export_tip');
                                            let tipColor = tipContainer.style.color;
                                            let tipString = tipContainer.innerText;
                                            tipContainer.style.color = '#ff0000';
                                            genericPopupQuerySelector('#amulet_persistence_string').select();
                                            if (document.execCommand('copy')) {
                                                tipContainer.innerText = '导出内容已复制到剪贴板';
                                            }
                                            else {
                                                tipContainer.innerText = '复制失败,这可能是因为浏览器没有剪贴板访问权限,请进行手工复制(CTRL+A, CTRL+C)';
                                            }
                                            setTimeout((() => {
                                                tipContainer.style.color = tipColor;
                                                tipContainer.innerText = tipString;
                                                e.target.disabled = '';
                                            }), 3000);
                                        }),
                                        true);
                                    genericPopupAddCloseButton(80);

                                    genericPopupQuerySelector('#amulet_persistence_string').value = formatAmuletsString();

                                    genericPopupSetContentSize(400, 600, false);
                                    genericPopupShowModal(true);
                                }

                                let amuletButtonsGroupContainer = document.getElementById('amulet_management_btn_group');
                                if (amuletButtonsGroupContainer == null) {
                                    amuletButtonsGroupContainer = document.createElement('div');
                                    amuletButtonsGroupContainer.id = 'amulet_management_btn_group';
                                    amuletButtonsGroupContainer.style.width = '100px';
                                    amuletButtonsGroupContainer.style.float = 'right';
                                    document.getElementById('backpacks').children[0].appendChild(amuletButtonsGroupContainer);

                                    let exportAmuletsBtn = document.createElement('button');
                                    exportAmuletsBtn.innerText = '导出护符';
                                    exportAmuletsBtn.style.width = '100%';
                                    exportAmuletsBtn.style.marginBottom = '1px';
                                    exportAmuletsBtn.onclick = (() => {
                                        exportAmulets();
                                    });
                                    amuletButtonsGroupContainer.appendChild(exportAmuletsBtn);

                                    let beginClearBagBtn = document.createElement('button');
                                    beginClearBagBtn.innerText = '清空背包';
                                    beginClearBagBtn.style.width = '100%';
                                    beginClearBagBtn.style.marginBottom = '1px';
                                    beginClearBagBtn.onclick = (() => {
                                        genericPopupShowProgressMessage('处理中,请稍候...');
                                        beginClearBag(
                                            document.querySelectorAll(bagObjectsQueryString),
                                            null, refreshEquipmentPage, null);
                                    });
                                    amuletButtonsGroupContainer.appendChild(beginClearBagBtn);

                                    let amuletSaveGroupBtn = document.createElement('button');
                                    amuletSaveGroupBtn.innerText = '存为护符组';
                                    amuletSaveGroupBtn.style.width = '100%';
                                    amuletSaveGroupBtn.style.marginBottom = '1px';
                                    amuletSaveGroupBtn.onclick = (() => {
                                        let groupName = inputAmuletGroupName('');
                                        if (groupName != null) {
                                            let amulets = [];
                                            if (queryAmulets(amulets, null) == 0) {
                                                alert('保存失败,请检查背包内容!');
                                            }
                                            else if (createAmuletGroup(groupName, amulets, false)) {
                                                alert('保存成功。');
                                            }
                                        }
                                    });
                                    amuletButtonsGroupContainer.appendChild(amuletSaveGroupBtn);

                                    let manageAmuletGroupBtn = document.createElement('button');
                                    manageAmuletGroupBtn.innerText = '管理护符组';
                                    manageAmuletGroupBtn.style.width = '100%';
                                    manageAmuletGroupBtn.style.marginBottom = '1px';
                                    manageAmuletGroupBtn.onclick = (() => {
                                        genericPopupInitialize();
                                        showAmuletGroupsPopup();
                                    });
                                    amuletButtonsGroupContainer.appendChild(manageAmuletGroupBtn);

                                    let clearAmuletGroupBtn = document.createElement('button');
                                    clearAmuletGroupBtn.innerText = '清除护符组';
                                    clearAmuletGroupBtn.style.width = '100%';
                                    clearAmuletGroupBtn.onclick = (() => {
                                        if (confirm('要删除全部已保存的护符组信息吗?')) {
                                            amuletClearGroups();
                                            alert('已删除全部预定义护符组信息。');
                                        }
                                    });
                                    amuletButtonsGroupContainer.appendChild(clearAmuletGroupBtn);

                                    document.getElementById(storeButtonId).onclick = (() => {
                                        if ($(storeQueryString).css('display') == 'none') {
                                            $(storeQueryString).show();
                                        } else {
                                            $(storeQueryString).hide();
                                        }
                                        backupEquipmentDivState({ target : document.getElementById(storeButtonId) });
                                    });
                                }

                                $('#equipmentDiv .btn-equipment .bg-danger.with-padding').css({
                                    'max-width': '200px',
                                    'padding': '5px 5px 5px 5px',
                                    'white-space': 'pre-line',
                                    'word-break': 'break-all'
                                });

                                collapseEquipmentDiv(equipmentExpand, forceEquipDivOperation);
                                changeEquipmentDivStyle(equipmentBG);

                                forceEquipDivOperation = false;
                            }
                        }
                    }, 500);
                }

                const g_bindingPopupLinkId = 'binding_popup_link';
                const g_cardOnekeyLinkId = 'card_one_key_link';
                const g_bindingSolutionId = 'binding_solution_div';
                const g_bindingListSelectorId = 'binding_list_selector';
                const g_equipOnekeyLinkId = 'equip_one_key_link';

                function switchCardTemporarily(roleId) {
                    let role = g_roleMap.get(roleId);
                    if (role == undefined) {
                        return;
                    }

                    genericPopupInitialize();
                    genericPopupShowProgressMessage('正在切换,请稍候...');

                    const upcard_data = getPostData(/upcard\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/).slice(7, -1);
                    const halosave_data = getPostData(/halosave\(\)\{[\s\S]*\}/m, /data: ".*\+savearr\+.*"/).slice(7, -1);

                    let roleInfo = [];
                    let haloInfo = [];
                    beginReadRoleAndHalo(roleInfo, haloInfo, switchToTempCard, null);

                    function switchCardCompletion() {
                        genericPopupClose(true);
                        window.location.reload();
                    }

                    function switchToTempCard() {
                        if (roleInfo.length == 2 && haloInfo.length == 3) {
                            const infoHTML =
                                  `<div style="display:block;width:100%;color:#0000c0;text-align:center;font-size:20px;padding-top:50px;"><b>
                                   <p></p><span style="width:100%;">当前卡片已经由 [ ${roleInfo[1]} ] 临时切换至 [ ${g_roleMap.get(roleId)?.name ?? 'UNKNOW'} ]</span><br><br>
                                   <p></p><span style="width:100%;">请切换至搜刮页面尽快完成搜刮操作</span><br><br>
                                   <p></p><span style="width:100%;">并返回本页面点击“恢复”按钮以恢复之前的卡片和光环设置</span></b></div>`;
                            genericPopupSetContent(`临时装备卡片 [ ${g_roleMap.get(roleId)?.name ?? 'UNKNOW'} ]`, infoHTML);
                            genericPopupSetContentSize(300, 600, false);
                            genericPopupAddButton('恢复', 80, restoreCardAndHalo, false);

                            switchCard(roleId, null, genericPopupShowModal, false);
                        }
                        else {
                            alert('无法读取当前装备卡片和光环信息,卡片未切换!');
                            switchCardCompletion();
                        }
                    }

                    function restoreCardAndHalo() {
                        genericPopupShowProgressMessage('正在恢复,请稍候...');
                        switchCard(roleInfo[0], haloInfo[2], switchCardCompletion, null);
                    }

                    function switchCard(newRoleId, newHaloArray, fnFurtherProcess, fnParams) {
                        let cardData = upcard_data.replace('"+id+"', newRoleId);
                        GM_xmlhttpRequest({
                            method: g_postMethod,
                            url: g_postUrl,
                            headers: g_postHeader,
                            data: cardData,
                            onload: response => {
                                if (response.responseText == 'ok' || response.responseText == '你没有这张卡片或已经装备中') {
                                    if (newHaloArray?.length > 0) {
                                        let haloData = halosave_data.replace('"+savearr+"', newHaloArray.join());
                                        GM_xmlhttpRequest({
                                            method: g_postMethod,
                                            url: g_postUrl,
                                            headers: g_postHeader,
                                            data: haloData,
                                            onload: response => {
                                                if (fnFurtherProcess != null) {
                                                    fnFurtherProcess(fnParams);
                                                }
                                            }
                                        });
                                        return;
                                    }
                                }
                                else {
                                    alert('无法完成卡片和光环切换,请尝试手动进行!');
                                    switchCardCompletion();
                                    return;
                                }
                                if (fnFurtherProcess != null) {
                                    fnFurtherProcess(fnParams);
                                }
                            }
                        });
                    }
                }

                function equipOnekey() {
                    const upcard_data = getPostData(/upcard\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/).slice(7, -1);
                    const halosave_data = getPostData(/halosave\(\)\{[\s\S]*\}/m, /data: ".*\+savearr\+.*"/).slice(7, -1);
                    const puton_data = getPostData(/puton\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/).slice(7, -1);

                    function roleSetupCompletion() {
                        httpRequestClearAll();
                        genericPopupClose(true);
                        window.location.reload();
                    }

                    function checkForRoleSetupCompletion() {
                        if (genericPopupTaskCheckCompletion()) {
                            // delay for the final state can be seen
                            genericPopupTaskSetState(0);
                            genericPopupTaskSetState(1);
                            genericPopupTaskSetState(2);
                            genericPopupTaskSetState(3);
                            setTimeout(roleSetupCompletion, 200);
                        }
                    }

                    function amuletLoadCompletion() {
                        genericPopupTaskComplete(3);
                        checkForRoleSetupCompletion();
                    }

                    let scheduledObjects = { equips : [] , amulets : [] , exchanged : [] };
                    function beginBagRestore() {
                        if (scheduledObjects.equips.length == 0 && scheduledObjects.amulets.length == 0) {
                            amuletLoadCompletion();
                        }
                        else {
                            beginRestoreObjects(null, scheduledObjects.amulets, scheduledObjects.equips, amuletLoadCompletion, null);
                        }
                    }

                    function beginUnloadExchangedEquipments(bag) {
                        beginMoveObjects(findEquipments(bag.pop(), scheduledObjects.exchanged, true),
                                         g_object_move_path.bag2store,
                                         beginBagRestore,
                                         null);
                    }

                    function beginAmuletLoadGroups() {
                        if (amuletGroupsToLoad?.length > 0) {
                            genericPopupTaskSetState(2);
                            genericPopupTaskSetState(3, `- 加载护符...(${amuletGroupsToLoad?.length})`);
                            beginLoadAmuletGroupFromStore(null, amuletGroupsToLoad.shift(), beginAmuletLoadGroups, null);
                        }
                        else {
                            amuletLoadCompletion();
                        }
                    }

                    function beginLoadAmulets() {
                        genericPopupTaskSetState(2);
                        genericPopupTaskComplete(2, equipmentOperationError > 0);

                        if (amuletGroupsToLoad != null) {
                            genericPopupTaskSetState(2, '- 清理装备...');
                            beginClearBag(null, null, beginAmuletLoadGroups, null);
                        }
                        else {
                            genericPopupTaskSetState(2, '- 恢复背包...');
                            if (scheduledObjects.exchanged.length > 0) {
                                beginReadObjects(originalBag = [], null, beginUnloadExchangedEquipments, originalBag);
                            }
                            else {
                                beginBagRestore();
                            }
                        }
                    }

                    let equipmentOperationError = 0;
                    let putonRequestsCount;
                    function putonEquipments(objects, fnFurtherProcess, fnParams) {
                        if (objects?.length > 0) {
                            let ids = [];
                            while (ids.length < g_maxConcurrentRequests && objects.length > 0) {
                                ids.push(objects.pop());
                            }
                            if ((putonRequestsCount = ids.length) > 0) {
                                while (ids.length > 0) {
                                    let request = GM_xmlhttpRequest({
                                        method: g_postMethod,
                                        url: g_postUrl,
                                        headers: g_postHeader,
                                        data: puton_data.replace('"+id+"', ids.shift()),
                                        onload: response => {
                                            if (response.responseText != 'ok') {
                                                equipmentOperationError++;
                                                console.log(response.responseText);
                                            }
                                            if (--putonRequestsCount == 0) {
                                                putonEquipments(objects, fnFurtherProcess, fnParams);
                                            }
                                        }
                                    });
                                    httpRequestRegister(request);
                                }
                                return;
                            }
                        }
                        if (fnFurtherProcess != null) {
                            fnFurtherProcess(fnParams);
                        }
                    }

                    function findEquipments(container, equipStrings, findId) {
                        let equips = [];
                        if (equipStrings.length > 0) {
                            let eqs = equipmentNodesToInfoArray(container);
                            for (let i = eqs.length - 1; i >= 0 && equipStrings.length > 0; i--) {
                                let idx = equipStrings.indexOf(eqs[i].slice(0, -1).join(','));
                                if (idx >= 0) {
                                    equipStrings.splice(idx, 1);
                                    equips.unshift(findId ? eqs[i][7] : eqs[i]);
                                }
                            }
                        }
                        return equips;
                    }

                    let originalBag, originalStore;
                    let currentEquipments = equipmentNodesToInfoArray(document.querySelectorAll(cardingObjectsQueryString));
                    function beginPutonEquipments(bindInfo) {
                        genericPopupTaskSetState(2, '- 检查装备...');
                        let equipsToPuton = [];
                        for (let i = 0; i < 4; i++) {
                            if (bindInfo[i] != currentEquipments[i].slice(0, -1).join(',')) {
                                equipsToPuton.push(bindInfo[i]);
                            }
                        }
                        if (equipsToPuton.length == 0) {
                            beginLoadAmulets();
                        }
                        else if (!(originalBag?.length > 0)) {
                            beginReadObjects(originalBag = [], originalStore = [], scheduleEquipments, null);
                        }

                        function scheduleEquipments() {
                            originalBag = originalBag.pop();
                            originalStore = originalStore.pop();

                            function rescheduleEquipments() {
                                genericPopupTaskSetState(2, '- 检查装备...');
                                beginReadObjects(originalBag = [], originalStore = [], scheduleEquipments, null);
                            }

                            let eqs = equipsToPuton.slice();
                            let eqsInBag = findEquipments(originalBag, eqs, true);
                            if (eqsInBag.length == equipsToPuton.length) {
                                genericPopupTaskSetState(2, `- 穿戴装备...(${eqsInBag.length})`);
                                putonEquipments(eqsInBag, beginLoadAmulets, null);
                                return;
                            }

                            let eqsInStore = findEquipments(originalStore, eqs, false);
                            if (eqs.length > 0) {
                                console.log(eqs);
                                alert('有装备不存在,请重新检查绑定!');
                                window.location.reload();
                                return;
                            }

                            let ids = [];
                            let freeCellsNeeded = eqsInStore.length;
                            for (let i = originalBag.length - 1; i >= 0; i--) {
                                if (objectIsEmptyNode(originalBag[i])) {
                                    if (--freeCellsNeeded == 0) {
                                        genericPopupTaskSetState(2, `- 取出仓库...(${eqsInStore.length})`);
                                        for (let j = eqsInStore.length - 1; j >= 0; j--) {
                                            ids.unshift(eqsInStore[j][7]);
                                            scheduledObjects.exchanged.push(
                                                currentEquipments[g_equipMap.get(eqsInStore[j][0]).type].slice(0, -1).join(','));
                                        }
                                        beginMoveObjects(ids, g_object_move_path.store2bag, rescheduleEquipments, null);
                                        return;
                                    }
                                }
                                else {
                                    let e = equipmentInfoParseNode(originalBag[i])
                                    if (e != null) {
                                        scheduledObjects.equips.push(e);
                                        ids.unshift(parseInt(e[7]));
                                    }
                                    else if ((e = (new Amulet()).fromNode(originalBag[i]))?.isValid()) {
                                        scheduledObjects.amulets.push(e);
                                        ids.unshift(e.id);
                                    }
                                    else {
                                        continue;
                                    }
                                    if (--freeCellsNeeded == 0) {
                                        genericPopupTaskSetState(2, `- 调整空间...(${ids.length})`);
                                        beginMoveObjects(ids, g_object_move_path.bag2store, rescheduleEquipments, null);
                                        return;
                                    }
                                }
                            }
                        }
                    }

                    function beginSetupHalo(bindInfo) {
                        let halo = [];
                        if (bindInfo.length > 4) {
                            bindInfo[4].split(',').forEach((item) => {
                                let hid = g_haloMap.get(item.trim())?.id;
                                if (hid > 0) {
                                    halo.push(hid);
                                }
                            });
                            if ((halo = halo.join(','))?.length > 0) {
                                genericPopupTaskSetState(1, '- 设置光环...');
                                let request = GM_xmlhttpRequest({
                                    method: g_postMethod,
                                    url: g_postUrl,
                                    headers: g_postHeader,
                                    data: halosave_data.replace('"+savearr+"', halo),
                                    onload: response => {
                                        genericPopupTaskSetState(1);
                                        genericPopupTaskComplete(1, response.responseText != 'ok');
                                        checkForRoleSetupCompletion();
                                    }
                                });
                                httpRequestRegister(request);
                                return;
                            }
                        }
                        genericPopupTaskComplete(1);
                        checkForRoleSetupCompletion();
                    }

                    let amuletGroupsToLoad = null;
                    function beginRoleSetup(bindInfo) {
                        beginSetupHalo(bindInfo);

                        if (bindInfo[5]?.length > 0) {
                            amuletGroupsToLoad = bindInfo[5].split(',');
                            genericPopupTaskSetState(2, '- 清理背包...');
                            beginClearBag(null, null, beginPutonEquipments, bindInfo);
                        }
                        else {
                            beginPutonEquipments(bindInfo);
                        }
                    }

                    let bindingElements = document.getElementById(g_bindingListSelectorId)?.value?.split(BINDING_NAME_SEPARATOR);
                    if (bindingElements?.length == 2) {
                        function equipOnekeyQuit() {
                            httpRequestAbortAll();
                            roleSetupCompletion();
                        }

                        genericPopupInitialize();
                        genericPopupTaskListPopupSetup('切换中...', 300, [ '卡片', '光环', '装备', '护符' ], equipOnekeyQuit);
                        genericPopupShowModal(false);

                        let roleId = g_roleMap.get(bindingElements[0].trim()).id;
                        let bindInfo = bindingElements[1].trim().split(BINDING_ELEMENT_SEPARATOR)
                        if (roleId == g_roleMap.get(document.getElementById('carding')
                                                           ?.querySelector('div.text-info.fyg_f24.fyg_lh60')
                                                           ?.children[0]?.innerText)?.id) {
                            genericPopupTaskComplete(0);
                            beginRoleSetup(bindInfo);
                        }
                        else {
                            genericPopupTaskSetState(0, '- 装备中...');
                            GM_xmlhttpRequest({
                                method: g_postMethod,
                                url: g_postUrl,
                                headers: g_postHeader,
                                data: upcard_data.replace('"+id+"', roleId),
                                onload: response => {
                                    genericPopupTaskSetState(0);
                                    if (response.responseText == 'ok' || response.responseText == '你没有这张卡片或已经装备中') {
                                        genericPopupTaskComplete(0);
                                        beginRoleSetup(bindInfo);
                                    }
                                    else {
                                        genericPopupTaskComplete(0, true);
                                        alert('卡片装备失败!');
                                        equipOnekeyQuit();
                                    }
                                }
                            });
                        }
                    }
                    else {
                        alert('绑定信息读取失败,无法装备!');
                    }
                }

                const BINDING_NAME_DEFAULT = '(未命名)';
                const BINDING_SEPARATOR = ';';
                const BINDING_NAME_SEPARATOR = '=';
                const BINDING_ELEMENT_SEPARATOR = '|';

                function showBindingPopup() {
                    let roleId = g_roleMap.get(document.querySelector('#backpacks > div.row > div.col-md-3 > span.text-info.fyg_f24')
                                                      ?.innerText)?.id;
                    let cardInfos = document.querySelector('#backpacks').querySelectorAll('.icon.icon-angle-down.text-primary');
                    let roleLv = cardInfos[0].innerText.match(/\d+/)[0];
                    let roleQl = cardInfos[1].innerText.match(/\d+/)[0];
                    let roleHs = cardInfos[2].innerText.match(/\d+/)[0];
                    let roleGv = (cardInfos[3]?.innerText.match(/\d+/)[0] ?? '0');
                    let rolePt = [];
                    for (let i = 1; i <= 6; i++) {
                        rolePt.push(document.getElementById('sjj' + i).innerText);
                    }
                    if (roleId == undefined || roleLv == undefined || roleQl == undefined || roleHs == undefined) {
                        alert('读取卡片信息失败,无法执行绑定操作!');
                        return;
                    }

                    let bind_info = null;
                    let udata = loadUserConfigData();
                    if (udata.dataBind[roleId] != null) {
                        bind_info = udata.dataBind[roleId];
                    }

                    genericPopupInitialize();
                    genericPopupShowProgressMessage('读取中,请稍候...');

                    const highlightBackgroundColor = '#80c0f0';
                    const fixedContent =
                        `<div style="width:100%;color:#0000ff;padding:20px 10px 5px 0px;"><b>绑定方案名称` +
                        `(不超过31个字符,请仅使用大、小写英文字母、数字、连字符、下划线及中文字符):` +
                        `<span id="${g_genericPopupInformationTipsId}" style="float:right;color:red;"></span></b></div>
                         <div style="width:100%;padding:0px 10px 20px 0px;"><input type="text" id="binding_name" maxlength="31"
                              list="binding_list" style="display:inline-block;width:100%;"></input>
                         <datalist id="binding_list"></datalist></div>`;
                    const mainContent =
                        `<style> .equipment_label    { display:inline-block; width:15%; }
                                 .equipment_selector { display:inline-block; width:84%; color:#145ccd; float:right; }
                                 div > li { cursor:pointer; } div > li:hover { background-color:#bbddff; } </style>
                         <div class="${g_genericPopupTopLineDivClass}" id="role_export_div" style="display:none;">
                         <div style="height:160px;">
                              <textarea id="role_export_string" readonly="true" style="height:100%;width:100%;resize:none;"></textarea></div>
                         <div style="padding:10px 0px 20px 0px;">
                              <button type="button" style="float:right;margin-left:1px;" id="hide_export_div">隐藏</button>
                              <button type="button" style="float:right;" id="copy_export_string">复制导出内容至剪贴板</button></div></div>
                         <div class="${g_genericPopupTopLineDivClass}">
                             <span class="equipment_label">武器装备:</span><select class="equipment_selector"></select><br><br>
                             <span class="equipment_label">手臂装备:</span><select class="equipment_selector"></select><br><br>
                             <span class="equipment_label">身体装备:</span><select class="equipment_selector"></select><br><br>
                             <span class="equipment_label">头部装备:</span><select class="equipment_selector"></select></div>
                         <div class="${g_genericPopupTopLineDivClass}" style="display:flex;position:relative;"><div id="halo_selector"></div></div>
                         <div class="${g_genericPopupTopLineDivClass}" id="amulet_selector" style="display:block;"><div></div></div>`;

                    genericPopupSetFixedContent(fixedContent);
                    genericPopupSetContent(`${g_roleMap.get(roleId)?.name ?? 'UNKNOW'} - ${roleLv} 级`, mainContent);

                    let eq_selectors = genericPopupQuerySelectorAll('select.equipment_selector');
                    let asyncOperations = 3;
                    let haloMax = 0;
                    let haloGroupItemMax = 0;

                    let bag, store;
                    beginReadObjects(
                        bag = [],
                        store = [],
                        () => {
                            let equipment = equipmentNodesToInfoArray(bag.pop());
                            equipmentNodesToInfoArray(store.pop(), equipment);
                            equipmentNodesToInfoArray(document.querySelectorAll(cardingObjectsQueryString), equipment);

                            equipment.sort((e1, e2) => {
                                if (e1[0] != e2[0]) {
                                    return (g_equipMap.get(e1[0]).index - g_equipMap.get(e2[0]).index);
                                }
                                return -equipmentInfoComparer(e1, e2);
                            });

                            equipment.forEach((item) => {
                                let eqMeta = g_equipMap.get(item[0]);
                                let lv = equipmentGetLevel(item);
                                let op0 = document.createElement('option');
                                op0.style.backgroundColor = g_equipmentLevelBGColor[lv];
                                op0.innerText =
                                    `${eqMeta.name} Lv.${item[1]} - ${item[2]}% ${item[3]}% ` +
                                    `${item[4]}% ${item[5]}% ${item[6] == 1 ? ' - [ 神秘 ]' : ''}`;
                                op0.title =
                                    `Lv.${item[1]} - ${item[6] == 1 ? '神秘' : ''}${g_equipmentLevelName[lv]}装备\n` +
                                    `${formatEquipmentAttributes(item, '\n')}`;
                                op0.value = item.slice(0, -1).join(',');
                                eq_selectors[eqMeta.type].appendChild(op0);
                            });

                            eq_selectors.forEach((eqs) => {
                                eqs.onchange = equipSelectionChange;
                                equipSelectionChange({ target : eqs });
                            });
                            function equipSelectionChange(e) {
                                for (var op = e.target.firstChild; op != null && op.value != e.target.value; op = op.nextSibling);
                                e.target.title = (op?.title ?? '');
                                e.target.style.backgroundColor = (op?.style.backgroundColor ?? 'white');
                            }
                            asyncOperations--;
                        },
                        null);

                    let currentHalo;
                    beginReadRoleAndHalo(
                        null,
                        currentHalo = [],
                        () => {
                            haloMax = currentHalo[0];
                            roleHs = currentHalo[1];
                            let haloInfo =
                                `天赋点:<span style="color:#0000c0;"><span id="halo_points">0</span> / ${haloMax}</span>,
                                 技能位:<span style="color:#0000c0;"><span id="halo_slots">0</span> / ${roleHs}</span>`;
                            let haloSelector = genericPopupQuerySelector('#halo_selector');
                            haloSelector.innerHTML =
                                `<style> .halo_group { display:block; width:25%; float:left; text-align:center; border-left:1px solid grey; }
                                         div > a { display:inline-block; width:90px; } div > a:hover { background-color:#bbddff; } </style>
                                 <div>${haloInfo}</div>
                                 <p></p>
                                 <div class="halo_group"></div>
                                 <div class="halo_group"></div>
                                 <div class="halo_group"></div>
                                 <div class="halo_group" style="border-right:1px solid grey;"></div>`;
                            let haloGroups = haloSelector.querySelectorAll('.halo_group');
                            let group = -1;
                            let points = -1;
                            g_halos.forEach((item) => {
                                if (item.points != points) {
                                    points = item.points;
                                    group++;
                                }
                                let a = document.createElement('a');
                                a.href = '#';
                                a.className = 'halo_item';
                                a.innerText = item.name + ' ' + item.points;
                                haloGroups[group].appendChild(a);
                                if (haloGroups[group].children.length > haloGroupItemMax) {
                                    haloGroupItemMax = haloGroups[group].children.length;
                                }
                            });

                            function selector_halo() {
                                let hp = parseInt(haloPoints.innerText);
                                let hs = parseInt(haloSlots.innerText);
                                if ($(this).attr('item-selected') != 1) {
                                    $(this).attr('item-selected', 1);
                                    $(this).css('background-color', highlightBackgroundColor);
                                    hp += parseInt($(this).text().split(' ')[1]);
                                    hs++;
                                }
                                else {
                                    $(this).attr('item-selected', 0);
                                    $(this).css('background-color', g_genericPopupBackgroundColor);
                                    hp -= parseInt($(this).text().split(' ')[1]);
                                    hs--;
                                }
                                haloPoints.innerText = hp;
                                haloSlots.innerText = hs;
                                haloPoints.style.color = (hp <= haloMax ? '#0000c0' : 'red');
                                haloSlots.style.color = (hs <= roleHs ? '#0000c0' : 'red');
                            }

                            haloPoints = genericPopupQuerySelector('#halo_points');
                            haloSlots = genericPopupQuerySelector('#halo_slots');
                            $('.halo_item').each(function(i, e) {
                                $(e).on('click', selector_halo);
                                $(e).attr('original-item', $(e).text().split(' ')[0]);
                            });
                            asyncOperations--;
                        },
                        null);

                    let wishpool;
                    beginReadWishpool(
                        wishpool = [],
                        null,
                        () => {
                            wishpool = wishpool.slice(-7);
                            asyncOperations--;
                        },
                        null);

                    function collectBindingInfo() {
                        let halo = [];
                        let sum = 0;
                        $('.halo_item').each(function(i, e) {
                            if ($(e).attr('item-selected') == 1) {
                                let ee = e.innerText.split(' ');
                                sum += parseInt(ee[1]);
                                halo.push($(e).attr('original-item'));
                            }
                        });
                        let h = parseInt(haloMax);
                        if (sum <= h && halo.length <= parseInt(roleHs)) {
                            let roleInfo = [ g_roleMap.get(roleId).shortMark, roleLv, roleHs, roleQl ];
                            if (roleId == 3000 || roleId == 3009) {
                                roleInfo.splice(1, 0, 'G=' + roleGv);
                            }

                            let amuletArray = [];
                            $('.amulet_item').each(function(i, e) {
                                if ($(e).attr('item-selected') == 1) {
                                    amuletArray[parseInt(e.lastChild.innerText) - 1] = ($(e).attr('original-item'));
                                }
                            });

                            let eqs = [];
                            eq_selectors.forEach((eq) => { eqs.push(eq.value); });

                            return [ roleInfo, wishpool, amuletArray, rolePt, eqs, halo ];
                        }
                        return null;
                    }

                    function generateExportString() {
                        let info = collectBindingInfo();
                        if (info?.length > 0) {
                            let exp = [ info[0].join(' '), 'WISH ' + info[1].join(' ') ];

                            let ag = new AmuletGroup();
                            ag.name = 'export-temp';
                            info[2].forEach((gn) => {
                                ag.merge(amuletGroups.get(gn));
                            });
                            if (ag.isValid()) {
                                exp.push(`AMULET ${ag.formatBuffShortMark(' ', ' ', false)} ENDAMULET`);
                            }

                            exp.push(info[3].join(' '));

                            info[4].forEach((eq) => {
                                exp.push(eq.replaceAll(',', ' '));
                            });

                            let halo = [ info[5].length ];
                            info[5].forEach((h) => {
                                halo.push(g_haloMap.get(h).shortMark);
                            });
                            exp.push(halo.join(' '));

                            return exp.join('\n') + '\n';
                        }
                        else {
                            alert('有装备未选或光环天赋选择错误!');
                        }
                        return null;
                    }

                    function unbindAll() {
                        if (confirm('这将清除本卡片全部绑定方案,继续吗?')) {
                            let udata = loadUserConfigData();
                            if (udata.dataBind[roleId] != null) {
                                delete udata.dataBind[roleId];
                            }
                            saveUserConfigData(udata);
                            bindingName.value = BINDING_NAME_DEFAULT;
                            bindingList.innerHTML = '';
                            refreshBindingSelector(roleId);
                            genericPopupShowInformationTips('解除全部绑定成功', 5000);
                        }
                    };

                    function deleteBinding() {
                        if (validateBindingName()) {
                            bindings = [];
                            let found = false;
                            $('.binding_name_item').each((index, item) => {
                                if (item.value == bindingName.value) {
                                    bindingList.removeChild(item);
                                    found = true;
                                }
                                else {
                                    bindings.push(`${item.value}${BINDING_NAME_SEPARATOR}${item.innerText}`);
                                }
                            });
                            if (found) {
                                let bn = bindingName.value;
                                let bi = null;
                                let udata = loadUserConfigData();
                                if (bindings.length > 0) {
                                    udata.dataBind[roleId] = bindings.join(BINDING_SEPARATOR);
                                    bindingName.value = bindingList.children[0].value;
                                    bi = bindingList.children[0].innerText;
                                }
                                else if(udata.dataBind[roleId] != null) {
                                    delete udata.dataBind[roleId];
                                    bindingName.value = BINDING_NAME_DEFAULT;
                                }
                                saveUserConfigData(udata);
                                refreshBindingSelector(roleId);
                                representBinding(bi);
                                genericPopupShowInformationTips(bn + ':解绑成功', 5000);
                            }
                            else {
                                alert('方案名称未找到!');
                            }
                        }
                    };

                    function saveBinding() {
                        if (validateBindingName()) {
                            let info = collectBindingInfo();
                            if (info?.length > 0) {
                                let bind_info = [ info[4][0], info[4][1], info[4][2], info[4][3],
                                                  info[5].join(','), info[2].join(',') ].join(BINDING_ELEMENT_SEPARATOR);
                                let newBinding = true;
                                bindings = [];
                                $('.binding_name_item').each((index, item) => {
                                    if (item.value == bindingName.value) {
                                        item.innerText = bind_info;
                                        newBinding = false;
                                    }
                                    bindings.push(`${item.value}${BINDING_NAME_SEPARATOR}${item.innerText}`);
                                });
                                if (newBinding) {
                                    let op0 = document.createElement('option');
                                    op0.className = 'binding_name_item';
                                    op0.innerText = bind_info;
                                    op0.value = bindingName.value;
                                    for (var op = bindingList.firstChild; op?.value < op0.value; op = op.nextSibling);
                                    bindingList.insertBefore(op0, op);
                                    bindings.push(`${op0.value}${BINDING_NAME_SEPARATOR}${op0.innerText}`);
                                }

                                let udata = loadUserConfigData();
                                udata.dataBind[roleId] = bindings.join(BINDING_SEPARATOR);
                                saveUserConfigData(udata);
                                refreshBindingSelector(roleId);
                                genericPopupShowInformationTips(bindingName.value + ':绑定成功', 5000);
                            }
                            else {
                                alert('有装备未选或光环天赋选择错误!');
                            }
                        }
                    }

                    function isValidBindingName(bindingName) {
                        return (bindingName?.length > 0 && bindingName.length < 32 && bindingName.search(USER_STORAGE_RESERVED_SEPARATORS) < 0);
                    }

                    function validateBindingName() {
                        let valid = isValidBindingName(bindingName.value);
                        genericPopupShowInformationTips(valid ? null : '方案名称不符合规则,请检查');
                        return valid;
                    }

                    function validateBinding() {
                        if (validateBindingName) {
                            let ol = bindingList.children.length;
                            for (let i = 0; i < ol; i++) {
                                if (bindingName.value == bindingList.children[i].value) {
                                    representBinding(bindingList.children[i].innerText);
                                    break;
                                }
                            }
                        }
                    }

                    function representBinding(items) {
                        if (items?.length > 0) {
                            let elements = items.split(BINDING_ELEMENT_SEPARATOR);
                            if (elements.length > 3) {
                                let v = elements.slice(0, 4);
                                eq_selectors.forEach((eqs) => {
                                    for (let op of eqs.childNodes) {
                                        if (v.indexOf(op.value) >= 0) {
                                            eqs.value = op.value;
                                            break;
                                        }
                                    }
                                    eqs.onchange({ target : eqs });
                                });
                            }
                            if (elements.length > 4) {
                                let hp = 0;
                                let hs = 0;
                                let v = elements[4].split(',');
                                $('.halo_item').each((index, item) => {
                                    let s = (v.indexOf($(item).attr('original-item')) < 0 ? 0 : 1);
                                    $(item).attr('item-selected', s);
                                    $(item).css('background-color', s == 0 ? g_genericPopupBackgroundColor : highlightBackgroundColor);
                                    hp += (s == 0 ? 0 : parseInt($(item).text().split(' ')[1]));
                                    hs += s;
                                });
                                haloPoints.innerText = hp;
                                haloSlots.innerText = hs;
                                haloPoints.style.color = (hp <= haloMax ? '#0000c0' : 'red');
                                haloSlots.style.color = (hs <= roleHs ? '#0000c0' : 'red');
                            }
                            selectedAmuletGroupCount = 0;
                            if (elements.length > 5 && amuletCount != null) {
                                let ac = 0;
                                let v = elements[5].split(',');
                                $('.amulet_item').each((index, item) => {
                                    let j = v.indexOf($(item).attr('original-item'));
                                    let s = (j < 0 ? 0 : 1);
                                    $(item).attr('item-selected', s);
                                    $(item).css('background-color', s == 0 ? g_genericPopupBackgroundColor : highlightBackgroundColor);
                                    item.lastChild.innerText = (j < 0 ? '' : j + 1);
                                    selectedAmuletGroupCount += s;
                                    ac += (s == 0 ? 0 : parseInt($(item).text().match(/\[(\d+)\]/)[1]));
                                });
                                amuletCount.innerText = ac;
                            }
                        }
                    }

                    function selector_amulet() {
                        let ac = parseInt(amuletCount.innerText);
                        let tc = parseInt($(this).text().match(/\[(\d+)\]/)[1]);
                        if ($(this).attr('item-selected') != 1) {
                            $(this).attr('item-selected', 1);
                            $(this).css('background-color', highlightBackgroundColor);
                            this.lastChild.innerText = ++selectedAmuletGroupCount;
                            ac += tc;
                        }
                        else {
                            $(this).attr('item-selected', 0);
                            $(this).css('background-color', g_genericPopupBackgroundColor);
                            let i = parseInt(this.lastChild.innerText);
                            this.lastChild.innerText = '';
                            ac -= tc;
                            if (i < selectedAmuletGroupCount) {
                                $('.amulet_item').each((index, item) => {
                                    var j;
                                    if ($(item).attr('item-selected') == 1 && (j = parseInt(item.lastChild.innerText)) > i) {
                                        item.lastChild.innerText = j - 1;
                                    }
                                });
                            }
                            selectedAmuletGroupCount--;
                        }
                        amuletCount.innerText = ac;
                    }

                    let bindingList = genericPopupQuerySelector('#binding_list');
                    let bindingName = genericPopupQuerySelector('#binding_name');
                    let haloPoints = null;
                    let haloSlots = null;
                    let amuletContainer = genericPopupQuerySelector('#amulet_selector').firstChild;
                    let amuletCount = null;
                    let amuletGroups = amuletLoadGroups();
                    let selectedAmuletGroupCount = 0;

                    let amuletGroupCount = (amuletGroups?.count() ?? 0);
                    if (amuletGroupCount > 0) {
                        amuletContainer.innerHTML =
                            '护符组:已选定 <span id="amulet_count">0</span> 个护符<span style="float:right;margin-right:5px;">加载顺序</span><p></p>';
                        amuletCount = genericPopupQuerySelector('#amulet_count');
                        amuletCount.style.color = '#0000c0';
                        let amuletArray = amuletGroups.toArray().sort((a, b) => a.name < b.name ? -1 : 1);
                        for (let i = 0; i < amuletGroupCount; i++) {
                            let li0 = document.createElement('li');
                            li0.className = 'amulet_item';
                            li0.setAttribute('original-item', amuletArray[i].name);
                            li0.title = amuletArray[i].formatBuffSummary('', '', '\n', false);
                            li0.innerHTML =
                                `<a href="#">${amuletArray[i].name} [${amuletArray[i].count()}]</a>` +
                                `<span style="color:#0000c0;width:40;float:right;margin-right:5px;"></span>`;
                            li0.onclick = selector_amulet;
                            amuletContainer.appendChild(li0);
                        }
                    }
                    else {
                        amuletContainer.innerHTML =
                            '<ul><li>未能读取护符组定义信息,这可能是因为您没有预先完成护符组定义。</li><p />' +
                                '<li>将护符与角色卡片进行绑定并不是必须的,但如果您希望使用此功能,' +
                                    '则必须先定义护符组然后才能将它们与角色卡片进行绑定。</li><p />' +
                                '<li>要定义护符组,您需要前往 [ <b style="color:#0000c0;">我的角色 → 武器装备</b> ] 页面,' +
                                    '并在其中使用将背包内容 [ <b style="color:#0000c0;">存为护符组</b> ] 功能,' +
                                    '或在 [ <b style="color:#0000c0;">管理护符组</b> ] 相应功能中进行定义。</li></ul>';
                    }

                    let bindings = null;
                    if (bind_info != null) {
                        bindings = bind_info.split(BINDING_SEPARATOR).sort((a, b) => {
                            a = a.split(BINDING_NAME_SEPARATOR);
                            b = b.split(BINDING_NAME_SEPARATOR);
                            a = a.length > 1 ? a[0] : BINDING_NAME_DEFAULT;
                            b = b.length > 1 ? b[0] : BINDING_NAME_DEFAULT;
                            return a < b ? -1 : 1;
                        });
                    }
                    else {
                        bindings = [];
                    }

                    bindings.forEach((item) => {
                        let elements = item.split(BINDING_NAME_SEPARATOR);
                        let binding = elements[elements.length - 1].split(BINDING_ELEMENT_SEPARATOR);
                        if (binding.length > 5) {
                            let amuletGroupNames = binding[5].split(',');
                            let ag = '';
                            let sp = '';
                            let al = amuletGroupNames.length;
                            for (let i = 0; i < al; i++) {
                                if (amuletGroups.contains(amuletGroupNames[i])) {
                                    ag += (sp + amuletGroupNames[i]);
                                    sp = ',';
                                }
                            }
                            binding[5] = ag;
                            elements[elements.length - 1] = binding.join(BINDING_ELEMENT_SEPARATOR);
                        }

                        let op0 = document.createElement('option');
                        op0.className = 'binding_name_item';
                        op0.innerText = elements[elements.length - 1];
                        op0.value = elements.length > 1 ? elements[0] : BINDING_NAME_DEFAULT;
                        bindingList.appendChild(op0);
                    });

                    let timer = setInterval(() => {
                        if (asyncOperations == 0) {
                            clearInterval(timer);
                            httpRequestClearAll();

                            if (bindingList.children.length > 0) {
                                bindingName.value = bindingList.children[0].value;
                                representBinding(bindingList.children[0].innerText);
                            }
                            else {
                                bindingName.value = BINDING_NAME_DEFAULT;
                            }

                            bindingName.oninput = validateBindingName;
                            bindingName.onchange = validateBinding;

                            genericPopupQuerySelector('#copy_export_string').onclick = (() => {
                                genericPopupQuerySelector('#role_export_string').select();
                                if (document.execCommand('copy')) {
                                    genericPopupShowInformationTips('导出内容已复制到剪贴板', 5000);
                                }
                                else {
                                    genericPopupShowInformationTips('复制失败,请进行手工复制(CTRL+A, CTRL+C)');
                                }
                            });

                            genericPopupQuerySelector('#hide_export_div').onclick = (() => {
                                genericPopupQuerySelector('#role_export_div').style.display = 'none';
                            });

                            genericPopupSetContentSize(Math.min((haloGroupItemMax + amuletGroupCount) * 20
                                                                                  + (amuletGroupCount > 0 ? 60 : 160) + 260,
                                                                window.innerHeight - 200),
                                                       600, true);

                            genericPopupAddButton('解除绑定', 0, deleteBinding, true);
                            genericPopupAddButton('全部解绑', 0, unbindAll, true);
                            genericPopupAddButton('绑定', 80, saveBinding, false);
                            genericPopupAddButton(
                                '导出计算器',
                                0,
                                () => {
                                    let string = generateExportString();
                                    if (string?.length > 0) {
                                        genericPopupQuerySelector('#role_export_string').value = string;
                                        genericPopupQuerySelector('#role_export_div').style.display = 'block';
                                    }
                                },
                                false);
                            genericPopupAddCloseButton(80);

                            genericPopupCloseProgressMessage();
                            genericPopupShowModal(true);
                        }
                    }, 200);
                };

                function refreshBindingSelector(roleId) {
                    let bindingsolutionDiv = document.getElementById(g_bindingSolutionId);
                    let bindingList = document.getElementById(g_bindingListSelectorId);

                    let bindings = null;
                    let bind_info = loadUserConfigData().dataBind[roleId];
                    if (bind_info != null) {
                        bindings = bind_info.split(BINDING_SEPARATOR).sort((a, b) => {
                            a = a.split(BINDING_NAME_SEPARATOR);
                            b = b.split(BINDING_NAME_SEPARATOR);
                            a = a.length > 1 ? a[0] : BINDING_NAME_DEFAULT;
                            b = b.length > 1 ? b[0] : BINDING_NAME_DEFAULT;
                            return a < b ? -1 : 1;
                        });
                    }
                    bindingList.innerHTML = '';
                    if (bindings?.length > 0) {
                        bindings.forEach((item) => {
                            let elements = item.split(BINDING_NAME_SEPARATOR);
                            let op0 = document.createElement('option');
                            op0.value = roleId + BINDING_NAME_SEPARATOR + elements[elements.length - 1];
                            op0.innerText = (elements.length > 1 ? elements[0] : BINDING_NAME_DEFAULT);
                            bindingList.appendChild(op0);
                        });
                        bindingsolutionDiv.style.display = 'inline-block';
                    }
                    else {
                        bindingsolutionDiv.style.display = 'none';
                    }
                }

                function addBindBtn() {
                    let mountedRoleId = g_roleMap.get(document.getElementById('carding')
                                                             ?.querySelector('div.text-info.fyg_f24.fyg_lh60')
                                                             ?.children[0]?.innerText)?.id;
                    let roleId = g_roleMap.get(document.querySelector('#backpacks > div.row > div.col-md-3 > span.text-info.fyg_f24')
                                                      ?.innerText)?.id;

                    function bindingLinks(e) {
                        if (e.target.id == g_bindingPopupLinkId) {
                            showBindingPopup();
                        }
                        else if (e.target.id == g_equipOnekeyLinkId) {
                            equipOnekey();
                        }
                    }

                    let bindingAnchor = document.querySelector('#backpacks > div.row > div.col-md-12').parentNode.nextSibling;
                    let bindingContainer = document.createElement('div');
                    bindingContainer.className = 'btn-group';
                    bindingContainer.style.display = 'block';
                    bindingContainer.style.width = '100%';
                    bindingContainer.style.marginTop = '15px';
                    bindingContainer.style.fontSize = '18px';
                    bindingContainer.style.padding = '10px';
                    bindingContainer.style.borderRadius = '5px';
                    bindingContainer.style.color = '#0000c0';
                    bindingContainer.style.backgroundColor = '#ebf2f9';
                    bindingAnchor.parentNode.insertBefore(bindingContainer, bindingAnchor);

                    let bindingLink = document.createElement('span');
                    bindingLink.setAttribute('class', 'fyg_lh30');
                    bindingLink.style.width = '30%';
                    bindingLink.style.textAlign = 'left';
                    bindingLink.style.display = 'inline-block';
                    bindingLink.innerHTML =
                        `<a href="#" style="text-decoration:underline;" id="${g_bindingPopupLinkId}">绑定(装备 光环 护符)</a>`;
                    bindingLink.querySelector('#' + g_bindingPopupLinkId).onclick = bindingLinks;
                    bindingContainer.appendChild(bindingLink);

                    let bindingsolutionDiv = document.createElement('div');
                    bindingsolutionDiv.id = g_bindingSolutionId;
                    bindingsolutionDiv.style.display = 'none';
                    bindingsolutionDiv.style.width = '70%';

                    let bindingList = document.createElement('select');
                    bindingList.id = g_bindingListSelectorId;
                    bindingList.style.width = '80%';
                    bindingList.style.color = '#0000c0';
                    bindingList.style.textAlign = 'center';
                    bindingList.style.display = 'inline-block';
                    bindingsolutionDiv.appendChild(bindingList);

                    let applyLink = document.createElement('span');
                    applyLink.setAttribute('class', 'fyg_lh30');
                    applyLink.style.width = '20%';
                    applyLink.style.textAlign = 'right';
                    applyLink.style.display = 'inline-block';
                    applyLink.innerHTML =
                        `<a href="#" style="text-decoration:underline;" id="${g_equipOnekeyLinkId}">应用此方案</a>`;
                    applyLink.querySelector('#' + g_equipOnekeyLinkId).onclick = bindingLinks;
                    bindingsolutionDiv.appendChild(applyLink);
                    bindingContainer.appendChild(bindingsolutionDiv);

                    refreshBindingSelector(roleId);
                }

                let backpacksObserver = new MutationObserver(() => {
                    $('.pop_main').hide();
                    let page = document.getElementsByClassName('nav nav-secondary nav-justified')[0].children;
                    let index = 0;
                    for (let i = 0; i < 4; i++) {
                        if (page[i].className == 'active') {
                            index = i;
                        }
                    }
                    switch (index) {
                        case 0: {
                            calcBtn.onclick = (() => {
                                try {
                                    let equip = document.querySelectorAll(cardingObjectsQueryString);
                                    let bag = Array.from(document.querySelectorAll(bagObjectsQueryString)).concat(
                                              Array.from(document.querySelectorAll(storeObjectsQueryString)));
                                    let bagdata = equipmentNodesToInfoArray(bag);
                                    let data = equipmentNodesToInfoArray(equip);
                                    bagdata = bagdata.concat(data).sort(equipmentInfoComparer);
                                    calcDiv.innerHTML =
                                        `<div class="pop_main" style="padding:0px 10px;"><a href="#">× 折叠 ×</a>
                                         <div class="pop_con">
                                         <div style="width:200px;padding:5px;margin-top:10px;margin-bottom:10px;
                                              color:purple;border:1px solid grey;">护符:</div>
                                         <div class="pop_text"></div>
                                         <div style="width:200px;padding:5px;margin-top:10px;margin-bottom:10px;
                                              color:purple;border:1px solid grey">已装备:</div>
                                         <div class="pop_text"></div>
                                         <div class="pop_text"></div>
                                         <div class="pop_text"></div>
                                         <div class="pop_text"></div>
                                         <div style="width:200px;padding:5px;margin-top:10px;margin-bottom:10px;
                                              color:purple;border:1px solid grey;">全部装备:</div>
                                         ${new Array(bagdata.length + 1).fill('<div class="pop_text"></div>').join('')}<hr></div>
                                         <a href="#">× 折叠 ×</a></div>`;

                                    $('.pop_main a').click(() => {
                                        $('.pop_main').hide()
                                    })
                                    let text = $('.pop_text');

                                    let amulet = document.getElementById('backpacks').lastChild.children[1].innerText.match(/\+\d+/g);
                                    for (let i = amulet.length - 1; i >= 0; i--) {
                                        if (amulet[i][1] == '0') {
                                            amulet.splice(i, 1);
                                        }
                                        else {
                                            amulet[i] = g_amuletBuffs[i].shortMark + amulet[i];
                                        }
                                    }
                                    text[0].innerText = `AMULET ${amulet.join(' ').replace(/\+/g, ' ')} ENDAMULET`;

                                    text[1].innerText = `${data[0].slice(0, -1).join(' ')}`;
                                    text[2].innerText = `${data[1].slice(0, -1).join(' ')}`;
                                    text[3].innerText = `${data[2].slice(0, -1).join(' ')}`;
                                    text[4].innerText = `${data[3].slice(0, -1).join(' ')}`;

                                    for (let i = 0; i < bagdata.length; i++) {
                                        text[5 + i].innerText = `${bagdata[i].slice(0, -1).join(' ')}`;
                                    }
                                    $('.pop_main').show();
                                }
                                catch (err) {
                                    console.log(err);
                                }
                            });
                            if (document.getElementById('equipmentDiv') == null) {
                                backpacksObserver.disconnect();
                                addCollapse();
                                backpacksObserver.observe(document.getElementById('backpacks'), { childList : true , characterData : true });
                            }
                            break;
                        }
                        case 1: {
                            let roleId = g_roleMap.get(document.querySelector('#backpacks > div.row > div.col-md-3 > span.text-info.fyg_f24')
                                                              ?.innerText)?.id;
                            if (roleId != undefined) {
                                calcBtn.onclick = (() => {
                                    calcDiv.innerHTML =
                                        `<div class="pop_main"><div class="pop_con">
                                         <div class="pop_text"></div><div class="pop_text"></div>
                                         </div><a href="#">× 折叠 ×</a></div>`;
                                    $('.pop_main a').click(() => {
                                        $('.pop_main').hide();
                                    })
                                    let text = $('.pop_text');
                                    let cardInfos = document.querySelector('#backpacks').querySelectorAll('.icon.icon-angle-down.text-primary');
                                    let cardInfo = [ g_roleMap.get(roleId)?.shortMark ?? 'UNKNOW',
                                                     cardInfos[0].innerText.match(/\d+/)[0],
                                                     cardInfos[2].innerText.match(/\d+/)[0],
                                                     cardInfos[1].innerText.match(/\d+/)[0] ];
                                    if (roleId == 3000 || roleId == 3009) {
                                        cardInfo.splice(1, 0, 'G=' + (cardInfos[3]?.innerText.match(/\d+/)[0] ?? '0'));
                                    }
                                    let points = [];
                                    for (let i = 1; i <= 6; i++) {
                                        points.push(document.getElementById('sjj' + i).innerText);
                                    }
                                    text[0].innerText = cardInfo.join(' ');
                                    text[1].innerText = points.join(' ');
                                    $('.pop_main').show();
                                });
                                backpacksObserver.disconnect();
                                addBindBtn();
                                backpacksObserver.observe(document.getElementById('backpacks'), { childList : true , characterData : true });
                            }
                            else {
                                calcBtn.onclick = (() => {});
                            }
                            break;
                        }
                        case 2: {
                            calcBtn.onclick = (() => {
                                try {
                                    calcDiv.innerHTML =
                                        `<div class="pop_main"><div class="pop_con">
                                         <div class="pop_text"></div></div>
                                         <a href="#">× 折叠 ×</a></div>`;
                                    $('.pop_main a').click(() => {
                                        $('.pop_main').hide();
                                    })
                                    let text = $('.pop_text');
                                    let aura = document.querySelectorAll('#backpacks .btn.btn-primary');
                                    let data = [];
                                    data.push(aura.length);
                                    aura.forEach((item) => { data.push(g_haloMap.get(item.childNodes[1].nodeValue.trim())?.shortMark ?? ''); });
                                    text[0].innerText = data.join(' ');
                                    $('.pop_main').show();
                                }
                                catch (err) {
                                    console.log(err);
                                }
                            });
                            break;
                        }
                        case 3: {
                            calcBtn.onclick = (() => {});
                            break;
                        }
                    }
                });
                backpacksObserver.observe(document.getElementById('backpacks'), { childList : true , characterData : true });
                document.getElementById('backpacks').appendChild(document.createElement('div'));
            }
        }, 500);
    }
    else if (window.location.pathname == '/fyg_beach.php') {
        genericPopupInitialize();

        let beachConfigDiv = document.createElement('form');
        beachConfigDiv.innerHTML =
            `<div style="padding:5px 15px;border-bottom:1px solid grey;">
             <button type="button" style="margin-right:15px;" id="siftSettings">筛选设置</button>
             <label for="ignoreStoreMysEquip" title="不将海滩上的装备与已有的神秘装备做比较"
                    style="margin-right:5px;cursor:pointer;">已有神秘装备不作为筛选依据</label>
             <input type="checkbox" id="ignoreStoreMysEquip" style="margin-right:15px;" />
             <label for="forceExpand" style="margin-right:5px;cursor:pointer;">强制展开所有装备</label>
             <input type="checkbox" id="forceExpand" style="margin-right:15px;" />
             <b><span id="analyze-indicator">正在分析...</span></b>
             <div style="float:right;"><label for="beach_BG"
                  style="margin-right:5px;cursor:pointer;">使用深色背景</label>
             <input type="checkbox" id="beach_BG" /></div></div>`;

        let ignoreStoreMysEquip = beachConfigDiv.querySelector('#ignoreStoreMysEquip').checked =
            (localStorage.getItem(g_beachIgnoreStoreMysEquipStorageKey) == 'true');
        beachConfigDiv.querySelector('#ignoreStoreMysEquip').onchange = (() => {
            localStorage.setItem(g_beachIgnoreStoreMysEquipStorageKey,
                                 ignoreStoreMysEquip = beachConfigDiv.querySelector('#ignoreStoreMysEquip').checked);
            document.getElementById('analyze-indicator').innerText = '正在分析...';
            setTimeout(() => { expandEquipment(equipment); }, 50);
        });

        let forceExpand = beachConfigDiv.querySelector('#forceExpand').checked =
            (localStorage.getItem(g_beachForceExpandStorageKey) == 'true');
        beachConfigDiv.querySelector('#forceExpand').onchange = (() => {
            localStorage.setItem(g_beachForceExpandStorageKey,
                                 forceExpand = beachConfigDiv.querySelector('#forceExpand').checked);
            document.getElementById('analyze-indicator').innerText = '正在分析...';
            setTimeout(() => { expandEquipment(equipment); }, 50);
        });

        let beach_BG = beachConfigDiv.querySelector('#beach_BG').checked =
            (localStorage.getItem(g_beachBGStorageKey) == 'true');
        beachConfigDiv.querySelector('#beach_BG').onchange = (() => {
            localStorage.setItem(g_beachBGStorageKey,
                                 beach_BG = beachConfigDiv.querySelector('#beach_BG').checked);
            changeBeachStyle('beach_copy', beach_BG);
        });

        beachConfigDiv.querySelector('#siftSettings').onclick = (() => {
            let fixedContent =
                '<div style="padding:20px 0px 10px;"><b><ul style="font-size:15px;color:#0000c0;">' +
                '<li>被勾选的装备不会被展开,不会产生与已有装备的对比列表,但传奇、史诗及有神秘属性的装备除外</li>' +
                '<li>未勾选的属性被视为主要属性,海滩装备的任一主要属性值大于已有装备的相应值时即会被展开</li>' +
                '<li>被勾选的属性被视为次要属性,当且仅当海滩装备和已有装备的主要属性值完全相等时才会被对比</li></ul></b></div>';
            let mainContent =
                `<style> #equip-table { width:100%; }
                         #equip-table tr.equip-tr { }
                         #equip-table tr.equip-tr-alt { background-color:${g_genericPopupBackgroundColorAlt}; }
                         #equip-table th { width:17%; text-align:right; }
                         #equip-table th.equip-th-equip { width:32%; text-align:left; }
                         #equip-table td { display:table-cell; text-align:right; }
                         #equip-table td.equip-td-equip { display:table-cell; text-align:left; }
                         #equip-table label.equip-checkbox-label { margin-left:5px; cursor:pointer; } </style>
                 <div class="${g_genericPopupTopLineDivClass}"><table id="equip-table">
                 <tr><th class="equip-th-equip"><input type="checkbox" id="equip-name-check" />
                 <label class= "equip-checkbox-label" for="equip-name-check">装备名称</label></th>
                 <th>装备属性</th><th /><th /><th /></tr></table><div>`;

            genericPopupSetFixedContent(fixedContent);
            genericPopupSetContent('海滩装备筛选设置', mainContent);

            genericPopupQuerySelector('#equip-name-check').onchange = ((e) => {
                let eqchecks = equipTable.querySelectorAll('input.sift-settings-checkbox');
                for (let i = 0; i < eqchecks.length; i += 5) {
                    eqchecks[i].checked = e.target.checked;
                }
            });

            let equipTable = genericPopupQuerySelector('#equip-table');
            let equipTypeColor = [ '#000080', '#008000', '#800080', '#008080' ];
            g_equipments.forEach((equip) => {
                let tr = document.createElement('tr');
                tr.id = `equip-index-${equip.index}`;
                tr.className = 'equip-tr' + ((equip.index & 1) == 0 ? ' equip-tr-alt' : '');
                tr.setAttribute('equip-abbr', equip.shortMark);
                tr.style.color = equipTypeColor[equip.type];
                let attrHTML = '';
                equip.attributes.forEach((item, index) => {
                    let attrId = `${tr.id}-attr-${index}`;
                    attrHTML +=
                        `<td><input type="checkbox" class="sift-settings-checkbox" id="${attrId}" />
                         <label class="equip-checkbox-label" for="${attrId}">${item.attribute.name}</label></td>`;
                });
                let equipId = `equip-${equip.index}`;
                tr.innerHTML =
                    `<td class="equip-td-equip"><input type="checkbox" class="sift-settings-checkbox" id="${equipId}" />
                         <label class="equip-checkbox-label" for="${equipId}">${equip.name}</label></td>${attrHTML}`;
                equipTable.appendChild(tr);
            });

            let udata = loadUserConfigData();
            if (udata.dataBeachSift == null) {
                udata.dataBeachSift = {};
                saveUserConfigData(udata);
            }

            let eqchecks = equipTable.querySelectorAll('input.sift-settings-checkbox');
            for (let i = 0; i < eqchecks.length; i += 5) {
                let abbr = eqchecks[i].parentNode.parentNode.getAttribute('equip-abbr');
                if (udata.dataBeachSift[abbr] != null) {
                    let es = udata.dataBeachSift[abbr].split(',');
                    for (let j = 0; j < es.length; j++) {
                        eqchecks[i + j].checked = (es[j] == 'true');
                    }
                }
            }

            genericPopupAddButton('全选', 80, (() => { $('#equip-table .sift-settings-checkbox').prop('checked', true); }), true);
            genericPopupAddButton('全不选', 80, (() => { $('#equip-table .sift-settings-checkbox').prop('checked', false); }), true);
            genericPopupAddButton(
                '确认',
                80,
                (() => {
                    let settings = {};
                    equipTable.querySelectorAll('tr.equip-tr').forEach((row) => {
                        let checks = [];
                        row.querySelectorAll('input.sift-settings-checkbox').forEach((col) => { checks.push(col.checked); });
                        settings[row.getAttribute('equip-abbr')] = checks.join(',');
                    });

                    let udata = loadUserConfigData();
                    udata.dataBeachSift = settings;
                    saveUserConfigData(udata);

                    genericPopupClose(true);
                    window.location.reload();
                }),
                false);
            genericPopupAddCloseButton(80);

            genericPopupSetContentSize(Math.min(g_equipments.length * 31 + 65, Math.max(window.innerHeight - 200, 500)),
                                       Math.min(750, Math.max(window.innerWidth - 100, 600)),
                                       true);
            genericPopupShowModal(true);
        });

        let beach = document.getElementById('beachall');
        beach.parentNode.insertBefore(beachConfigDiv, beach);

        let batbtns = document.querySelector('div.col-md-9 > div.panel.panel-primary > div.panel-body > div.btn-group > button.btn.btn-danger');
        let toAmuletBtn = document.createElement('button');
        toAmuletBtn.className = batbtns.className;
        toAmuletBtn.innerText = '批量沙滩装备转护符';
        toAmuletBtn.style.marginLeft = '1px';
        toAmuletBtn.onclick = equipToAmulet;
        batbtns.parentNode.appendChild(toAmuletBtn);

        function equipToAmulet() {
            function divHeightAdjustment(div) {
                div.style.height = (div.parentNode.offsetHeight - div.offsetTop - 3) + 'px';
            }

            function moveAmuletItem(e) {
                let li = e.target;
                if (li.tagName == 'LI') {
                    let liIndex = parseInt(li.getAttribute('item-index'));
                    let container = (li.parentNode == amuletToStoreList ? amuletToDestroyList : amuletToStoreList);
                    for (var li0 = container.firstChild; parseInt(li0?.getAttribute('item-index')) < liIndex; li0 = li0.nextSibling);
                    container.insertBefore(li, li0);
                }
            }

            function refreshBackpacks(fnFurtherProcess) {
                let asyncOperations = 1;
                let asyncObserver = new MutationObserver(() => { asyncObserver.disconnect(); asyncOperations = 0; });
                asyncObserver.observe(document.getElementById('backpacks'), { childList : true , subtree : true });

                stbp();

                let timer = setInterval(() => {
                    if (asyncOperations == 0) {
                        clearInterval(timer);
                        if (fnFurtherProcess != null) {
                            fnFurtherProcess();
                        }
                    }
                }, 200);
            }

            function queryObjects(bag, queryBagId, ignoreEmptyCell, beach, beachEquipLevel) {
                if (bag != null) {
                    let nodes = document.getElementById('backpacks').children;
                    if (queryBagId) {
                        objectIdParseNodes(nodes, bag, null, ignoreEmptyCell);
                    }
                    else {
                        let i = 0;
                        for (let node of nodes) {
                            let e = ((new Amulet()).fromNode(node) ?? equipmentInfoParseNode(node));
                            if (e != null) {
                                bag.push([ i++, e ]);
                            }
                        }
                    }
                }
                if (beach != null) {
                    let nodes = document.getElementById('beachall').children;
                    for (let node of nodes) {
                        let lv = equipmentGetLevel(node);
                        if (lv > 1) {
                            let e = equipmentInfoParseNode(node);
                            if (e != null && ((lv == 2 && parseInt(e[1]) >= beachEquipLevel) || lv > 2)) {
                                beach.push(parseInt(e[7]));
                            }
                        }
                    }
                }
            }

            const pirl_verify_data ='124';
            const pirl_data =
                  getPostData(/pirl\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*\+pirlyz\+.*"/).slice(7, -1)
                                                                                         .replace('"+pirlyz+"', pirl_verify_data);
            let equipPirlRequestsCount = 0;
            function pirlEquip(objects, fnFurtherProcess, fnParams) {
                if (objects?.length > 0) {
                    let ids = [];
                    while (ids.length < g_maxConcurrentRequests && objects.length > 0) {
                        ids.push(objects.pop());
                    }
                    if ((equipPirlRequestsCount = ids.length) > 0) {
                        while (ids.length > 0) {
                            let request = GM_xmlhttpRequest({
                                method: g_postMethod,
                                url: g_postUrl,
                                headers: g_postHeader,
                                data: pirl_data.replace('"+id+"', ids.shift()),
                                onload: response => {
                                    if (--equipPirlRequestsCount == 0) {
                                        pirlEquip(objects, fnFurtherProcess, fnParams);
                                    }
                                }
                            });
                            httpRequestRegister(request);
                        }
                        return;
                    }
                }
                if (fnFurtherProcess != null) {
                    fnFurtherProcess(fnParams);
                }
            }

            let pickCount;
            function pickEquip() {
                genericPopupShowInformationTips('拾取装备...', 0);
                let ids = [];
                while (originalBeach.length > 0 && ids.length < freeCell) {
                    ids.unshift(originalBeach.pop());
                }
                pickCount = ids.length;
                beginMoveObjects(ids, g_object_move_path.beach2bag, refreshBackpacks, findPickedEquip);
            }

            function findPickedEquip() {
                let bag = [];
                queryObjects(bag, true, true, null, 0);
                let ids = findNewObjects(bag, originalBag, (a, b) => a - b, false);
                if (ids.length != pickCount) {
                    alert('从海滩拾取装备出错无法继续,请手动处理!');
                    window.location.reload();
                    return;
                }
                genericPopupShowInformationTips('熔炼装备...', 0);
                pirlEquip(ids, refreshBackpacks, prepareNewAmulets);
            }

            const objectTypeColor = [ '#e0fff0', '#ffe0ff', '#fff0e0', '#d0f0ff' ];
            let minBeachAmuletPointsToStore = [ 1, 1, 1 ];
            let cfg = g_configMap.get('minBeachAmuletPointsToStore')?.value?.split(',');
            if (cfg?.length == 3) {
                cfg.forEach((item, index) => {
                    if (isNaN(minBeachAmuletPointsToStore[index] = parseInt(item))) {
                        minBeachAmuletPointsToStore[index] = 1;
                    }
                });
            }
            function prepareNewAmulets() {
                newAmulets = findNewObjects(amuletNodesToArray(document.getElementById('backpacks').children),
                                            originalBag, (a, b) => a.id - b, false);
                if (newAmulets.length != pickCount) {
                    alert('熔炼装备出错无法继续,请手动处理!');
                    window.location.reload();
                    return;
                }
                newAmulets.forEach((am, index) => {
                    let li = document.createElement('li');
                    li.style.backgroundColor = g_equipmentLevelBGColor[am.level + 2];
                    li.setAttribute('item-index', index);
                    li.innerText = (am.type == 2 || am.level == 2 ? '★ ' : '') + am.formatBuffText();
                    (am.getTotalPoints() < minBeachAmuletPointsToStore[am.type] ? amuletToDestroyList : amuletToStoreList).appendChild(li);
                });
                if (window.getSelection) {
                    window.getSelection().removeAllRanges();
                }
                else if (document.getSelection) {
                    document.getSelection().removeAllRanges();
                }
                genericPopupShowInformationTips((originalBeach.length > 0 ? '本批' : '全部') + '装备熔炼完成,请分类后继续', 0);
                btnContinue.innerText = `继续 (剩余 ${originalBeach.length} 件装备)`;
                btnContinue.disabled = '';
                btnCloseOnBatch.disabled = '';
            }

            function processNewAmulets() {
                btnContinue.disabled = 'disabled';
                btnCloseOnBatch.disabled = 'disabled';

                if (freeCell == 0) {
                    scheduleFreeCell();
                }
                else if (pickCount > 0) {
                    let indices = [];
                    for (let li of amuletToDestroyList.children) {
                        indices.push(parseInt(li.getAttribute('item-index')));
                    }
                    if (indices.length > 0) {
                        let ids = [];
                        let warning = 0;
                        indices.sort((a, b) => a - b).forEach((i) => {
                            let am = newAmulets[i];
                            if (am.type == 2 || am.level == 2) {
                                warning++;
                            }
                            ids.push(am.id);
                        });
                        if (warning > 0 && !confirm(`这将把 ${warning} 个“樱桃/传奇”护符转换成果核,要继续吗?`)) {
                            btnContinue.disabled = '';
                            btnCloseOnBatch.disabled = '';
                            return;
                        }
                        amuletToDestroyList.innerHTML = '';
                        coresCollected += indices.length;
                        pickCount -= indices.length;
                        genericPopupShowInformationTips('转换果核...', 0);
                        pirlEquip(ids, refreshBackpacks, processNewAmulets);
                    }
                    else {
                        let bag = [];
                        queryObjects(bag, true, true, null, 0);
                        let ids = findNewObjects(bag, originalBag, (a, b) => a - b, false);
                        if (ids.length != pickCount) {
                            alert('将新护符放入仓库出错无法继续,请手动处理!');
                            window.location.reload();
                            return;
                        }
                        amuletToStoreList.innerHTML = '';
                        amuletsCollected += pickCount;
                        pickCount = 0;
                        genericPopupShowInformationTips('放入仓库...', 0);
                        beginMoveObjects(ids, g_object_move_path.bag2store, refreshBackpacks, processNewAmulets);
                    }
                }
                else if (originalBeach.length > 0) {
                    pickEquip();
                }
                else {
                    restoreBag(15);
                }
            }

            let originalFreeCell = 0;
            let originalBag = [];
            let originalBeach = [];
            let scheduledObjects = { equips : [] , amulets : [] };

            let freeCell = 0;
            let amuletsCollected = 0;
            let coresCollected = 0;
            let newAmulets = null;

            let minBeachEquipLevelToAmulet = (g_configMap.get('minBeachEquipLevelToAmulet')?.value ?? 1);
            queryObjects(originalBag, true, false, originalBeach, minBeachEquipLevelToAmulet);
            if (originalBeach.length == 0) {
                alert('海滩无可熔炼装备!');
                return;
            }

            function prepareBagList() {
                let info = (originalFreeCell > 0 ? [ '可', '更多' ] : [ '请', '必要' ])
                genericPopupShowInformationTips(`${info[0]}将部分背包内容入仓以提供${info[1]}的操作空间,点击“继续”开始`, 0);
                amuletToStoreList.parentNode.parentNode.children[0].innerText = '背包内容';
                amuletToDestroyList.parentNode.parentNode.children[0].innerText = '临时入仓';

                queryObjects(originalBag = [], false, true, null, 0);
                let bag = originalBag.slice().sort((a, b) => {
                    if (Array.isArray(a[1]) && Array.isArray(b[1])) {
                        return equipmentInfoComparer(a[1], b[1]);
                    }
                    else if (Array.isArray(a[1])){
                        return -1;
                    }
                    else if (Array.isArray(b[1])){
                        return 1;
                    }
                    return a[1].compareTo(b[1], true);
                });
                bag.forEach((item, index) => {
                    let e = item[1];
                    let isEq = Array.isArray(e);
                    let li = document.createElement('li');
                    li.style.backgroundColor = (isEq ? objectTypeColor[3] : objectTypeColor[e.type]);
                    li.setAttribute('item-index', index);
                    li.setAttribute('original-index', item[0]);
                    li.innerText = (isEq ? `${g_equipMap.get(e[0]).name} Lv.${e[1]}` : e.formatBuffText());
                    amuletToStoreList.appendChild(li);
                });
            }

            function scheduleFreeCell() {
                let info = '背包已满,请选择至少一个背包内容暂时放入仓库以提供必要的操作空间。';
                function refreshOriginalBag() {
                    amuletToStoreList.innerHTML = '';
                    amuletToDestroyList.innerHTML = '';

                    originalFreeCell = 0;
                    queryObjects(originalBag = [], true, false, null, 0);
                    while (originalBag[originalBag.length - 1] == -1) {
                        originalBag.pop();
                        originalFreeCell++;
                    }
                    if (originalFreeCell == 0) {
                        alert(info);
                        scheduledObjects.equips = [];
                        scheduledObjects.amulets = [];

                        prepareBagList();
                        btnContinue.disabled = '';
                    }
                    else {
                        amuletToStoreList.parentNode.parentNode.children[0].innerText = '放入仓库';
                        amuletToDestroyList.parentNode.parentNode.children[0].innerText = '转换果核';

                        freeCell = originalFreeCell;
                        originalBag.sort((a, b) => a - b);
                        processNewAmulets();
                    }
                }

                let storeObjectsCount = (amuletToDestroyList?.children?.length ?? 0);
                if (originalFreeCell + storeObjectsCount == 0) {
                    alert(info);
                    btnContinue.disabled = '';
                    return;
                }
                else if (storeObjectsCount == 0) {
                    refreshOriginalBag();
                    return;
                }

                let indices = [];
                for (let li of amuletToDestroyList.children) {
                    indices.push(parseInt(li.getAttribute('original-index')));
                }
                indices.sort((a, b) => a - b);

                let ids = [];
                indices.forEach((i) => {
                    let e = originalBag[i][1];
                    let isEq = Array.isArray(e);
                    ids.push(isEq ? e[7] : e.id);
                    (isEq ? scheduledObjects.equips : scheduledObjects.amulets).push(e);
                });

                genericPopupShowInformationTips('临时放入仓库...', 0);
                beginMoveObjects(ids, g_object_move_path.bag2store, refreshBackpacks, refreshOriginalBag);
            }

            function restoreBag(closeCountDown) {
                function restoreCompletion() {
                    if (scheduledObjects.amulets.length > 0 || scheduledObjects.equips.length > 0) {
                        alert('部分背包内容无法恢复,请手动处理!');
                        console.log(scheduledObjects.equips);
                        console.log(scheduledObjects.amulets);
                        scheduledObjects.equips = [];
                        scheduledObjects.amulets = [];
                    }

                    if (closeCountDown > 0) {
                        genericPopupQuerySelector('#' + g_genericPopupInformationTipsId).previousSibling.innerText =
                            `操作完成,共获得 ${amuletsCollected} 个护符, ${coresCollected} 个果核`;
                        genericPopupShowInformationTips(`窗口将在 ${closeCountDown} 秒后关闭`, 0);

                        let timer = setInterval(() => {
                            if (--closeCountDown == 0) {
                                clearInterval(timer);
                                genericPopupClose(true);
                                window.location.reload();
                            }
                            else {
                                genericPopupShowInformationTips(`窗口将在 ${closeCountDown} 秒后关闭`, 0);
                            }
                        }, 1000);
                    }
                    else {
                        genericPopupClose(true);
                        window.location.reload();
                    }
                }

                if (scheduledObjects.equips.length == 0 && scheduledObjects.amulets.length == 0) {
                    restoreCompletion();
                }
                else {
                    genericPopupShowInformationTips('恢复背包内容...', 0);
                    beginRestoreObjects(null, scheduledObjects.amulets, scheduledObjects.equips, refreshBackpacks, restoreCompletion);
                }
            }

            let fixedContent =
                '<div style="width:100%;padding:10px 0px 0px 0px;font-size:16px;color:blue;"><b><span>双击条目进行分类间移动</span>' +
                  `<span id="${g_genericPopupInformationTipsId}" style="float:right;color:red;font-size:15px;"></span></b></div>`;
            let mainContent =
                '<div style="display:block;height:96%;width:100%;">' +
                  '<div style="position:relative;display:block;float:left;height:96%;width:48%;' +
                              'margin-top:10px;border:1px solid #000000;">' +
                    '<div style="display:block;width:100%;padding:5px;border-bottom:2px groove #d0d0d0;margin-bottom:10px;">放入仓库</div>' +
                    '<div style="position:absolute;display:block;height:1px;width:100%;overflow:scroll;">' +
                      '<ul id="amulet_to_store_list" style="cursor:pointer;"></ul></div></div>' +
                  '<div style="position:relative;display:block;float:right;height:96%;width:48%;' +
                              'margin-top:10px;border:1px solid #000000;">' +
                    '<div style="display:block;width:100%;padding:5px;border-bottom:2px groove #d0d0d0;margin-bottom:10px;">转换果核</div>' +
                    '<div style="position:absolute;display:block;height:1px;width:100%;overflow:scroll;">' +
                      '<ul id="amulet_to_destroy_list" style="cursor:pointer;"></ul></div></div></div>';

            genericPopupSetFixedContent(fixedContent);
            genericPopupSetContent('批量护符转换', mainContent);

            let amuletToStoreList = genericPopupQuerySelector('#amulet_to_store_list');
            let amuletToDestroyList = genericPopupQuerySelector('#amulet_to_destroy_list');
            amuletToStoreList.ondblclick = amuletToDestroyList.ondblclick = moveAmuletItem;

            while (originalBag[originalBag.length - 1] == -1) {
                originalBag.pop();
                originalFreeCell++;
            }
            if (originalBag.length > 0) {
                prepareBagList();
            }
            else {
                freeCell = originalFreeCell;
                genericPopupShowInformationTips('这会分批将海滩可熔炼装备转化为护符,请点击“继续”开始', 0);
            }

            let btnContinue = genericPopupAddButton(`继续 (剩余 ${originalBeach.length} 件装备 / ${originalFreeCell} 个背包空位)`,
                                                    0, processNewAmulets, true);
            let btnCloseOnBatch = genericPopupAddButton('本批完成后关闭', 0, (() => { originalBeach = []; processNewAmulets(); }), false);
            btnCloseOnBatch.disabled = 'disabled';
            genericPopupAddButton('关闭', 80, (() => { genericPopupClose(true); window.location.reload(); }), false);

            genericPopupSetContentSize(400, 700, false);
            genericPopupShowModal(false);

            divHeightAdjustment(amuletToStoreList.parentNode);
            divHeightAdjustment(amuletToDestroyList.parentNode);
        }

        let asyncOperations = 2;
        let equipment = null;
        let equipedbtn = null;
        let bag, store;
        beginReadObjects(
            bag = [],
            store = [],
            () => {
                equipedbtn = Array.from(bag.pop()).concat(Array.from(store.pop()));
                asyncOperations--;

                GM_xmlhttpRequest({
                    method: g_postMethod,
                    url: g_readUrl,
                    headers: g_postHeader,
                    data: 'f=9',
                    onload: response => {
                        let div0 = document.createElement('div');
                        div0.innerHTML = response.responseText;

                        equipedbtn = equipedbtn.concat(Array.from(div0.querySelectorAll('div.row > div.fyg_tc > button.fyg_mp3')));
                        equipedbtn.sort(objectNodeComparer);
                        equipment = equipmentNodesToInfoArray(equipedbtn);

                        document.getElementById('analyze-indicator').innerText = '分析完成';
                        asyncOperations--;
                    }
                });
            },
            null);

        //分析装备并显示属性
        var g_expandingEquipment = false;
        function expandEquipment(equipment) {
            if (g_expandingEquipment || !(equipedbtn?.length > 0) || !(equipment?.length > 0) || equipment[0] == -1) {
                document.getElementById('analyze-indicator').innerText = '分析完成';
                return;
            }

            let beach_copy = document.getElementById('beach_copy');
            if (beach_copy == null) {
                let beachall = document.getElementById('beachall');
                beach_copy = beachall.cloneNode();
                beachall.style.display = 'none';
                beach_copy.id = 'beach_copy';
                beach_copy.style.backgroundColor = beach_BG ? 'black' : 'white';
                beachall.parentNode.insertBefore(beach_copy, beachall);

                (new MutationObserver((mList) => {
                    if (!g_expandingEquipment && mList?.length == 1 && mList[0].type == 'childList' &&
                        mList[0].addedNodes?.length == 1 && !(mList[0].removedNodes?.length > 0)) {

                        let node = mList[0].addedNodes[0];
                        if (node.hasAttribute('role')) {
                            node.remove();
                        }
                        else if (node.className?.indexOf('popover') >= 0) {
                            node.setAttribute('id', 'id_temp_apply_beach_BG');
                            changeBeachStyle('id_temp_apply_beach_BG', beach_BG);
                            node.removeAttribute('id');
                            if (node.className?.indexOf('popover-') < 0) {
                                let content = node.querySelector('.popover-content');
                                content.style.borderRadius = '5px';
                                content.style.border = (beach_BG ? '4px double white' : '4px double black');
                            }
                        }
                    }
                })).observe(beach_copy, { childList : true });
            }

            g_expandingEquipment = true;
            copyBeach(beach_copy);

            let udata = loadUserConfigData();
            if (udata.dataBeachSift == null) {
                udata.dataBeachSift = {};
                saveUserConfigData(udata);
            }

            let settings = {};
            for (let abbr in udata.dataBeachSift) {
                let checks = udata.dataBeachSift[abbr].split(',');
                if (checks?.length == 5) {
                    let setting = [];
                    checks.forEach((checked) => { setting.push(checked.trim().toLowerCase() == 'true'); });
                    settings[abbr] = setting;
                }
            }

            const defaultSetting = [ false, false, false, false, false ];
            beach_copy.querySelectorAll('.btn.fyg_mp3').forEach((btn) => {
                let e = equipmentInfoParseNode(btn);
                if (e != null) {
                    let isExpanding = false;
                    let eqLv = equipmentGetLevel(btn);
                    if (forceExpand || eqLv > 2 || btn.getAttribute('data-content')?.match(/\[神秘属性\]/) != null) {
                        isExpanding = true;
                    }
                    else {
                        let setting = (settings[e[0]] ?? defaultSetting);
                        if (!setting[0]) {
                            let isFind = false;
                            for (let j = 0; j < equipment.length; j++) {
                                if (equipment[j][0] == e[0] && !(ignoreStoreMysEquip && equipment[j][6] == 1)) {
                                    isFind = true;
                                    let e1 = [ parseInt(e[1]), parseInt(e[2]), parseInt(e[3]), parseInt(e[4]), parseInt(e[5]) ];
                                    let e2 = [ parseInt(equipment[j][1]), parseInt(equipment[j][2]), parseInt(equipment[j][3]),
                                               parseInt(equipment[j][4]), parseInt(equipment[j][5]) ];
                                    if (isExpanding = defaultEquipmentNodeComparer(setting, e[0], e1, e2)) {
                                        break;
                                    }
                                }
                            }
                            if (!isFind) {
                                isExpanding = true;
                            }
                        }
                    }
                    if (isExpanding) {
                        let btn0 = document.createElement('button');
                        btn0.className = `btn btn-light ${g_equipmentLevelTipClass[eqLv]}`;
                        btn0.style.minWidth = '200px';
                        btn0.style.padding = '0px';
                        btn0.style.marginBottom = '5px';
                        btn0.style.textAlign = 'left';
                        btn0.style.boxShadow = 'none';
                        btn0.style.lineHeight = '150%';
                        btn0.setAttribute('data-toggle', 'popover');
                        btn0.setAttribute('data-trigger', 'hover');
                        btn0.setAttribute('data-placement', 'bottom');
                        btn0.setAttribute('data-html', 'true');
                        btn0.setAttribute('onclick', btn.getAttribute('onclick'));

                        let popover = document.createElement('div');
                        popover.innerHTML = '<style> .popover { max-width:100%; } </style>';
                        let eqMeta = g_equipMap.get(e[0]);
                        equipedbtn.forEach((eqbtn) => {
                            if (eqMeta.index == parseInt(eqbtn.dataset.metaIndex)) {
                                let btn1 = document.createElement('button');
                                btn1.className = `btn btn-light ${g_equipmentLevelTipClass[equipmentGetLevel(eqbtn)]}`;
                                btn1.style.cssText =
                                    'min-width:180px;padding:10px 5px 0px 5px;text-align:left;box-shadow:none;background-color:none;' +
                                    'line-height:120%;border-width:3px;border-style:double;margin-right:5px;margin-bottom:5px;';
                                btn1.innerHTML = eqbtn.dataset.content;
                                if (btn1.lastChild.nodeType == 3) { //清除背景介绍文本
                                    btn1.lastChild.remove();
                                }
                                if (btn1.lastChild.className.indexOf('bg-danger') != -1) {
                                    btn1.lastChild.style.cssText = 'max-width:180px;padding:3px;white-space:pre-line;word-break:break-all;';
                                }
                                popover.appendChild(btn1);
                            }
                        });
                        btn0.setAttribute('data-content', popover.innerHTML);
                        btn0.innerHTML =
                            `<h3 class="popover-title" style="background-color:${getComputedStyle(btn0).getPropertyValue('background-color')}">` +
                            `${btn.dataset.originalTitle}</h3>` +
                            `<div class="popover-content-show" style="padding:10px 10px 0px 10px;">${btn.dataset.content}</div>`;
                        beach_copy.insertBefore(btn0, btn.nextSibling);
                    }
                }
            });

            $(function() {
                $('#beach_copy .btn[data-toggle="popover"]').popover();
            });
            $('#beach_copy .bg-danger.with-padding').css({
                'max-width': '200px',
                'padding': '5px',
                'white-space': 'pre-line',
                'word-break': 'break-all'
            });

            changeBeachStyle('beach_copy', beach_BG);
            document.getElementById('analyze-indicator').innerText = '分析完成';
            g_expandingEquipment = false;
        }

        function changeBeachStyle(container, bg)
        {
            $(`#${container}`).css({
                'background-color': bg ? 'black' : 'white'
            });
            $(`#${container} .popover-content-show`).css({
                'background-color': bg ? 'black' : 'white'
            });
            $(`#${container} .btn-light`).css({
                'background-color': bg ? 'black' : 'white'
            });
            $(`#${container} .popover-title`).css({
                'color': bg ? 'black' : 'white'
            });
            $(`#${container} .pull-right`).css({
                'color': bg ? 'black' : 'white'
            });
            $(`#${container} .bg-danger.with-padding`).css({
                'color': bg ? 'black' : 'white'
            });
        }

        //等待海滩装备加载
        let beachTimer = setInterval(() => {
            if ($('#beachall .btn').length != 0) {
                clearInterval(beachTimer);
                //等待装备读取完成
                let equipTimer = setInterval(() => {
                    if (asyncOperations == 0) {
                        clearInterval(equipTimer);

                        document.getElementById('analyze-indicator').innerText = '正在分析...';
                        setTimeout(() => { expandEquipment(equipment); }, 50);

                        (new MutationObserver(() => {
                            document.getElementById('analyze-indicator').innerText = '正在分析...';
                            setTimeout(() => { expandEquipment(equipment); }, 50);
                        })).observe(document.getElementById('beachall'), { childList : true });
                    }
                }, 500);
            }
        }, 500);

        function copyBeach(beach_copy) {
            beach_copy.innerHTML = '';
            Array.from(document.getElementById('beachall').children).sort(sortBeach).forEach((node) => {
                beach_copy.appendChild(node.cloneNode(true));
            });
        }

        function sortBeach(a, b) {
            let delta = equipmentGetLevel(a) - equipmentGetLevel(b);
            if (delta == 0) {
                if ((delta = parseInt(a.innerText.match(/\d+/)[0]) - parseInt(b.innerText.match(/\d+/)[0])) == 0) {
                    delta = (a.getAttribute('data-original-title') < b.getAttribute('data-original-title') ? -1 : 1);
                }
            }
            return -delta;
        }

        document.querySelector('body').style.paddingBottom = '1000px';
    }
    else if (window.location.pathname == '/fyg_pk.php') {
        let pkConfigDiv = document.createElement('div');
        pkConfigDiv.style.className = 'panel-heading';
        pkConfigDiv.style.float = 'right';
        pkConfigDiv.style.padding = '5px 15px';
        pkConfigDiv.style.color = 'white';
        pkConfigDiv.innerHTML =
            `<label for="indexRallyCheckbox" style="margin-right:5px;cursor:pointer;">为攻击回合加注索引</label>
             <input type="checkbox" id="indexRallyCheckbox" style="margin-right:15px;" />
             <label for="keepPkRecordCheckbox" style="margin-right:5px;cursor:pointer;">暂时保持战斗记录</label>
             <input type="checkbox" id="keepPkRecordCheckbox" style="margin-right:15px;" />
             <label for="autoTaskEnabledCheckbox" style="margin-right:5px;cursor:pointer;">允许执行自定义任务</label>
             <input type="checkbox" id="autoTaskEnabledCheckbox" />`;

        let indexRally = pkConfigDiv.querySelector('#indexRallyCheckbox').checked =
            (localStorage.getItem(g_indexRallyStorageKey) == 'true');
        pkConfigDiv.querySelector('#indexRallyCheckbox').onchange = (() => {
            localStorage.setItem(g_indexRallyStorageKey, indexRally = pkConfigDiv.querySelector('#indexRallyCheckbox').checked);
        });

        let keepPkRecord = pkConfigDiv.querySelector('#keepPkRecordCheckbox').checked =
            (localStorage.getItem(g_keepPkRecordStorageKey) == 'true');
        pkConfigDiv.querySelector('#keepPkRecordCheckbox').onchange = (() => {
            localStorage.setItem(g_keepPkRecordStorageKey, keepPkRecord = pkConfigDiv.querySelector('#keepPkRecordCheckbox').checked);
        });

        let autoTaskEnabled = pkConfigDiv.querySelector('#autoTaskEnabledCheckbox').checked =
            (localStorage.getItem(g_autoTaskEnabledStorageKey) == 'true');
        pkConfigDiv.querySelector('#autoTaskEnabledCheckbox').onchange = (() => {
            localStorage.setItem(g_autoTaskEnabledStorageKey, autoTaskEnabled = pkConfigDiv.querySelector('#autoTaskEnabledCheckbox').checked);
            window.location.reload();
        });

        let panel = document.getElementsByClassName('panel panel-primary');
        panel[1].insertBefore(pkConfigDiv, panel[1].children[0]);

        let div0_pk_text_more = document.createElement('div');
        div0_pk_text_more.setAttribute('id', 'pk_text_more');
        div0_pk_text_more.setAttribute('class', 'panel-body');
        document.getElementsByClassName('panel panel-primary')[1].appendChild(div0_pk_text_more);

        let addingRallyIndices = false;
        let pkText = document.querySelector('#pk_text').innerHTML;
        (new MutationObserver(() => {
            if (addingRallyIndices) {
                return;
            }
            else if (indexRally) {
                addingRallyIndices = true;
                let turn_l = 0;
                let turn_r = 0;
                document.querySelectorAll('#pk_text p.bg-special').forEach((e, i) => {
                    let myTurn = (e.className.indexOf('fyg_tr') >= 0);
                    let rally = document.createElement('b');
                    rally.innerText = (myTurn ? `${i + 1} (${++turn_l})` : `(${++turn_r}) ${i + 1}`);
                    rally.style.float = (myTurn ? 'left' : 'right');
                    rally.style.marginLeft = rally.style.marginRight = '10px';
                    e.appendChild(rally);
                });
                addingRallyIndices = false;
            }
            if (keepPkRecord) {
                document.querySelector('#pk_text_more').innerHTML = pkText + document.querySelector('#pk_text_more').innerHTML;
                pkText = document.querySelector('#pk_text').innerHTML;
                $('#pk_text_more .btn[data-toggle="tooltip"]').tooltip();
            }
        })).observe(document.querySelector('#pk_text'), { characterData : true , childList : true });

        if (autoTaskEnabled) {
            let btngroup0 = document.createElement('div');
            btngroup0.setAttribute('class', 'action_selector');
            btngroup0.innerHTML = `<p></p><div class="btn-group" role="group">
                  <button type="button" class="btn btn-secondary">0</button>
                  <button type="button" class="btn btn-secondary">10</button>
                  <button type="button" class="btn btn-secondary">20</button>
                  <button type="button" class="btn btn-secondary">30</button>
                  <button type="button" class="btn btn-secondary">40</button>
                  <button type="button" class="btn btn-secondary">50</button>
                  <button type="button" class="btn btn-secondary">60</button>
                  <button type="button" class="btn btn-secondary">70</button>
                  <button type="button" class="btn btn-secondary">80</button>
                  <button type="button" class="btn btn-secondary">90</button>
                  <button type="button" class="btn btn-secondary">100</button>
                </div>`;
            let btngroup1 = document.createElement('div');
            btngroup1.setAttribute('class', 'action_selector');
            btngroup1.innerHTML = `<p></p><div class="btn-group" role="group">
                  <button type="button" class="btn btn-secondary">0</button>
                  <button type="button" class="btn btn-secondary">5</button>
                  <button type="button" class="btn btn-secondary">10</button>
                  <button type="button" class="btn btn-secondary">15</button>
                  <button type="button" class="btn btn-secondary">20</button>
                  <button type="button" class="btn btn-secondary">25</button>
                  <button type="button" class="btn btn-secondary">30</button>
                  <button type="button" class="btn btn-secondary">35</button>
                  <button type="button" class="btn btn-secondary">40</button>
                  <button type="button" class="btn btn-secondary">45</button>
                  <button type="button" class="btn btn-secondary">50</button>
                  <button type="button" class="btn btn-secondary">55</button>
                  <button type="button" class="btn btn-secondary">60</button>
                  <button type="button" class="btn btn-secondary">65</button>
                  <button type="button" class="btn btn-secondary">70</button>
                  <button type="button" class="btn btn-secondary">75</button>
                  <button type="button" class="btn btn-secondary">80</button>
                  <button type="button" class="btn btn-secondary">85</button>
                  <button type="button" class="btn btn-secondary">90</button>
                  <button type="button" class="btn btn-secondary">95</button>
                  <button type="button" class="btn btn-secondary">100</button>
                </div>`;
            let btngroup2 = document.createElement('div');
            btngroup2.setAttribute('class', 'action_selector');
            btngroup2.innerHTML = `<p></p><div class="btn-group" role="group">
                  <button type="button" class="btn btn-secondary">0</button>
                  <button type="button" class="btn btn-secondary">5</button>
                  <button type="button" class="btn btn-secondary">10</button>
                  <button type="button" class="btn btn-secondary">15</button>
                  <button type="button" class="btn btn-secondary">20</button>
                  <button type="button" class="btn btn-secondary">25</button>
                  <button type="button" class="btn btn-secondary">30</button>
                  <button type="button" class="btn btn-secondary">35</button>
                  <button type="button" class="btn btn-secondary">40</button>
                  <button type="button" class="btn btn-secondary">45</button>
                  <button type="button" class="btn btn-secondary">50</button>
                  <button type="button" class="btn btn-secondary">55</button>
                  <button type="button" class="btn btn-secondary">60</button>
                  <button type="button" class="btn btn-secondary">65</button>
                  <button type="button" class="btn btn-secondary">70</button>
                  <button type="button" class="btn btn-secondary">75</button>
                  <button type="button" class="btn btn-secondary">80</button>
                  <button type="button" class="btn btn-secondary">85</button>
                  <button type="button" class="btn btn-secondary">90</button>
                  <button type="button" class="btn btn-secondary">95</button>
                  <button type="button" class="btn btn-secondary">100</button>
                </div>`;

            let taskObserver = new MutationObserver(() => {
                if (document.getElementsByClassName('btn-secondary').length == 0) {
                    let addbtn = setInterval(() => {
                        let col = document.querySelector('#pklist > div > div.col-md-8');
                        if (col != null) {
                            clearInterval(addbtn);

                            let obtns = document.getElementsByClassName('btn-block dropdown-toggle fyg_lh30');
                            col.insertBefore(btngroup0, obtns[0]);
                            col.insertBefore(btngroup1, obtns[1]);
                            col.insertBefore(btngroup2, obtns[2]);

                            if (document.getElementsByClassName('btn-outline-secondary').length == 0) {
                                if (localStorage.getItem('dataReward') == null) {
                                    localStorage.setItem('dataReward', '{"sumShell":"0","sumExp":"0"}');
                                }

                                let ok = document.createElement('div');
                                ok.innerHTML = `<p></p><button type="button" class="btn btn-outline-secondary">任务执行</button>`;
                                col.appendChild(ok);

                                function gobattle() {
                                    let times = [ 0, 0, 0 ];
                                    let sum = 0;
                                    $('.btn-secondary').each(function(i, e) {
                                        if ($(e).attr('style') != null && $(e).css('background-color') == 'rgb(135, 206, 250)') {
                                            let a = parseInt(e.innerText);
                                            let b = $('.btn-group .btn-secondary').index(e);
                                            sum += a;
                                            if (b < 11) {
                                                times[0] = a / 10;
                                            } else if (b >= 11 && b < 32) {
                                                times[1] = a / 5;
                                            } else if (b >= 32) {
                                                times[2] = a / 5;
                                            }
                                        }
                                    });

                                    if (sum <= parseInt(document.getElementsByClassName('fyg_colpz03')[0].innerText)) {
                                        let gox_data = getPostData(/gox\(\)\{[\s\S]*\}/m, /data: ".*"/).slice(7, -1);

                                        let dataReward = JSON.parse(localStorage.getItem('dataReward'));
                                        let sum0 = parseInt(dataReward.sumShell);
                                        let sum1 = parseInt(dataReward.sumExp);

                                        function parseGainResponse(response)
                                        {
                                            let gainText = '';
                                            if (response.startsWith('<p>获得了</p>')) {
                                                let gain;
                                                let sp = '获得';
                                                let regex = /<span class="fyg_f18">x\s*(\d+)\s*([^<]+)<\/span>/g;
                                                while ((gain = regex.exec(response))?.length == 3) {
                                                    gainText += `${sp}${gain[2].trim()}:${gain[1]}`;
                                                    sp = ', ';
                                                }
                                                let lvlUp = response.match(/角色 \[ [^\s]+ \] 卡片等级提升!/g);
                                                if (lvlUp?.length > 0) {
                                                    lvlUp.forEach((e) => {
                                                        gainText += `${sp}${e}`;
                                                        sp = ', ';
                                                    });
                                                }
                                            }
                                            return gainText;
                                        }

                                        function func0(time) {
                                            if (time == 0) {
                                                if (times[0] != 0) {
                                                    GM_xmlhttpRequest({
                                                        method: g_postMethod,
                                                        url: g_readUrl,
                                                        headers: g_postHeader,
                                                        data: 'f=12',
                                                        onload: response => {
                                                            let ap = response.responseText.match(/class="fyg_colpz03" style="font-size:32px;font-weight:900;">\d+</)[0].match(/>\d+</)[0].slice(1, -1);
                                                            document.getElementsByClassName('fyg_colpz03')[0].innerText = ap;
                                                            let rankp = response.responseText.match(/class="fyg_colpz02" style="font-size:32px;font-weight:900;">\d+%</)[0].match(/\d+%/)[0];
                                                            document.getElementsByClassName('fyg_colpz02')[0].innerText = rankp;
                                                            let div_sum = document.createElement('div');
                                                            div_sum.innerText = `贝壳总次数:经验总次数=${sum0}:${sum1}=${(sum0 == 0 || sum1 == 0) ? 'undefined' : (sum0 / sum1).toFixed(4)}`;
                                                            dataReward.sumShell = sum0;
                                                            dataReward.sumExp = sum1;
                                                            localStorage.setItem('dataReward', JSON.stringify(dataReward));
                                                            document.getElementsByClassName('btn-outline-secondary')[0].parentNode.appendChild(div_sum);
                                                            times[0] = 0;
                                                        }
                                                    });
                                                }
                                                return;
                                            }
                                            GM_xmlhttpRequest({
                                                method: g_postMethod,
                                                url: g_postUrl,
                                                headers: g_postHeader,
                                                data: gox_data,
                                                onload: response => {
                                                    let gainText = parseGainResponse(response.responseText);
                                                    if (gainText.length > 0) {
                                                        let div_info = document.createElement('div');
                                                        div_info.innerText = gainText;
                                                        document.getElementsByClassName('btn-outline-secondary')[0].parentNode.appendChild(div_info);
                                                        if (gainText.indexOf('贝壳') != -1) {
                                                            sum0 += 1;
                                                        }
                                                        if (gainText.indexOf('经验') != -1) {
                                                            sum1 += 1;
                                                        }
                                                        func0(time - 1);
                                                    } else {
                                                        let div_info = document.createElement('div');
                                                        div_info.innerText = '段位进度不足或无法识别的应答信息';
                                                        document.getElementsByClassName('btn-outline-secondary')[0].parentNode.appendChild(div_info);
                                                        func0(0);
                                                    }
                                                }
                                            });
                                        }

                                        function func1(time) {
                                            if (time == 0) {
                                                times[1] = 0;
                                                return;
                                            }
                                            let observerPk = new MutationObserver((mutationsList, observer) => {
                                                let isPk = 0;
                                                for (let mutation of mutationsList) {
                                                    if (mutation.type == 'childList') {
                                                        isPk = 1;
                                                    }
                                                }
                                                if (isPk) {
                                                    observerPk.disconnect();
                                                    func1(time - 1);
                                                }
                                            });
                                            observerPk.observe(document.querySelector('#pk_text'), { characterData : true , childList : true });
                                            jgjg(1);
                                        }

                                        function func2(time) {
                                            if (time == 0) {
                                                times[2] = 0;
                                                return;
                                            }
                                            let observerPk = new MutationObserver((mutationsList, observer) => {
                                                let isPk = 0;
                                                for (let mutation of mutationsList) {
                                                    if (mutation.type == 'childList') {
                                                        isPk = 1;
                                                    }
                                                }
                                                if (isPk) {
                                                    observerPk.disconnect();
                                                    func2(time - 1);
                                                }
                                            });
                                            observerPk.observe(document.querySelector('#pk_text'), { characterData : true , childList : true });
                                            jgjg(2);
                                        }
                                        func0(times[0]);
                                        let waitFor0 = setInterval(() => {
                                            if (times[0] == 0) {
                                                clearInterval(waitFor0);
                                                func1(times[1]);
                                            }
                                        }, 1000);
                                        let waitFor1 = setInterval(() => {
                                            if (times[0] == 0 && times[1] == 0) {
                                                clearInterval(waitFor1);
                                                func2(times[2]);
                                            }
                                        }, 1000);
                                    } else {
                                        alert('体力不足');
                                    }
                                }
                                document.getElementsByClassName('btn-outline-secondary')[0].addEventListener('click', gobattle, false);
                            }
                            function selector_act() {
                                var btnNum = $('.btn-group .btn-secondary').index(this);
                                $('.btn-group .btn-secondary')
                                    .eq(btnNum)
                                    .css('background-color', 'rgb(135, 206, 250)')
                                    .siblings('.btn-group .btn-secondary')
                                    .css('background-color', 'rgb(255, 255, 255)');
                            }
                            let btnselector = document.getElementsByClassName('btn-secondary');
                            for (let i = 0; i < btnselector.length; i++) {
                                btnselector[i].addEventListener('click', selector_act, false);
                            }
                        }
                    }, 1000);
                }
            });
            taskObserver.observe(document.getElementsByClassName('panel panel-primary')[0], { childList : true, subtree : true, });
        }
    }
    else if (window.location.pathname == '/fyg_wish.php') {
        let timer = setInterval(() => {
            let wishPoints = parseInt(document.getElementById('xys_dsn')?.innerText);
            if (!isNaN(wishPoints)) {
                clearInterval(timer);

                for (let title of document.getElementsByClassName('panel-heading')) {
                    if (title.innerText.indexOf('我的愿望') >= 0) {
                        let div = document.createElement('div');
                        div.style.float = 'right';
                        div.innerHTML =
                            '<label for="ignoreWishpoolExpirationCheckbox" style="margin-right:5px;cursor:pointer;">禁止许愿池过期提醒</label>' +
                            '<input type="checkbox" id="ignoreWishpoolExpirationCheckbox" />';

                        div.querySelector('#ignoreWishpoolExpirationCheckbox').checked =
                            (localStorage.getItem(g_ignoreWishpoolExpirationStorageKey) == 'true');
                        div.querySelector('#ignoreWishpoolExpirationCheckbox').onchange = (() => {
                            localStorage.setItem(g_ignoreWishpoolExpirationStorageKey,
                                                 div.querySelector('#ignoreWishpoolExpirationCheckbox').checked);
                            window.location.reload();
                        });

                        title.appendChild(div);
                        break;
                    }
                }

                function getWishPoints() {
                    let text = 'WISH';
                    for (let i = 7; i <= 13; i++) {
                        text += (' ' + (document.getElementById('xyx_' + ('0' + i).slice(-2))?.innerText ?? '0'));
                    }
                    return text;
                }

                let div = document.createElement('div');
                div.className = 'row';
                div.innerHTML =
                    '<div class="panel panel-info"><div class="panel-heading"> 计算器许愿点设置 (' +
                    '<a href="#" id="copyWishPoints">点击这里复制到剪贴板</a>)</div>' +
                    '<input type="text" class="panel-body" id="calcWishPoints" readonly="true" ' +
                           'style="border:none;outline:none;" value="" /></div>';

                let calcWishPoints = div.querySelector('#calcWishPoints');
                calcWishPoints.value = getWishPoints();

                let xydiv = document.getElementById('xydiv');
                xydiv.parentNode.parentNode.insertBefore(div, xydiv.parentNode.nextSibling);

                div.querySelector('#copyWishPoints').onclick = ((e) => {
                    calcWishPoints.select();
                    if (document.execCommand('copy')) {
                        e.target.innerText = '许愿点设置已复制到剪贴板';
                    }
                    else {
                        e.target.innerText = '复制失败,这可能是因为浏览器没有剪贴板访问权限,请进行手工复制';
                    }
                });

                (new MutationObserver(() => {
                    calcWishPoints.value = getWishPoints();
                })).observe(xydiv, { subtree : true , childList : true , characterData : true });
            }
        }, 500);
    }
})();

QingJ © 2025

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