// ==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);
};
})();