榭洛科特的背包Via Beta

榭洛科特的背包测试版 For Via

// ==UserScript==
// @name                榭洛科特的背包Via Beta
// @version             0.0.3
// @author              Alk
// @license             GPL-3.0
// @description         榭洛科特的背包测试版 For Via
// @description         仅控制浏览器,不对游戏进行任何操作。
// @description         Sierokarte's BackPack Beta Version. This script only affects the browser, does nothing to GBF game.
// @match               https://gbf.game.mbga.jp/*
// @match               https://game.granbluefantasy.jp/*
// @run-at              document-start
// @namespace           sierokarte-backpack-for-via
// @icon                https://raw.githubusercontent.com/enorsona/thirdparty/refs/heads/master/sherurotes_carrybag.ico
// ==/UserScript==

(function () {
    'use strict';
    // 配置默认值
    const options = {
        current: {
            mode: 'normal', // accepts: normal / the obj_name in modes
            auto_redirect_hash: '',
            delay_ratio: 100,
            contribution: 1800000
        },
        modes: {
            normal: {
                delay_ratio: 100,
            },
            gold_hunt: {
                enabled: false,
                selector: 'a.btn-targeting:nth-child(1) > div:nth-child(2) > div:nth-child(3)',
                delay_ratio: 300, // 0 => any
                enemies: [
                    'Lv150 プロトバハムート', 'Lv200 アーカーシャ'
                ]
            }
        },
        ui_rework: {
            enabled: true,
            hides: {
                side_bar: {
                    left: true,
                    right: true,
                },
                assist: {
                    enabled: true,
                    hp_less_than: 50,
                    member_more_than: 30,
                }
            },
            sort: {
                enabled: true,
                ascend: false,
            }
        },
        features: {
            refresh: {
                attack: true,
                ability: {
                    enabled: true,
                    keywords: {
                        koenig_dekret: {
                            enabled: true,
                            keys: ['ケーニヒ・ベシュテレン']
                        },
                        coronal_ejection: {
                            enabled: true,
                            keys: ['攻撃行動', '2回']
                        },
                        secret_triad: {
                            enabled: true,
                            keys: ['シークレットトライアド']
                        },
                    }
                }
            },
            reload_type: 'back', // accepts: back | reload
            auto_back_to_assist: true,
            auto_redirect: {
                enabled: false,
            },
            battle_set: [
                'normal_attack_result.json',
                'ability_result.json',
                'fatal_chain_result.json',
                'summon_result.json'
            ]
        },
        delays: {
            enabled: true,
            factors: [100, 100, 100],
        },
        enums: {
            hash: {
                result: '#result',
                assist: '#quest/assist',
                raid: '#raid',
            },
            specified: {
                assist: {
                    list: '.prt-search-list',
                    card: '.btn-multi-raid.lis-raid.search',
                    hp_bar: '.prt-raid-gauge-inner',
                    members: '.prt-flees-in'
                },
                contribution: {
                    number: 'div.lis-user:nth-child(1) > div:nth-child(4)',
                }
            }
        }
    }

    const functions = {
        event: {
            onGameLoad: () => {
                if (options.ui_rework.enabled) {
                    functions.ui.hide.side_bar.left();
                    functions.ui.hide.side_bar.right();
                    functions.ui.hide.assist.do();
                    functions.ui.sort.assist.do();
                    functions.redirect.do();
                }
            },
            onGameUnload: () => {
                functions.intervals.unload();
            }
        },
        ui: {
            hide: {
                side_bar: {
                    left: () => {
                        if (options.ui_rework.hides.side_bar.left) {
                            const sbl = document.querySelector('body > div:first-child > div:first-child');
                            if (sbl) {
                                if (sbl.classList[0].startsWith('_')) {
                                    sbl.remove();
                                }
                            }
                        }
                    },
                    right: () => {
                        document.querySelector('#submenu')?.remove();
                    }
                },
                assist: {
                    do: () => {
                        const is_assist = functions.check.do(options.enums.hash.assist);
                        if (is_assist === false) return;

                        const config = options.ui_rework.hides.assist;
                        if (config.enabled) {
                            functions.intervals.append('assist_filter', 200, functions.ui.hide.assist.run);
                        }
                    },
                    run: () => {
                        const config = options.ui_rework.hides.assist;
                        const enums = options.enums.specified.assist;
                        const li = document.querySelector(enums.list);
                        if (!li) return;
                        const targets = li.querySelectorAll(enums.card);
                        if (targets) {
                            if (targets.length === 0) return;
                            targets.forEach((i) => {
                                const percentage = parseInt(i.querySelector(enums.hp_bar).style.width);
                                const members = parseInt(i.querySelector(enums.members).innerText);
                                const less_than = config.hp_less_than;
                                const more_than = config.hide_member_more_than;
                                if (percentage < less_than || members > more_than) {
                                    i.remove();
                                }
                            })
                        }
                    }
                }
            },
            sort: {
                assist: {
                    do: () => {
                        if (options.ui_rework.sort.enabled) {
                            functions.intervals.append('assist_sorter', 200, functions.ui.sort.assist.run);
                        }
                    },
                    run: () => {
                        const enums = options.enums.specified.assist;
                        const li = document.querySelector(enums.list);
                        if (!li) return;
                        const children = Array.from(li.children);
                        children.sort((a, b) => {
                            const a_hp = parseInt(a.querySelector(enums.hp_bar).style.width);
                            const b_hp = parseInt(b.querySelector(enums.hp_bar).style.width);

                            const a_members = parseInt(a.querySelector(enums.members).innerText);
                            const b_members = parseInt(b.querySelector(enums.members).innerText);

                            const use_asc = options.ui_rework.sort.ascend;

                            if (a_hp < b_hp) return use_asc ? -1 : 1;
                            if (a_hp > b_hp) return use_asc ? 1 : -1;

                            if (a_members < b_members) return use_asc ? 1 : -1;
                            if (a_members > b_members) return use_asc ? -1 : 1;
                        })
                        children.forEach(child => li.appendChild(child));
                    },
                }
            }
        },
        check: {
            do: (hash) => {
                return location.hash.includes(hash);
            }
        },
        game: {
            handle: (scenario, urlKey) => {
                const winObj = scenario.find(o => o['cmd'] === 'win');
                const winStatus = winObj
                    ? (winObj['is_last_raid'] === 1 ? 'raid' : 'battle')
                    : 'continue';

                if (winStatus === 'raid') return functions.redirect.back();
                if (winStatus === 'battle') return functions.redirect.reload();
                if (urlKey === 'normal_attack_result.json' && options.features.refresh.attack) return functions.redirect.reload();

                if (urlKey === 'ability_result.json' || urlKey === 'summon_result.json') {
                    const ability = scenario.find(o => o['cmd'] === 'ability');
                    if (ability) {
                        Object.keys(options.features.refresh.ability.keywords).forEach(keyword => {
                            const self = options.features.refresh.ability.keywords[keyword];
                            if (self.enabled) {
                                self.keys.some((key) => {
                                    if (ability.name.includes(key)) return functions.redirect.reload();
                                })
                            }
                        })
                    }
                }
            }
        },
        redirect: {
            do: () => {
                let enabled = false;
                if (options.current.mode === 'gold_hunt') {
                    enabled = true;
                } else if (options.features.auto_redirect.enabled === true) {
                    enabled = true;
                }

                if (options.current.auto_redirect_hash === '') {
                    enabled = false;
                }

                if (enabled) {
                    functions.redirect.goto(options.current.auto_redirect_hash);
                }
            },
            goto: (hash) => {
                setTimeout(() => {
                    window.open(`${location.origin}/${hash}`, '_self')
                }, functions.random.get())
            },
            reload: () => {
                const reloadType = options.features.reload_type;
                if (reloadType === 'back') {
                    functions.redirect.back();
                } else if (reloadType === 'reload') {
                    setTimeout(() => {
                        location.reload();
                    }, functions.random.get())
                }
            },
            back: () => {
                setInterval(() => {history.back()}, functions.random.get())
            },
            contribution: {
                do: () => {
                    let enabled = options.features.auto_back_to_assist;
                    if (options.current.mode === 'gold_hunt') enabled = true;
                    if (functions.check.do(options.enums.hash.raid)) enabled = true;

                    if (enabled === false) return;

                    const enums = options.enums.specified.contribution;
                    const contribution = document.querySelector(enums.number);
                    if (contribution) {
                        const contribution_number = parseInt(contribution.innerText);
                        if (contribution_number > options.current.contribution) {
                            functions.redirect.contribution.goto();
                        }
                    } else {
                        setTimeout(functions.redirect.contribution.do, 200);
                    }
                },
                goto: () => {
                    functions.redirect.goto(options.enums.hash.assist);
                }
            }
        },
        random: {
            ratio: (value, ratio) => {
                return Math.random() * (ratio / 100 * value);
            },
            get: () => {
                let result = 0;
                options.delays.factors.forEach(factor => {
                    result = result + functions.random.ratio(factor, options.current.ratio);
                })
                return result;
            }
        },
        intervals: {
            list: {},
            append: (name, timeout, callback) => {
                functions.intervals.list[name] = setInterval(callback, timeout);
            },
            remove: (name) => {
                clearInterval(functions.intervals.list[name]);
                functions.intervals.list[name] = undefined;
            },
            unload: () => {
                Object.keys(functions.intervals.list).forEach((name) => {
                    functions.intervals.remove(name);
                })
            }
        }
    }

    // 页面加载后隐藏侧栏
    window.addEventListener('load', functions.event.onGameLoad);
    window.addEventListener('DOMContentLoaded', functions.event.onGameLoad);
    window.addEventListener('popstate', functions.event.onGameLoad);
    window.addEventListener('unload', functions.event.onGameUnload);
    window.addEventListener('sierokarte-xhr', (e) => {
        const {key, scenario} = e.detail;
        functions.game.handle(scenario, key);
    });

    // 拦截 XHR
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url, ...rest) {
        this.addEventListener('load', () => {
            try {
                const targetUrl = new URL(this.responseURL);
                const key = targetUrl.pathname.split('/')[3];
                if (!this.responseURL.includes('.json')) return;
                const resp = JSON.parse(this.responseText);
                window.dispatchEvent(new CustomEvent('sierokarte-xhr', {
                    detail: {key, scenario: resp['scenario'] || resp}
                }));
            } catch (e) {
                console.log(e);
            }
        });
        return originalOpen.call(this, method, url, ...rest);
    };
})();