咕咕镇数据采集

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

目前为 2022-06-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         咕咕镇数据采集
// @namespace    https://gf.qytechs.cn/users/448113
// @version      1.4.15
// @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-06-05 14:30:00';

    const g_kfUser = document.getElementsByClassName('icon-user')[0].parentNode.innerText.split(' ')[1];
    const g_autoTaskEnabledStorageKey = g_kfUser + '_autoTaskEnabled';
    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_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_keepPkRecordStorageKey, g_amuletGroupsStorageKey,
                                         g_equipmentExpandStorageKey, g_equipmentBGStorageKey,
                                         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) {
        // just in case, i discovered that sometimes if we use array.length directly in for(...) statement
        // (either some other statements too), the statement get chances to be executed incorrectly (or just
        // console.log can be effected?), don't know why, but we can use a temporary variable to handle this.
        let newObjects = [];
        let nl = (newArray?.length ?? 0);
        for (let i = 0; i < nl; i++) {
            if (searchElement(oldArray, newArray[i], fnComparer) < 0) {
                newObjects.push(findIndices ? i : newArray[i]);
            }
        }
        return newObjects;
    }

    // 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(bagIds, storeIds, key, ignoreEmptyCell, fnFurtherProcess, fnParams) {
        if (bagIds != null || storeIds != 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 (bagIds != null) {
                        objectIdParseNodes(div.children[0].children[1].children, bagIds, key, ignoreEmptyCell);
                    }
                    if (storeIds != null) {
                        objectIdParseNodes(div.children[1].children[1].children, storeIds, key, ignoreEmptyCell);
                    }

                    if (fnFurtherProcess != null) {
                        fnFurtherProcess(fnParams);
                    }
                }
            });
            httpRequestRegister(request);
        }
        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(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) {
                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);
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // 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_amuletBuffUnits = [ '点', '%', '%' ];
    const g_amuletBuffTypeNames = [ '力量', '敏捷', '智力', '体魄', '精神', '意志',
                                    '物理攻击', '魔法攻击', '速度', '生命护盾回复效果', '最大生命值', '最大护盾值',
                                    '固定生命偷取', '固定反伤', '固定暴击几率', '固定技能几率', '物理防御效果', '魔法防御效果' ];
    const g_amuletBuffShortMarks = [ 'STR', 'AGI', 'INT', 'VIT', 'SPR', 'MND',
                                     'PATK', 'MATK', 'SPD', 'REC', 'HP', 'SLD',
                                     'LCH', 'RFL', 'CRT', 'SKL', 'PDEF', 'MDEF' ];

    const g_amuletBuffTypeOrders = new Map();
    g_amuletBuffTypeNames.forEach((item, index) => { g_amuletBuffTypeOrders.set(item, index); });

    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_amuletBuffTypeOrders.get(nv[0]) % 6)) * 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) {
                        this.buffCode += ((100 ** (g_amuletBuffTypeOrders.get(attr[1]) % 6)) * attr[2]);
                        this.text += `${this.text.length > 0 ? ', ' : ''}${attr[1]} +${attr[2]} ${g_amuletBuffUnits[this.type]}`;
                    }
                    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 type = this.type * 6;
                g_amuletBuffTypeNames.slice(type, type + 6).forEach((buff) => {
                    let v = Math.trunc(this.buffCode / (100 ** (g_amuletBuffTypeOrders.get(buff) % 6))) % 100;
                    if (v > 0) {
                        buffs[buff] = v;
                    }
                });
            }
            return buffs;
        });

        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_amuletBuffUnits[this.type]}`;
                }
            }
            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_amuletBuffShortMarks[g_amuletBuffTypeOrders.get(buff)]);
                }
                return this.formatName() + ' = ' + text;
            }
            return null;
        });

        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_amuletBuffTypeOrders.get(tbuff[0]) - g_amuletBuffTypeOrders.get(obuff[0])) != 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.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.type != b.type ? a.type - b.type : a.buffCode - b.buffCode);
                    for ( ; amulets.length > 0 && i < al; i++) {
                        let mi = searchElement(amulets, this.items[i], (a, b) => a.type != b.type ? a.type - b.type : a.buffCode - b.buffCode);
                        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.type != b.type ? a.type - b.type : a.buffCode - b.buffCode);
                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.type != b.type ? a.type - b.type : a.buffCode - b.buffCode)) >= 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) => {
            if (this.isValid()) {
                let str = '';
                let nl = '';
                g_amuletBuffTypeNames.forEach((buff) => {
                    let v = this.buffSummary[buff];
                    if (v > 0) {
                        str += `${nl}${linePrefix}${buff} +${v} ${g_amuletBuffUnits[Math.trunc(g_amuletBuffTypeOrders.get(buff) / 6)]}${lineSuffix}`;
                        nl = lineSeparator;
                    }
                });
                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_amuletBuffTypeNames.forEach((buff) => {
                    if (this.buffSummary[buff] > 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) {
        let amulet;
        for (let node of nodes) {
            if (objectMatchTitle(node, key) && (amulet ??= new Amulet()).fromNode(node)?.isValid()) {
                array.push(amulet);
                amulet = null;
            }
        }
    }

    function beginReadAmulets(bagAmulets, storeAmulets, key, fnFurtherProcess, fnParams) {
        if (bagAmulets != null || storeAmulets != 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 (bagAmulets != null) {
                        amuletNodesToArray(div.children[0].children[1].children, bagAmulets, key);
                    }
                    if (storeAmulets != null) {
                        amuletNodesToArray(div.children[1].children[1].children, storeAmulets, key);
                    }

                    if (fnFurtherProcess != null) {
                        fnFurtherProcess(fnParams);
                    }
                }
            });
            httpRequestRegister(request);
        }
        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, store, null);
            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, bag, null);
            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 });
        }
    }

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

        let objects = [];
        if (bag?.length > 0) {
            objectIdParseNodes(bag, objects, key, true);
            beginClearBagObjects({ objects : objects, proc : fnFurtherProcess, params : fnParams });
        }
        else {
            beginReadObjects(objects, null, key, true, beginClearBagObjects,
                             { objects : objects, proc : fnFurtherProcess, params : 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_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>`;

    if (document.getElementById(g_genericPopupContainerId) == null) {
        let genericPopupContainer = document.createElement('div');
        genericPopupContainer.id = g_genericPopupContainerId;
        genericPopupContainer.innerHTML = g_genericPopupHTML;
        document.body.appendChild(genericPopupContainer);
    }

    function genericPopupInitialize() {
        document.getElementById(g_genericPopupContainerId).innerHTML = g_genericPopupHTML;
    }

    function genericPopupReset() {
        let fixedContent = document.getElementById(g_genericPopupFixedContentId);
        fixedContent.style.display = 'none';
        fixedContent.innerHTML = '';

        document.getElementById(g_genericPopupTitleTextId).innerText = '';
        document.getElementById(g_genericPopupContentId).innerHTML = '';
        document.getElementById(g_genericPopupTitleButtonContainerId).innerHTML = '';
        document.getElementById(g_genericPopupFootButtonContainerId).innerHTML = '';
    }

    function genericPopupSetContent(title, content) {
        document.getElementById(g_genericPopupTitleTextId).innerText = title;
        document.getElementById(g_genericPopupContentId).innerHTML = content;
    }

    function genericPopupSetFixedContent(content) {
        let fixedContent = document.getElementById(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';
        }

        document.getElementById(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%');

        document.getElementById(g_genericPopupContentContainerId).style.width
            = width + (width.endsWith('px') || width.endsWith('%') ? '' : 'px');

        let content = document.getElementById(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 = document.getElementById(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 = document.getElementById(g_genericPopupId);
        popup.style.display = "none";

        if (reset) {
            genericPopupReset();
        }

        httpRequestClearAll();
    }

    function genericPopupShowProgressMessage(progressMessage) {
        genericPopupClose(false);

        document.getElementById(g_genericPopupProgressContentId).innerText = (progressMessage?.length > 0 ? progressMessage : '请稍候…');
        document.getElementById(g_genericPopupProgressId).style.display = "flex";
    }

    function genericPopupCloseProgressMessage() {
        document.getElementById(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 = document.getElementById(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 = document.getElementById(g_genericPopupTaskItemId + index)?.lastChild;
        if (item != undefined) {
            item.innerText = state;
        }
    }

    function genericPopupTaskComplete(index, error) {
        let li = document.getElementById(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 : -1 , type : 0 , name : '物理攻击' },
        { index : -1 , type : 0 , name : '魔法攻击' },
        { index : -1 , type : 0 , name : '攻击速度' },
        { index : -1 , type : 0 , name : '最大生命' },
        { index : -1 , type : 0 , name : '最大护盾' },
        { index : -1 , type : 1 , name : '附加物伤' },
        { index : -1 , type : 1 , name : '附加魔伤' },
        { index : -1 , type : 1 , name : '附加攻速' },
        { index : -1 , type : 1 , name : '附加生命' },
        { index : -1 , type : 1 , name : '附加护盾' },
        { index : -1 , type : 1 , name : '附加回血' },
        { index : -1 , type : 1 , name : '附加回盾' },
        { index : -1 , type : 0 , name : '护盾回复' },
        { index : -1 , type : 0 , name : '物理穿透' },
        { index : -1 , type : 0 , name : '魔法穿透' },
        { index : -1 , type : 0 , name : '暴击穿透' },
        { index : -1 , type : 1 , name : '附加物穿' },
        { index : -1 , type : 1 , name : '附加物防' },
        { index : -1 , type : 1 , name : '附加魔防' },
        { index : -1 , type : 1 , name : '物理减伤' },
        { index : -1 , type : 1 , name : '魔法减伤' },
        { index : -1 , type : 0 , name : '生命偷取' },
        { index : -1 , type : 0 , 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[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 : 30 , 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[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[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((item, index) => {
            let d = Math.trunc((eq1[0] * item.factor + item.additive) * eq1[index + 1]) -
                    Math.trunc((eq2[0] * item.factor + item.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);
    }

    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);
            })
        } ];

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

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

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

    function initiatizeConfig() {
        let udata = getUserData();
        if (udata == null) {
            udata = {
                dataIndex : { battleInfoNow : '' , battleInfoBefore : '' , battleInfoBack : '' },
                dataBind : {},
                config : {}
            };
        }
        else if (udata.config == null) {
            udata.config = {};
        }

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

        for (let key in udata.dataBind) {
            if (g_roleMap.get(key) == undefined) {
                delete udata.dataBind[key];
            }
        }

        setUserData(udata);
    }

    function readEquipmentDOM(responseText) {
        let div0 = document.createElement('div');
        div0.innerHTML = `${responseText}</div>`;
        div0.innerHTML = `${div0.children[0].children[1].innerHTML}${div0.children[1].children[1].innerHTML}`;
        return div0;
    }

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

    function equipmentNodeComparer(e1, e2) {
        try {
            let equip1 = undefined;
            let title1 = (e1.getAttribute('data-original-title') ?? e1.getAttribute('title'));
            if (title1?.length > 0) {
                equip1 = g_equipMap.get(title1.substring(title1.lastIndexOf('>') + 1).trim());
                e1.setAttribute('data-abbr', (equip1?.index ?? ''));
            }

            let equip2 = undefined;
            let title2 = (e2.getAttribute('data-original-title') ?? e2.getAttribute('title'));
            if (title2?.length > 0) {
                equip2 = g_equipMap.get(title2.substring(title2.lastIndexOf('>') + 1).trim());
                e2.setAttribute('data-abbr', (equip2?.index ?? ''));
            }

            if (!equip1 && !equip2) {
                return ((new Amulet()).fromNode(e1)?.compareTo((new Amulet()).fromNode(e2)) ?? 1);
            }
            else if (!equip1) {
                return 1;
            }
            else if (!equip2) {
                return -1;
            }
            else if (equip1.index == equip2.index) {
                return parseInt(title1.match(/>(\d+)</)[1]) - parseInt(title2.match(/>(\d+)</)[1]);
            }
            return (equip1.index - equip2.index);
        }
        catch (err) {
            console.log(err);
        }
    }

    function getPostData(p1, p2) {
        let data = null;
        let sc = document.getElementsByTagName('script');
        for (let i = 0; i < sc.length; i++) {
            let func = sc[i].innerText.match(p1);
            if (func != null) {
                data = func[0].match(p2)[0];
                break;
            }
        }
        return data;
    }

    function wishExpireTip() {
        GM_xmlhttpRequest({
            method: g_postMethod,
            url: g_readUrl,
            headers: g_postHeader,
            data: `f=19`,
            onload: response => {
                let points = response.responseText.split('#')[1];
                if (parseInt(points) < 2) {
                    let navBar = document.querySelector('.nav.navbar-nav');
                    for (let nav of navBar.children) {
                        if (nav.firstChild.innerHTML.indexOf('许愿池') >= 0) {
                            nav.firstChild.innerHTML = nav.firstChild.innerHTML.replace('许愿池', '许愿池(已过期)');
                            nav.firstChild.style.color = 'white';
                            nav.firstChild.style.backgroundColor = 'red';
                            break;
                        }
                    }
                }
            }
        });
    }

    initiatizeConfig();
    wishExpireTip();

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

    if (window.location.pathname == '/fyg_index.php') {
        let userData = getUserData();
        let dataIndex = userData.dataIndex;

        let waitForCol = setInterval(() => {
            let colmd4 = document.getElementsByClassName('col-md-4');
            if (colmd4?.length > 0 && colmd4[0]?.children?.length > 5) {
                clearInterval(waitForCol);

                let px = colmd4[0].children[5];
                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
                    setUserData(userData);
                }

                p0.appendChild(sp);
                colmd4[0].appendChild(p0);
            }
        }, 200);

        function doConfig() {
            let fixedContent =
                '<div style="padding:20px 0px 10px;color:#0000c0;font-size:15px;"><b>请勿随意修改配置项,' +
                '除非您知道它的准确用途并且设置为正确的值,否则可能会导致插件工作异常。<span id="config-information-tips" ' +
                '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:30%; }
                         #config-table th.config-th-name { width:50%; }
                         #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 infoMsg = document.getElementById('config-information-tips');
            let infoTimer = null;
            function showInfoMessage(msg, time) {
                if (infoTimer != null) {
                    clearTimeout(infoTimer);
                    infoTimer = null;
                }
                infoMsg.innerText = msg;
                if ((time = parseInt(time)) > 0) {
                    infoTimer = setTimeout(() => {
                        infoTimer = null;
                        infoMsg.innerText = '';
                    }, time);
                }
            }

            let configTable = document.getElementById('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 });
                showInfoMessage('配置项已' + 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 });
                });
                showInfoMessage('全部配置项已' + 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 = getUserData();
                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[2].children[0].value = row.children[1].children[0].children[0].value;
                    if (cfg.validate?.call(null, value) ?? true) {
                        config[id] = cfg.value = (cfg.onchange?.call(null, value) ?? value);
                    }
                    else {
                        error.push(cfg.name);
                    }
                });

                udata.config = config;
                setUserData(udata);

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

            genericPopupAddCloseButton(80);

            genericPopupSetContentSize(Math.min(g_configs.length * 28 + 65, Math.max(window.innerHeight - 200, 400)),
                                       Math.min(600, Math.max(window.innerWidth - 100, 400)),
                                       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 = document.getElementById("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 = document.getElementById('user_data_export_tip');
                    let tipColor = tipContainer.style.color;
                    let tipString = tipContainer.innerText;
                    tipContainer.style.color = '#ff0000';
                    document.querySelector('#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}`);
                }
            });
            document.getElementById("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 waitForUserd = setInterval(() => {
            let userd = document.getElementById('userd');
            if (userd?.children?.length > 0) {
                clearInterval(waitForUserd);

                let globalDataBtnContainer = document.createElement(userd.tagName);
                globalDataBtnContainer.id = 'global-data-button-container';
                globalDataBtnContainer.className = userd.className;
                globalDataBtnContainer.style.borderTop = '2px solid #d0d0d0';

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

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

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

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

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

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

                userd.parentNode.appendChild(globalDataBtnContainer);
            }
        }, 200);
    }
    else if (window.location.pathname == '/fyg_equip.php') {
        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 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_Expand" style="margin-right:5px;cursor:pointer;">全部展开</label>
                          <input type="checkbox" id="equipment_Expand" /></div>
                        <div style="padding:0px 0px 10px 15px;float:right;">
                          <label for="equipment_BG" style="margin-right:5px;cursor:pointer;">使用深色背景</label>
                          <input type="checkbox" id="equipment_BG" /></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.querySelector("#carding > div.row > div.fyg_tc")?.children;
                            if (eqbtns?.length > 0 && eqbtns[0].className.endsWith('fyg_mp3')) {
                                clearInterval(waitForBtn);

                                let eqstore = document.querySelectorAll("#backpacks > div.alert-success > div.content > button.fyg_mp3");
                                eqstore.forEach((item) => {
                                    if (item.className.split(' ').length == 3) {
                                        item.dataset.instore = 1;
                                    }
                                });

                                eqbtns =
                                    Array.from(eqbtns).concat(
                                    Array.from(document.querySelectorAll("#backpacks > div.alert-danger > div.content > button.fyg_mp3"))
                                         .sort(equipmentNodeComparer)).concat(
                                    Array.from(eqstore).sort(equipmentNodeComparer));

                                for (let i = eqbtns.length - 1; i >= 0; i--) {
                                    if (eqbtns[i].className.split(' ').length != 3) {
                                        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 (btn.innerText == '空') {
                                        return;
                                    }

                                    let btn0 = document.createElement('button');
                                    btn0.className = 'btn btn-light';
                                    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.style.borderColor = getComputedStyle(btn).getPropertyValue('background-color');
                                    btn0.setAttribute('onclick', btn.getAttribute('onclick'));

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

                                    let enhancements = btn.innerText;
                                    if (enhancements.indexOf('+') == -1) {
                                        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.abbr)];
                                        if (a == null) {
                                            let title = (btn.getAttribute('data-original-title') ?? btn.getAttribute('title'));
                                            a = g_equipMap.get(title?.substring(title.lastIndexOf('>') + 1).trim());
                                        }
                                        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("#backpacks > div.alert-danger > div.content > button.fyg_mp3"),
                                            bag, key);
                                        count += bag.length;
                                    }
                                    if (store != null) {
                                        amuletNodesToArray(
                                            document.querySelectorAll("#backpacks > div.alert-success > div.content > button.fyg_mp3"),
                                            store, key);
                                        count += store.length;
                                    }
                                    return count;
                                }

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

                                    genericPopupClose(true);

                                    let bag = [];
                                    let store = [];
                                    if (queryAmulets(bag, store, null) == 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('护符组管理', '<div id="popup_amulet_groups" style="margin-top:15px;"></div>');
                                    let amuletContainer = document.getElementById('popup_amulet_groups');

                                    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 id="popup_amulet_group_bag_name" style="color:${err ? "red" : "blue"};
                                             font-size:20px;">当前背包内容 [${bagGroup.count()}]</b>`;

                                        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("#backpacks > div.alert-danger > div.content > button.fyg_mp3"),
                                                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>', '')}</ul>
                                             <hr><ul>${bagGroup.formatItems('<li>', '<li style="color:red;">', '</li>', '</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 id="popup_amulet_group_${i}_name" style="color:${err ? "red" : "blue"};
                                                 font-size:20px;">${groupArray[i].name} [${groupArray[i].count()}]</b>`;

                                            let amuletDeleteGroupBtn = document.createElement('button');
                                            amuletDeleteGroupBtn.innerText = '删除';
                                            amuletDeleteGroupBtn.style.float = 'right';
                                            amuletDeleteGroupBtn.onclick = (() => {
                                                let groupName = document.getElementById(`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 = document.getElementById(`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 = document.getElementById(`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 = document.getElementById(`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 = document.getElementById(`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 = document.getElementById(`popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                genericPopupShowProgressMessage('卸载中,请稍候…');
                                                beginUnloadAmuletGroupFromBag(
                                                    document.querySelectorAll("#backpacks > div.alert-danger > div.content > button.fyg_mp3"),
                                                    groupName, refreshEquipmentPage, showAmuletGroupsPopup);
                                            });
                                            groupDiv.appendChild(unamuletLoadGroupBtn);

                                            let amuletLoadGroupBtn = document.createElement('button');
                                            amuletLoadGroupBtn.innerText = '装备';
                                            amuletLoadGroupBtn.style.float = 'right';
                                            amuletLoadGroupBtn.onclick = (() => {
                                                let groupName = document.getElementById(`popup_amulet_group_${i}_name`).innerText.split(' ')[0];
                                                genericPopupShowProgressMessage('加载中,请稍候…');
                                                beginLoadAmuletGroupFromStore(
                                                    document.querySelectorAll("#backpacks > div.alert-success > div.content > button.fyg_mp3"),
                                                    groupName, refreshEquipmentPage, showAmuletGroupsPopup);
                                            });
                                            groupDiv.appendChild(amuletLoadGroupBtn);

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

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

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

                                    genericPopupAddButton('新建护符组', 0, (() => { modifyAmuletGroup(null); }), 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("#backpacks > div.alert-danger > div.content > button.fyg_mp3"),
                                                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);
                                }

                                function modifyAmuletGroup(groupName) {
                                    function refreshAmuletListDivHeight() {
                                        let amuletListDiv = amuletList.parentNode;
                                        amuletListDiv.style.height
                                            = (amuletListDiv.parentNode.offsetHeight - amuletListDiv.offsetTop - 3) + 'px';
                                    }

                                    function refreshGroupAmuletListDivHeight() {
                                        let groupAmuletListDiv = groupAmuletList.parentNode;
                                        groupAmuletListDiv.style.height
                                            = (groupAmuletListDiv.parentNode.offsetHeight - groupAmuletListDiv.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>', '');
                                            groupSummary.style.display = 'block';
                                        }
                                        else {
                                            groupSummary.style.display = 'none';
                                            groupSummary.innerHTML = '';
                                        }
                                        refreshGroupAmuletListDivHeight();
                                        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;
                                                         item?.getAttribute('original-id') <= am.id;
                                                         item = item.nextSibling);
                                                    amuletList.insertBefore(li, item);
                                                }
                                            }
                                            refreshGroupAmuletSummary();
                                            groupChanged = true;
                                        }
                                    }

                                    let bag = [];
                                    let store = [];
                                    if (queryAmulets(bag, store, null) == 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 =
                                        '<br><b><span style="margin-left:10px;font-size:18px;color:blue;">双击护符条目以进行添加或移除操作</span>' +
                                        '<span style="margin-right:10px;font-size:18px;color:blue;float:right;">共 ' +
                                        '<span id="amulet_count" style="color:#800020;">0</span> 个护符</span></b>';
                                    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 = document.getElementById('amulet_count');
                                    let amuletFilterList = document.getElementById('amulet_filter');
                                    let amuletList = document.getElementById('amulet_list');
                                    let groupSummary = document.getElementById('group_summary');
                                    let groupAmuletList = document.getElementById('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);

                                    refreshAmuletListDivHeight();
                                    refreshGroupAmuletListDivHeight();
                                }

                                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, null) > 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 = document.getElementById('amulet_export_tip');
                                            let tipColor = tipContainer.style.color;
                                            let tipString = tipContainer.innerText;
                                            tipContainer.style.color = '#ff0000';
                                            document.querySelector('#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);

                                    document.getElementById("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("#backpacks > div.alert-danger > div.content > button.fyg_mp3"),
                                            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, 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);
                }

                if (document.getElementsByClassName('nav nav-secondary nav-justified')[0].children[0].className == 'active') {
                    addCollapse();
                }

                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() {
                    function sendEquipHttpRequest(eqinfo) {
                        let request = GM_xmlhttpRequest({
                            method: g_postMethod,
                            url: g_readUrl,
                            headers: g_postHeader,
                            data: 'f=7',
                            onload: response => {
                                let div0 = readEquipmentDOM(response.responseText);
                                let equipment = getEquipmentInfo(div0.children);

                                let ids = [];
                                for (let i = 0; i < eqinfo.length; i++) {
                                    for (let j = 0; j < equipment.length; j++) {
                                        if (eqinfo[i] == equipment[j].slice(0, -2).join()) {
                                            ids.push(equipment[j]);
                                            break;
                                        }
                                    }
                                }
                                let c = ids.length;
                                let puton_data = getPostData(/puton\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/).slice(7, -1);

                                for (let i = 0; i < ids.length; i++) {
                                    request = GM_xmlhttpRequest({
                                        method: g_postMethod,
                                        url: g_postUrl,
                                        headers: g_postHeader,
                                        data: puton_data.replace('"+id+"', ids[i][ids[i].length - 1]),
                                        onload: response => {
                                            if (!(g_equipmentOperationError = (response.responseText != 'ok'))) {
                                                c--;
                                                if (c == 0) {
													beginLoadAmulets();
                                                }
                                            }
                                        }
                                    });
									httpRequestRegister(request);
                                }
                            }

                        });
						httpRequestRegister(request);
                    }

                    function sendBeginEquipHttpRequest(bind_info) {
                        let isStore = false;
                        genericPopupTaskSetState(0, '');

                        let request = GM_xmlhttpRequest({
                            method: g_postMethod,
                            url: g_readUrl,
                            headers: g_postHeader,
                            data: 'f=7',
                            onload: response => {
                                let div0 = readEquipmentDOM(response.responseText);
                                let equipment = getEquipmentInfo(div0.children);
                                let equiped = getEquipmentInfo(document.querySelector("#carding > div.row > div.fyg_tc")?.children);

                                let ids = [];
                                let err = 0;
                                for (let i = 0; i < 4; i++) {
                                    let exist = 0;
                                    for (let j = 0; j < 4; j++) {
                                        if (bind_info[i] == equiped[j].slice(0, -2).join()) { //已装备
                                            exist = 1;
                                            break;
                                        }
                                    }
                                    if (exist == 0) { //未装备
                                        for (let j = 0; j < equipment.length; j++) {
                                            if (bind_info[i] == equipment[j].slice(0, -2).join()) {
                                                ids.push(equipment[j]);
                                                exist = 1;
                                                break;
                                            }
                                        }
                                        if (exist == 0) {
                                            console.log(bind_info[i]);
                                            alert('有装备不存在,请重新绑定');
                                            err = 1;
                                            window.location.reload();
                                        }
                                    }
                                }

                                if (err == 0) {
                                    let c = ids.length;
                                    if (c == 0) {
										beginLoadAmulets();
                                    }
                                    let puton_data = getPostData(/puton\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/).slice(7, -1);
                                    let puto_data = 'c=22' + puton_data.slice(3);

                                    let storeCount = 0;
                                    let storePuton = [];

                                    for (let i = 0; i < ids.length; i++) {
                                        request = GM_xmlhttpRequest({
                                            method: g_postMethod,
                                            url: g_postUrl,
                                            headers: g_postHeader,
                                            data: puton_data.replace('"+id+"', ids[i][ids[i].length - 1]),
                                            onload: response => {
                                                if (!(g_equipmentOperationError = (response.responseText != 'ok'))) {
                                                    c--;
                                                    if (c == 0 && !isStore) {
														beginLoadAmulets();
                                                    } else if (c == 0 && isStore) {
                                                        request = sendEquipHttpRequest(storePuton);
                                                    }
                                                } else if (response.responseText == '这不是你的装备') {
                                                    let item = ids[i].slice(0, -1);
                                                    item = [ g_equipMap.get(item[0]).name, `Lv.${item[1]}`, `${item[2]}%`, `${item[3]}%`,
                                                             `${item[4]}%`, `${item[5]}%`, `${item[6] == 1 ? '神秘' : ''}` ].join(' ');
                                                    storeCount++;
                                                    genericPopupTaskSetState(0, '- 取出仓库……');
                                                    request = GM_xmlhttpRequest({
                                                        method: g_postMethod,
                                                        url: g_postUrl,
                                                        headers: g_postHeader,
                                                        data: puto_data.replace('"+id+"', ids[i][ids[i].length - 1]),
                                                        onload: response => {
                                                            if (!(g_equipmentOperationError = (response.responseText != 'ok'))) {
                                                                g_equipmentFromStoreCount++;
                                                                storeCount--;
                                                                if (storeCount == 0) {
                                                                    genericPopupTaskSetState(0, '');
                                                                }

                                                                isStore = true;
                                                                storePuton.push(ids[i].slice(0, 6));
                                                                c--;
                                                                if (c == 0) {
                                                                    request = sendEquipHttpRequest(storePuton);
                                                                }
                                                            } else if (response.responseText == '背包已满。') {
                                                                alert(`背包已满,仓库装备\n[ ${item} ]\n无法取出,请整理背包`);
                                                                c--;
                                                                if (c == 0 && !isStore) {
																	beginLoadAmulets();
                                                                } else if (c == 0 && isStore) {
                                                                    request = sendEquipHttpRequest(storePuton);
                                                                }
                                                            }
                                                        }
                                                    });
													httpRequestRegister(request);
                                                }
                                            }
                                        });
                                        httpRequestRegister(request);
                                    }
                                }
                            }
                        });
                        httpRequestRegister(request);
                    }

                    let g_equipmentOperationError = false;
                    let g_equipmentFromStoreCount = 0;
                    let g_equipmentToStoreCount = 0;

                    let g_amuletGroupsToLoad = null;
                    let g_originalBagObjectIds = null;
                    let g_originalStoreObjectIds = null;
                    let g_scheduledObjectIndices = null;
                    let g_exchangedBagObjectIds = null;

                    function roleSetupCleanup() {
                        httpRequestClearAll();

                        g_equipmentOperationError = false;
                        g_amuletGroupsToLoad = null;
                        g_originalBagObjectIds = null;
                        g_originalStoreObjectIds = null;
                        g_scheduledObjectIndices = null;
                        g_exchangedBagObjectIds = null;
                    }

                    function roleSetupCompletion() {
                        roleSetupCleanup();
                        genericPopupClose(true);

                        window.location.reload();
                    }

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

                    function amuletLoadCompletion() {
                        genericPopupTaskComplete(2);
                        genericPopupTaskSetState(2, '');
                        checkForRoleSetupCompletion();
                    }

                    // 'cause of non-equipment-object's id would change when move it from bag to store, and,
                    // even in store the object's id still can be changed by many kind of operations.
                    // so unfortunately, restore the scheduled objects to bag by id will fail in most time,
                    // and so we use indices instead of id, but there always have chances to produce incorrect
                    // results, that means you * SHOULD * check the result by yourself manually to make sure
                    // everything is ok
                    function beginRestoreScheduledObjects() {
                        let ids;
                        let sl = g_originalStoreObjectIds.length;
                        g_scheduledObjectIndices?.sort((a, b) => b - a);
                        while (g_scheduledObjectIndices?.length > 0) {
                            // in some case the equipments that putted on was originally in the bag, but the replaced
                            // equipments will always been putted in the store, so the scheduled object's indices
                            // should be recalculated to comply with these situations
                            let i = g_scheduledObjectIndices.pop() + g_equipmentToStoreCount - g_equipmentFromStoreCount;
                            if (i < sl && g_originalStoreObjectIds[i] >= 0) {
                                (ids ??= []).push(g_originalStoreObjectIds[i]);
                            }
                        }
                        beginMoveObjects(ids, g_object_move_path.store2bag, amuletLoadCompletion, 0);
                    }

                    // maximum equipments exchanging count should be 4, and it should be placed at the head of the bag cells,
                    // on the other hand, when you move an equipment-object to store, it will be placed before non-equipment-object cells,
                    // and this will not change the order between those non-equipment-objects, they just move backward as a whole group,
                    // based on this fact, we can consider the indices of the scheduled non-equipment-objects is permanent
                    function beginStoreExchangedObjects() {
                        function beginRefreshStoredObjects() {
                            beginReadObjects(null, g_originalStoreObjectIds = [], null, true, beginRestoreScheduledObjects, 0);
                        }

                        g_originalStoreObjectIds.sort((a, b) => a - b);
                        g_scheduledObjectIndices = findNewObjects(g_scheduledObjectIndices,
                                                                  g_originalStoreObjectIds,
                                                                  (a, b) => a < 0 ? 0 : a - b,
                                                                  true);
                        g_originalBagObjectIds.sort((a, b) => a - b);
                        let ids = findNewObjects(g_exchangedBagObjectIds?.length > 4
                                                     ? g_exchangedBagObjectIds.slice(0, 4) : g_exchangedBagObjectIds,
                                                 g_originalBagObjectIds,
                                                 (a, b) => a < 0 ? 0 : a - b,
                                                 false);
                        if (ids?.length > 0) {
                            g_equipmentToStoreCount = ids.length;
                            beginMoveObjects(ids,
                                             g_object_move_path.bag2store,
                                             g_scheduledObjectIndices?.length > 0 ? beginRefreshStoredObjects : amuletLoadCompletion,
                                             0);
                        }
                        else if (g_scheduledObjectIndices?.length > 0) {
                            beginRefreshStoredObjects();
                        }
                        else {
                            amuletLoadCompletion();
                        }
                    }

                    function beginamuletLoadGroups() {
                        if (g_amuletGroupsToLoad?.length > 0) {
                            genericPopupTaskSetState(2, `- 加载护符……[${g_amuletGroupsToLoad?.length}]`);
                            beginLoadAmuletGroupFromStore(null, g_amuletGroupsToLoad.shift(), beginamuletLoadGroups, 0);
                        }
                        else {
                            amuletLoadCompletion();
                        }
                    }

                    function beginLoadAmulets() {
                        // equipment task should be completed
                        genericPopupTaskComplete(0, g_equipmentOperationError);

                        if (g_amuletGroupsToLoad != null) {
                            genericPopupTaskSetState(2, '- 清理装备……');
                            beginClearBag(null, null, beginamuletLoadGroups, 0);
                        }
                        else {
                            genericPopupTaskSetState(2, '- 恢复背包……');
                            beginReadObjects(g_exchangedBagObjectIds = [], null, null, true, beginStoreExchangedObjects, 0);
                        }
                    }

                    function beginScheduleCells(bindInfo) {
                        function beginReadScheduledStore(bindInfo) {
                            genericPopupTaskSetState(2, '');
                            // use g_scheduledObjectIndices as scheduled store temporarily
                            // (why i'd like save variable names? it's weird...)
                            beginReadObjects(null, g_scheduledObjectIndices = [], null, true, sendBeginEquipHttpRequest, bindInfo);
                        }

                        if (g_originalBagObjectIds != null) {
                            g_equipmentFromStoreCount = 0;
                            g_equipmentToStoreCount = 0;

                            let ids;
                            let freeCellsNeeded = 4;
                            let il = g_originalBagObjectIds.length - 1;
                            for (let i = il; i >= 0; i--) {
                                if (g_originalBagObjectIds[i] == -1) {
                                    if (--freeCellsNeeded == 0) {
                                        genericPopupTaskSetState(2, '');
                                        sendBeginEquipHttpRequest(bindInfo);
                                        break;
                                    }
                                }
                                else {
                                    (ids ??= []).push(g_originalBagObjectIds[i]);
                                    if (--freeCellsNeeded == 0) {
                                        beginMoveObjects(ids, g_object_move_path.bag2store, beginReadScheduledStore, bindInfo);
                                        break;
                                    }
                                }
                            }
                        }
                        else {
                            genericPopupTaskSetState(2, '- 调整空间……');
                            beginReadObjects(g_originalBagObjectIds = [], g_originalStoreObjectIds = [],
                                             null, false, beginScheduleCells, bindInfo);
                        }
                    }

                    function beginRoleSetup(bindInfo) {
                        if (bindInfo.length > 5 && bindInfo[5].length > 0) {
                            g_amuletGroupsToLoad = bindInfo[5].split(',');
                            if (g_amuletGroupsToLoad.length > 0) {
                                g_originalBagObjectIds = [ -1, -1, -1, -1 ];
                                genericPopupTaskSetState(2, '- 清理背包……');
                                beginClearBag(null, null, beginScheduleCells, bindInfo);
                                return;
                            }
                        }

                        g_amuletGroupsToLoad = null;
                        beginScheduleCells(bindInfo);
                    }

                    let bindingElements = document.getElementById(g_bindingListSelectorId)?.value?.split(BINDING_NAME_SEPARATOR);
                    let roleId = (bindingElements?.length == 2 ? bindingElements[0] : undefined);
                    let bind_info = (bindingElements?.length == 2 ? bindingElements[1] : undefined);

                    if (roleId != undefined && bind_info != undefined) {
                        let upcard_data = getPostData(/upcard\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/).slice(7, -1).replace('"+id+"', roleId);
                        GM_xmlhttpRequest({
                            method: g_postMethod,
                            url: g_postUrl,
                            headers: g_postHeader,
                            data: upcard_data,
                            onload: response => {
                                if (response.responseText == 'ok' || response.responseText == '你没有这张卡片或已经装备中') {
                                    bind_info = bind_info.split(BINDING_ELEMENT_SEPARATOR);

                                    function equipOnekeyQuit() {
                                        genericPopupClose(true);
                                        httpRequestAbortAll();
                                        roleSetupCleanup();
                                    }

                                    genericPopupInitialize();
                                    genericPopupTaskListPopupSetup('更换中……', 300, [ '装备', '光环', '护符' ], equipOnekeyQuit);
                                    beginRoleSetup(bind_info);

                                    let halo = [];
                                    if (bind_info.length > 4) {
                                        bind_info[4].split(',').forEach((item) => {
                                            let hid = g_haloMap.get(item.trim())?.id;
                                            if (hid > 0) {
                                                halo.push(hid);
                                            }
                                        });
                                        if ((halo = halo.join(','))?.length > 0) {
                                            let halosave_data = getPostData(/halosave\(\)\{[\s\S]*\}/m, /data: ".*\+savearr\+.*"/).slice(7, -1).replace('"+savearr+"', halo);
                                            let request = GM_xmlhttpRequest({
                                                method: g_postMethod,
                                                url: g_postUrl,
                                                headers: g_postHeader,
                                                data: halosave_data,
                                                onload: response => {
                                                    genericPopupTaskComplete(1, response.responseText != 'ok');
                                                    checkForRoleSetupCompletion();
                                                }
                                            });
                                            httpRequestRegister(request);
                                        }
                                    }
                                    if (!(halo?.length > 0)) {
                                        genericPopupTaskComplete(1);
                                        checkForRoleSetupCompletion();
                                    }
                                    genericPopupShowModal(false);
                                }
                            }
                        });
                    }
                    else {
                        alert('读取绑定信息失败,无法装备!');
                        return;
                    }
                }

                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 roleLv = document.querySelector("#backpacks")?.querySelectorAll(".icon.icon-angle-down.text-primary")[0]
                                        ?.innerText?.match(/\d+/)[0];
                    let roleHs = document.querySelector("#backpacks")?.querySelectorAll(".icon.icon-angle-down.text-primary")[2]
                                        ?.innerText?.match(/\d+/)[0];
                    if (roleId == undefined || roleLv == undefined || roleHs == undefined) {
                        alert('读取卡片信息失败,无法执行绑定操作!');
                        return;
                    }

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

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

                    const highlightBackgroundColor = '#80c0f0';
                    const bindingNameHTML =
                        `<div style="width:100%;color:#0000ff;padding:20px 0px;"><b>绑定方案名称` +
                        `(不超过31个字符,请仅使用大、小写英文字母、数字、连字符、下划线及中文字符):` +
                        `<span id="binding-information-tips" style="float:right;color:red;"></span></b><br>
                         <div style="width:100%;"><input type="text" id="binding_name" maxlength="31"
                              list="binding_list" style="display:inline-block;width:98%;margin-top:15px;"></input></div>
                         <datalist id="binding_list"></datalist></div>`;
                    const bindingHTML =
                        `<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}">
                             <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>`;

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

                    let infoMsg = document.getElementById('binding-information-tips');
                    let infoTimer = null;
                    function showInfoMessage(msg, time) {
                        if (infoTimer != null) {
                            clearTimeout(infoTimer);
                            infoTimer = null;
                        }
                        infoMsg.innerText = msg;
                        if ((time = parseInt(time)) > 0) {
                            infoTimer = setTimeout(() => {
                                infoTimer = null;
                                infoMsg.innerText = '';
                            }, time);
                        }
                    }

                    let eq_selectors = document.querySelectorAll('select.equipment_selector');
                    let asyncOperations = 2;
                    let haloMax = 0;
                    let haloGroupItemMax = 0;

                    GM_xmlhttpRequest({ //获取装备
                        method: g_postMethod,
                        url: g_readUrl,
                        headers: g_postHeader,
                        data: 'f=7',
                        onload: response => {
                            let div0 = readEquipmentDOM(response.responseText);
                            let equipment = getEquipmentInfo(div0.children);
                            equipment = equipment.concat(getEquipmentInfo(document.querySelector("#carding > div.row > div.fyg_tc")?.children));

                            let e = [ [], [], [], [] ];
                            let origin = [ [], [], [], [] ];
                            equipment.sort((e1, e2) => {
                                if (e1[0] == e2[0]) {
                                    return (parseInt(e2[1]) - parseInt(e1[1]));
                                }
                                let a = g_equipMap.get(e1[0]);
                                let b = g_equipMap.get(e2[0]);
                                let c = a.type - b.type;
                                return (c != 0 ? c : a.index - b.index);
                            });

                            equipment.forEach(item => {
                                let eqType = g_equipMap.get(item[0]);
                                origin[eqType.type].push(item)
                                e[eqType.type].push([ eqType.name, `Lv.${item[1]}`, `${item[2]}%`, `${item[3]}%`,
                                                      `${item[4]}%`, `${item[5]}%`, `${item[6] == 1 ? '神秘' : ''}` ]);
                            });

                            for (let i = 0; i < 4; i++) {
                                for (let j = 0; j < e[i].length; j++) {
                                    let op0 = document.createElement('option');
                                    op0.innerText = e[i][j].join(' ');
                                    op0.value = origin[i][j].slice(0, -2).join(',');
                                    eq_selectors[i].appendChild(op0);
                                }
                            }
                            asyncOperations--;
                        }
                    });

                    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 = document.getElementById('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 = document.getElementById('halo_points');
                            haloSlots = document.getElementById('halo_slots');
                            $('.halo_item').each(function(i, e) {
                                $(e).on('click', selector_halo);
                                $(e).attr('original-item', $(e).text().split(' ')[0]);
                            });
                            asyncOperations--;
                        },
                        null);

                    function unbindAll() {
                        if (confirm('这将清除本卡片全部绑定方案,继续吗?')) {
                            let udata = getUserData();
                            if (udata.dataBind[roleId] != null) {
                                delete udata.dataBind[roleId];
                            }
                            setUserData(udata);
                            bindingName.value = BINDING_NAME_DEFAULT;
                            bindingList.innerHTML = '';
                            refreshBindingSelector(roleId);
                            showInfoMessage('解除全部绑定成功', 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 = getUserData();
                                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;
                                }
                                setUserData(udata);
                                refreshBindingSelector(roleId);
                                representBinding(bi);
                                showInfoMessage(bn + ':解绑成功', 5000);
                            }
                            else {
                                alert('方案名称未找到!');
                            }
                        }
                    };

                    function saveBinding() {
                        if (validateBindingName()) {
                            let equ = [];
                            let halo = [];
                            let sum = 0;
                            eq_selectors.forEach((eq) => { equ.push(eq.value); });
                            $(".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 (equ.length == 4 && sum <= h && halo.length <= parseInt(roleHs)) {
                                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 bind_info = [ equ[0], equ[1], equ[2], equ[3],
                                                  halo.join(','), amuletArray.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 = getUserData();
                                udata.dataBind[roleId] = bindings.join(BINDING_SEPARATOR);
                                setUserData(udata);
                                refreshBindingSelector(roleId);
                                showInfoMessage(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);
                        showInfoMessage(valid ? '' : '方案名称不符合规则,请检查');
                        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((eq) => {
                                    for (let op of eq.childNodes) {
                                        if (v.indexOf(op.value) >= 0) {
                                            eq.value = op.value;
                                            break;
                                        }
                                    }
                                });
                            }
                            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 = document.getElementById('binding_list');
                    let bindingName = document.getElementById('binding_name');
                    let haloPoints = null;
                    let haloSlots = null;
                    let amuletContainer = document.getElementById('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 = document.getElementById('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.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;

                            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);
                            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 = getUserData().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((mutationsList) => {
                    $('.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.querySelector("#carding > div.row > div.fyg_tc").children;
                                    let bag = Array.from(document.querySelectorAll
                                                         ("#backpacks > div.alert-danger > div.content > button.fyg_mp3"))
                                                   .concat(Array.from(document.querySelectorAll
                                                                      ("#backpacks > div.alert-success > div.content > button.fyg_mp3")));
                                    let bagdata = getEquipmentInfo(bag);
                                    let data = getEquipmentInfo(equip);
                                    bagdata = bagdata.concat(data).sort((e1, e2) => {
                                        if (e1[0] == e2[0]) {
                                            return (parseInt(e1[1]) - parseInt(e2[1]));
                                        }
                                        let delta = g_equipMap.get(e1[0]).type - g_equipMap.get(e2[0]).type;
                                        return (delta != 0 ? delta : (e1[0] < e2[0] ? -1 : 1));
                                    });
                                    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_amuletBuffShortMarks[i] + 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);
                                }
                            });
                            try {
                                if (!(mutationsList[0]?.addedNodes[0]?.className?.split(' ')[0] == 'popover' ||
                                      mutationsList[0]?.removedNodes[0]?.className?.split(' ')[0] == 'popover' ||
                                      mutationsList[0]?.addedNodes[0]?.id == 'equipmentDiv')) {

                                    addCollapse();
                                }
                            }
                            catch (err) {
                                console.log(err);
                            }
                            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') {
        let beachConfigDiv = document.createElement('form');
        beachConfigDiv.innerHTML =
            `<div class="form-group form-check" style="padding:5px 15px;border-bottom:1px solid grey;">
             <button type="button" style="margin-right:15px;" id="siftSettings">筛选设置</button>
             <label class="form-check-label" for="ignoreStoreMysEquip" title="不将海滩上的装备与已有的神秘装备做比较"
                    style="margin-right:5px;cursor:pointer;">已有神秘装备不作为筛选依据</label>
             <input type="checkbox" class="form-check-input" id="ignoreStoreMysEquip" style="margin-right:15px;" />
             <label class="form-check-label" for="forceExpand" style="margin-right:5px;cursor:pointer;">强制展开所有装备</label>
             <input type="checkbox" class="form-check-input" id="forceExpand" style="margin-right:15px;" />
             <b><span id="analyze-indicator">正在分析...</span></b>
             <div style="float:right;"><label class="form-check-input" for="beach_BG"
                  style="margin-right:5px;cursor:pointer;">使用深色背景</label>
             <input type="checkbox" class="form-check-input" 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(() => { pickEquipment(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(() => { pickEquipment(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">装备名称</th><th>装备属性</th><th /><th /><th /></tr></table><div>`;

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

            let equipTable = document.getElementById('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-addr', 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 = getUserData();
            if (udata.dataBeachSift == null) {
                udata.dataBeachSift = {};
                setUserData(udata);
            }

            let eqchecks = document.getElementsByClassName('sift-settings-checkbox');
            for (let i = 0; i < eqchecks.length; i += 5) {
                let abbr = eqchecks[i].parentNode.parentNode.getAttribute('equip-addr');
                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-addr')] = checks.join(',');
                    });

                    let udata = getUserData();
                    udata.dataBeachSift = settings;
                    setUserData(udata);

                    genericPopupClose(true);
                }),
                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 panel = document.getElementsByClassName('panel panel-primary')[2]
        panel.insertBefore(beachConfigDiv, panel.children[1]);

        let popStyle =
            `<style>
             .popup {
                 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;
             }
             .popup-content {
                 width: 400px;
                 height: 200px;
                 background-color: #fff;
                 box-sizing: border-box;
                 padding: 10px 30px;
                 color: black;
             }
             .topline {
                 width: 100%;
                 border-bottom: 1px solid black;
             }
             </style>`;

        let divAmulet = document.createElement('div');
        divAmulet.setAttribute('id', 'amuletpage_pop');

        let initAmuletHTML =
            `<div class="popup-content"><div class="topline"><p></p></div>
             <button type="button" id="exitAmulet">取消</button>
             <button type="button" disabled="disabled" id="goAmulet">继续</button>
             <span>请稍候...</span></div>`;

        divAmulet.innerHTML = `${popStyle}<div class="popup" id="amuletpop">${initAmuletHTML}</div>`;
        divAmulet.querySelector('#exitAmulet').onclick = exitAmulet;
        divAmulet.querySelector('#goAmulet').onclick = goAmulet;

        let toAmuletExit = false;
        let continueAmulet = true;

        function exitAmulet() {
            toAmuletExit = true;
            document.getElementById('amuletpop').style.display = 'none';
            document.getElementById('amuletpop').innerHTML = initAmuletHTML;
        }

        function goAmulet() {
            continueAmulet = true;
        }

        let p1 = document.getElementsByClassName('panel panel-primary')[1];
        if (p1.lastChild.id != 'amuletpage_pop') {
            p1.appendChild(divAmulet);
        }

        let batbtns = document.getElementsByClassName('panel-body')[1].children[0];
        let toAmuletBtn = document.createElement('button');
        toAmuletBtn.className = 'btn btn-danger';
        toAmuletBtn.innerText = '批量沙滩转护符(背包至少留一个空位)';
        let _amuletsID = [];
        let _equipmentsID = [];
        let _pickID = [];
        let _safeid = null;
        let _next = -1;
        let waitForContinue = null;
        toAmuletBtn.onclick = () => {
            _amuletsID = [];
            _equipmentsID = [];
            _pickID = [];
            _safeid = null;
            _next = -1;
            waitForContinue = null;
            toAmuletExit = false;
            continueAmulet = true;
            let bagItems = document.querySelector("#backpacks").children;
            if (bagItems != null && bagItems.length > 2 && confirm('确定要把沙滩所有装备转换成护符吗?')) {

                for (let i = 0; i < bagItems.length; i++) {
                    if (bagItems[i].className.split(' ').length == 3 && bagItems[i].innerText.indexOf('+') != -1) {
                        _amuletsID.push(bagItems[i].getAttribute('onclick').match(/'\d+'/)[0].slice(1, -1));
                    } else if (bagItems[i].className.split(' ').length == 3 && bagItems[i].innerText.indexOf('+') == -1) {
                        _equipmentsID.push(bagItems[i].getAttribute('onclick').match(/'\d+'/)[0].slice(1, -1));
                    }
                }

                let btns = document.getElementsByClassName('fyg_mp3');
                for (let i = 0; i < btns.length; i++) {
                    if (btns[i].parentNode.id == 'beachall' && btns[i].className.indexOf('btn') != -1) {
                        let rare = btns[i].className.split(' ')[1];
                        if (rare != 'fyg_colpz01bg' && rare != 'fyg_colpz02bg') {
                            _pickID.push(btns[i].getAttribute('onclick').match(/\(\d+\)/)[0].slice(1, -1))
                        }
                    }
                }

                _safeid = getPostData(/sttz\(\)\{[\s\S]*\}/m, /data: ".*"/).slice(12, -1);

                let amuletBlock = document.getElementById('amuletpop').children[0].children[0];
                let btnExit = document.getElementById('amuletpop').children[0].children[1];
                let btnContinue = document.getElementById('amuletpop').children[0].children[2];
                let infospan = document.getElementById('amuletpop').children[0].children[3];

                function showAmuletPop() {
                    $('.popup-content').height(Math.min(_pickID.length, 19) * 10 + 200);
                    $('.popup-content').width(700);
                    $('.popup-content').css('overflow', 'scroll');

                    document.getElementById('amuletpop').style.display = 'flex';
                }

                function destroyAmulet(e) {
                    let btn = e.target.parentNode.parentNode.parentNode;
                    let id = btn.getAttribute('id');
                    let pirlam_data = `c=9&id=${id}&yz=124&${_safeid}`;
                    GM_xmlhttpRequest({ //pirl(id)
                        method: g_postMethod,
                        url: g_postUrl,
                        headers: g_postHeader,
                        data: pirlam_data,
                        onload: response => {
                            if (response.responseText.indexOf('水') != -1) {
                                GM_xmlhttpRequest({ //read
                                    method: g_postMethod,
                                    url: g_readUrl,
                                    headers: g_postHeader,
                                    data: 'f=2',
                                    onload: response => {
                                        _amuletsID.splice(_amuletsID.indexOf(id), 1);
                                        btn.parentNode.removeChild(btn);
                                    }
                                });
                            } else {
                                console.log(`except at destroy: ${response.responseText}`);
                            }
                        }
                    });
                }

                function storeAmulet(e) {
                    let btn = e.target.parentNode.parentNode.parentNode;
                    let id = btn.getAttribute('id');
                    let pirlam_data = `c=21&id=${id}&${_safeid}`;
                    GM_xmlhttpRequest({ //puti(id)
                        method: g_postMethod,
                        url: g_postUrl,
                        headers: g_postHeader,
                        data: pirlam_data,
                        onload: response => {
                            if (response.responseText == 'ok') {
                                GM_xmlhttpRequest({ //read
                                    method: g_postMethod,
                                    url: g_readUrl,
                                    headers: g_postHeader,
                                    data: `f=2`,
                                    onload: response => {
                                        _amuletsID.splice(_amuletsID.indexOf(id), 1);
                                        btn.parentNode.removeChild(btn);
                                    }
                                });
                            } else {
                                console.log(`except at store: ${response.responseText}`);
                                alert('仓库已满');
                            }
                        }
                    });
                }

                showAmuletPop();

                function toAmulet(count, equipmentsID, amuletsID, pickID, safeid) {
                    infospan.innerText = '请稍候..';
                    btnContinue.disabled = 'disabled';
                    if (count == 0) {
                        infospan.innerText = '';
                        btnExit.innerText = '完成';
                        return;
                    }
                    if (toAmuletExit) {
                        return;
                    }
                    let stpick_data = `c=1&id=${pickID[count-1]}&${safeid}`;
                    GM_xmlhttpRequest({ //pick equipment
                        method: g_postMethod,
                        url: g_postUrl,
                        headers: g_postHeader,
                        data: stpick_data,
                        onload: response => {
                            if (toAmuletExit) {
                                return;
                            }
                            if (response.responseText != 'ok') {
                                infospan.innerText = '背包已满,请处理后继续';
                                btnContinue.disabled = '';
                                continueAmulet = false;
                                _next = count;
                                _equipmentsID = equipmentsID;
                                _amuletsID = amuletsID;
                                _pickID = pickID;
                                _safeid = safeid;
                                waitForContinue = setInterval(() => {
                                    // $('.amulet_menu').bind('click', () => {
                                    //     let id = $(this).parent().parent().parent().attr('id');
                                    //     console.log(`splice: ${id}`);
                                    //     _amuletsID.splice(_amuletsID.indexOf(id), 1);
                                    // });
                                    if (continueAmulet) {
                                        clearInterval(waitForContinue);
                                        toAmulet(_next, _equipmentsID, _amuletsID, _pickID, _safeid);
                                    } else if (toAmuletExit) {
                                        clearInterval(waitForContinue);
                                    }
                                }, 200);
                                //alert('背包已满,请处理后继续');
                                return;
                            }
                            GM_xmlhttpRequest({ //find eqid
                                method: g_postMethod,
                                url: g_readUrl,
                                headers: g_postHeader,
                                data: 'f=2',
                                onload: response => {
                                    if (toAmuletExit) {
                                        return;
                                    }
                                    let eqid = null;
                                    let bagdiv = document.createElement('div');
                                    bagdiv.innerHTML = response.responseText;
                                    for (let i = 0; i < bagdiv.children.length; i++) {
                                        if (bagdiv.children[i].className.split(' ').length == 3 && bagdiv.children[i].innerText.indexOf('+') == -1) {
                                            eqid = bagdiv.children[i].getAttribute('onclick').match(/'\d+'/)[0].slice(1, -1);
                                            if (equipmentsID.length == 0 || equipmentsID.indexOf(eqid) == -1) {
                                                break;
                                            } else {
                                                eqid = null;
                                            }
                                        }
                                    }
                                    let pirleq_data = `c=9&id=${eqid}&yz=124&${safeid}`;
                                    GM_xmlhttpRequest({ //pirl(id)
                                        method: g_postMethod,
                                        url: g_postUrl,
                                        headers: g_postHeader,
                                        data: pirleq_data,
                                        onload: response => {
                                            if (toAmuletExit) {
                                                return;
                                            }
                                            GM_xmlhttpRequest({ //find amid
                                                method: g_postMethod,
                                                url: g_readUrl,
                                                headers: g_postHeader,
                                                data: 'f=2',
                                                onload: response => {
                                                    if (toAmuletExit) {
                                                        return;
                                                    }
                                                    let amid = null;
                                                    let bagdiv = document.createElement('div');
                                                    bagdiv.innerHTML = response.responseText;
                                                    for (let i = 0; i < bagdiv.children.length; i++) {
                                                        if (bagdiv.children[i].className.split(' ').length == 3 &&
                                                            bagdiv.children[i].innerText.indexOf('+') != -1) {

                                                            amid = bagdiv.children[i].getAttribute('onclick').match(/'\d+'/)[0].slice(1, -1);
                                                            if (amuletsID.length == 0 || amuletsID.indexOf(amid) == -1) {
                                                                let btnColor = '#00c000';
                                                                if (bagdiv.children[i].outerHTML.indexOf('樱桃') != -1 ||
                                                                    bagdiv.children[i].outerHTML.indexOf('传奇') != -1) {

                                                                    btnColor = '#c00000';
                                                                }
                                                                else if (bagdiv.children[i].outerHTML.indexOf('史诗') != -1){
                                                                    btnColor = '#c0c000';
                                                                }
                                                                let btn0 = document.createElement('div');
                                                                btn0.setAttribute('class', 'btn-group');
                                                                btn0.setAttribute('id', amid);
                                                                btn0.innerHTML =
                                                                    `<button type="button" class="btn btn-light dropdown-toggle" data-toggle="dropdown" style="background-color:${btnColor}; padding: 0px; text-align: left; box-shadow: none; line-height: 90%;">
                                                                     <div class="popover-content-show" style="padding: 10px 10px 0px;">${bagdiv.children[i].dataset.content}</div>
                                                                     <span class="caret"></span></button>
                                                                     <ul class="dropdown-menu" role="menu">
                                                                        <li><a class="amulet_menu" href="#" style="background-color:#FF8C00" id="destroyAmulet">销毁</a></li>
                                                                        <li><a class="amulet_menu" href="#" style="background-color:#00CED1" id="storeAmulet">入库</a></li>
                                                                     </ul>`;
                                                                btn0.querySelector('#destroyAmulet').onclick = destroyAmulet;
                                                                btn0.querySelector('#storeAmulet').onclick = storeAmulet;

                                                                amuletBlock.appendChild(btn0);
                                                                amuletsID.push(amid);
                                                                break;
                                                            } else {
                                                                amid = null;
                                                            }
                                                        }
                                                    }
                                                    toAmulet(count - 1, equipmentsID, amuletsID, pickID, safeid);
                                                }
                                            });
                                        }
                                    });
                                }
                            });
                        }
                    });
                }
                toAmulet(_pickID.length, _equipmentsID, _amuletsID, _pickID, _safeid);
            }
        }
        batbtns.appendChild(toAmuletBtn);

        let equipment = [];
        let equipedbtn = null;
        //读取拥有的装备
        GM_xmlhttpRequest({
            method: g_postMethod,
            url: g_readUrl,
            headers: g_postHeader,
            data: 'f=7',
            onload: response => {
                let div0 = readEquipmentDOM(response.responseText);

                for (let i = div0.children.length - 1; i >= 0; i--) {
                    if (div0.children[i].className.split(' ').length != 3 || div0.children[i].innerText.indexOf('+') != -1) {
                        div0.removeChild(div0.children[i]);
                    }
                }
                equipedbtn = Array.from(div0.children);
                let equipbag = getEquipmentInfo(div0.children);

                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.querySelector("div.row > div.fyg_tc")?.children));
                        equipedbtn.sort(equipmentNodeComparer);

                        let equiped = getEquipmentInfo(div0.querySelector("div.row > div.fyg_tc")?.children);
                        equipment = equipbag.concat(equiped);
                        if (equipment.length == 0) {
                            equipment[0] = -1;
                        }
                        document.getElementById("analyze-indicator").innerText = '分析完成';
                    }
                });
            }
        });

        //分析装备并显示属性
        var g_pickingEquipment = false;
        function pickEquipment(equipment) {
            if (!(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);

                let beachCopyObserver = new MutationObserver((mList) => {
                    if (!g_pickingEquipment && 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');
                            }
                        }
                    }
                });
                beachCopyObserver.observe(beach_copy, { childList : true });
            }

            g_pickingEquipment = true;
            copyBeach(beach_copy);

            let udata = getUserData();
            if (udata.dataBeachSift == null) {
                udata.dataBeachSift = {};
                setUserData(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 isPick = false;
                if (forceExpand || btn.className.indexOf('fyg_colpz04bg') >= 0 ||
                    btn.className.indexOf('fyg_colpz05bg') >= 0 ||
                    btn.getAttribute('data-content')?.match(/\[神秘属性\]/) != null) {

                    isPick = true;
                }
                else {
                    let btninfo = getEquipmentInfo([ btn ])[0];
                    let setting = (settings[btninfo[0]] ?? defaultSetting);
                    if (!setting[0]) {
                        let isFind = false;
                        for (let j = 0; j < equipment.length; j++) {
                            if (equipment[j][0] == btninfo[0] && !(ignoreStoreMysEquip && equipment[j][6])) {
                                isFind = true;
                                let e1 = [ parseInt(btninfo[1]), parseInt(btninfo[2]), parseInt(btninfo[3]),
                                          parseInt(btninfo[4]), parseInt(btninfo[5]) ];
                                let e2 = [ parseInt(equipment[j][1]), parseInt(equipment[j][2]), parseInt(equipment[j][3]),
                                          parseInt(equipment[j][4]), parseInt(equipment[j][5]) ];

                                if (isPick = defaultequipmentNodeComparer(setting, btninfo[0], e1, e2)) {
                                    break;
                                }
                            }
                        }
                        if (!isFind) {
                            isPick = true;
                        }
                    }
                }

                if (isPick) {
                    let btn0 = document.createElement('button');
                    btn0.className = 'btn btn-light';
                    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.style.borderColor = getComputedStyle(btn).getPropertyValue('background-color');
                    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>';
                    equipedbtn.forEach((eqbtn) => {
                        let e = g_equipMap.get(btn.dataset.originalTitle
                                                 ?.substring(btn.dataset.originalTitle.lastIndexOf('>') + 1).trim());
                        if (e?.index == parseInt(eqbtn.getAttribute('data-abbr'))) {
                            let btn1 = document.createElement('button');
                            btn1.setAttribute('class', `btn btn-light ${eqbtn.getAttribute('data-tip-class') ?? ''}`);
                            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:${btn0.style.borderColor}">` +
                        `${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_pickingEquipment = 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 show = setInterval(() => {
            if ($('#beachall .btn').length != 0) {
                clearInterval(show);
                //等待装备读取完成
                let pick = setInterval(() => {
                    if (equipment.length > 0) {
                        clearInterval(pick);
                        document.getElementById("analyze-indicator").innerText = '正在分析...';
                        setTimeout(() => { pickEquipment(equipment); }, 50);

                        let beachObserver = new MutationObserver(() => {
                            document.getElementById("analyze-indicator").innerText = '正在分析...';
                            setTimeout(() => { pickEquipment(equipment); }, 50);
                        });
                        beachObserver.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) {
            if (a.className != b.className) {
                if (a.className == null || !a.className.endsWith('fyg_mp3')) {
                    return -1;
                }
                else if (b.className == null || !b.className.endsWith('fyg_mp3')) {
                    return 1;
                }
                return (a.className.split(' ')[1] > b.className.split(' ')[1] ? -1 : 1);
            }
            else if (a == null || !a.className.endsWith('fyg_mp3')) {
                return -1;
            }
            let delta = parseInt(a.innerText.match(/\d+/)[0]) - parseInt(b.innerText.match(/\d+/)[0]);
            return (delta != 0 ? -delta : (a.getAttribute('data-original-title') > b.getAttribute('data-original-title') ? -1 : 1));
        }

        document.querySelector('body').style.paddingBottom = '1000px';
    }
    else if (window.location.pathname == '/fyg_pk.php') {
        let autoTaskDiv = document.createElement('div');
        autoTaskDiv.style.className = 'panel-heading';
        autoTaskDiv.style.float = 'right';
        autoTaskDiv.style.padding = '5px 15px';
        autoTaskDiv.style.color = 'white';
        autoTaskDiv.innerHTML =
            `<label class="form-check-label" for="autoTaskEnabledCheckbox" style="margin-right:5px;cursor:pointer;">允许执行自定义任务</label>
             <input type="checkbox" class="form-check-input" id="autoTaskEnabledCheckbox" />`;

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

        let keepCheckDiv = document.createElement('div');
        keepCheckDiv.style.className = 'panel-heading';
        keepCheckDiv.style.float = 'right';
        keepCheckDiv.style.padding = '5px 15px';
        keepCheckDiv.style.color = 'white';
        keepCheckDiv.innerHTML =
            `<label class="form-check-label" for="keepPkRecordCheckbox" style="margin-right:5px;cursor:pointer;">暂时保持战斗记录</label>
             <input type="checkbox" class="form-check-input" id="keepPkRecordCheckbox" />`;

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

        let panel = document.getElementsByClassName('panel panel-primary');
        panel[0].insertBefore(autoTaskDiv, panel[0].children[0]);
        panel[1].insertBefore(keepCheckDiv, 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 pkText = document.querySelector("#pk_text").innerHTML;
        let pkObserver = new MutationObserver(() => {
            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();
            }
        });
        pkObserver.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);

                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="" />';

                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或关注我们的公众号极客氢云获取最新地址