// ==UserScript==
// @name 咕咕镇数据采集
// @namespace https://greasyfork.org/users/448113
// @version 1.4.47
// @description 咕咕镇数据采集,目前采集已关闭,兼作助手
// @author paraii
// @match https://www.guguzhen.com/*
// @grant GM_xmlhttpRequest
// @connect www.guguzhen.com
// @run-at document-body
// @license MIT License
// @downloadURL none
// ==/UserScript==
(function() {
'use strict'
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// common utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const g_modificationVersion = '2022-08-19 18:00:00';
const g_navigatorSelector = 'div.panel > div.panel-body > div.row > div.col-md-10 > div > ';
let kfUserSpan = document.querySelector(g_navigatorSelector + 'span.fyg_colpz06.fyg_f24');
const g_kfUser = kfUserSpan.innerText;
const g_guguzhenHome = '/fyg_index.php';
const g_guguzhenPK = '/fyg_pk.php';
const g_guguzhenEquip = '/fyg_equip.php';
const g_guguzhenWish = '/fyg_wish.php';
const g_guguzhenBeach = '/fyg_beach.php';
const g_guguzhenGift = '/fyg_gift.php';
const g_guguzhenShop = '/fyg_shop.php';
const g_autoTaskEnabledStorageKey = g_kfUser + '_autoTaskEnabled';
const g_autoTaskCheckStoneProgressStorageKey = g_kfUser + '_autoTaskCheckStoneProgress';
const g_indexRallyStorageKey = g_kfUser + '_indexRally';
const g_keepPkRecordStorageKey = g_kfUser + '_keepPkRecord';
const g_amuletGroupsStorageKey = g_kfUser + '_amulet_groups';
const g_equipmentExpandStorageKey = g_kfUser + '_equipment_Expand';
const g_equipmentStoreExpandStorageKey = g_kfUser + '_equipment_StoreExpand';
const g_equipmentBGStorageKey = g_kfUser + '_equipment_BG';
const g_stoneProgressEquipTipStorageKey = g_kfUser + '_stone_ProgressEquipTip';
const g_stoneProgressCardTipStorageKey = g_kfUser + '_stone_ProgressCardTip';
const g_stoneProgressHaloTipStorageKey = g_kfUser + '_stone_ProgressHaloTip';
const g_ignoreWishpoolExpirationStorageKey = g_kfUser + '_ignoreWishpoolExpiration';
const g_beachIgnoreStoreMysEquipStorageKey = g_kfUser + '_beach_ignoreStoreMysEquip';
const g_beachForceExpandStorageKey = g_kfUser + '_beach_forceExpand';
const g_beachBGStorageKey = g_kfUser + '_beach_BG';
const g_userDataStorageKeyConfig = [ g_kfUser, g_autoTaskEnabledStorageKey, g_autoTaskCheckStoneProgressStorageKey,
g_indexRallyStorageKey, g_keepPkRecordStorageKey, g_amuletGroupsStorageKey,
g_equipmentExpandStorageKey, g_equipmentStoreExpandStorageKey, g_equipmentBGStorageKey,
g_stoneProgressEquipTipStorageKey, g_stoneProgressCardTipStorageKey,
g_stoneProgressHaloTipStorageKey, g_ignoreWishpoolExpirationStorageKey,
g_beachIgnoreStoreMysEquipStorageKey, g_beachForceExpandStorageKey, g_beachBGStorageKey ];
const g_userDataStorageKeyExtra = [ 'attribute', 'cardName', 'title', 'over', 'halo_max', 'beachcheck', 'dataReward', 'keepcheck' ];
const USER_STORAGE_RESERVED_SEPARATORS = /[:;,|=+*%!#$&?<>{}^`"\\\/\[\]\r\n\t\v\s]/;
const USER_STORAGE_KEY_VALUE_SEPARATOR = ':';
kfUserSpan.style.cursor = 'pointer';
kfUserSpan.onclick = (() => { window.location.href = g_guguzhenHome; });
console.log(g_kfUser)
// perform a binary search. array must be sorted, but no matter in ascending or descending order.
// in this manner, you must pass in a proper comparer function for it works properly, aka, if the
// array was sorted in ascending order, then the comparer(a, b) should return a negative value
// while a < b or a positive value while a > b; otherwise, if the array was sorted in descending
// order, then the comparer(a, b) should return a positive value while a < b or a negative value
// while a > b, and in both, if a equals b, the comparer(a, b) should return 0. if you pass nothing
// or null / undefined value as comparer, then you must make sure about that the array was sorted
// in ascending order.
//
// in this particular case, we just want to check whether the array contains the value or not, we
// don't even need to point out the first place where the value appears (if the array actually
// contains the value), so we perform a simplest binary search and return an index (may not the
// first place where the value appears) or a negative value (means value not found) to indicate
// the search result.
function searchElement(array, value, fnComparer) {
if (array?.length > 0) {
fnComparer ??= ((a, b) => a < b ? -1 : (a > b ? 1 : 0));
let li = 0;
let hi = array.length - 1;
while (li <= hi) {
let mi = ((li + hi) >> 1);
let cr = fnComparer(value, array[mi]);
if (cr == 0) {
return mi;
}
else if (cr > 0) {
li = mi + 1;
}
else {
hi = mi - 1;
}
}
}
return -1;
}
// perform a binary insertion. the array and comparer must exactly satisfy as it in the searchElement
// function. this operation behaves sort-stable, aka, the newer inserting element will be inserted
// into the position after any existed equivalent elements.
function insertElement(array, value, fnComparer) {
if (array != null) {
fnComparer ??= ((a, b) => a < b ? -1 : (a > b ? 1 : 0));
let li = 0;
let hi = array.length - 1;
while (li <= hi) {
let mi = ((li + hi) >> 1);
let cr = fnComparer(value, array[mi]);
if (cr >= 0) {
li = mi + 1;
}
else {
hi = mi - 1;
}
}
array.splice(li, 0, value);
return li;
}
return -1;
}
// it's not necessary to have newArray been sorted, but the oldArray must be sorted since we are calling
// searchElement. if there are some values should be ignored in newArray, the comparer(a, b) should be
// implemented as return 0 whenever parameter a equals any of values that should be ignored.
function findNewObjects(newArray, oldArray, fnComparer, findIndices) {
let newObjects = [];
for (let i = (newArray?.length ?? 0) - 1; i >= 0; i--) {
if (searchElement(oldArray, newArray[i], fnComparer) < 0) {
newObjects.unshift(findIndices ? i : newArray[i]);
}
}
return newObjects;
}
function loadUserConfigData() {
return JSON.parse(localStorage.getItem(g_kfUser));
}
function saveUserConfigData(json) {
localStorage.setItem(g_kfUser, JSON.stringify(json));
}
function getPostData(functionPattern, dataPattern) {
let sc = document.getElementsByTagName('script');
for (let i = (sc?.length ?? 0) - 1; i >= 0 ; i--) {
let func = sc[i].innerText.match(functionPattern);
if (func != null) {
return func[0].match(dataPattern)[0];
}
}
return null;
}
// generic configuration items represented using checkboxes
const g_configCheckboxMap = new Map();
function setupConfigCheckbox(checkbox, configKey, fnPostProcess, fnParams) {
g_configCheckboxMap.set(configKey, { postProcess : fnPostProcess , params : fnParams });
checkbox.setAttribute('config-key', configKey);
checkbox.checked = (localStorage.getItem(configKey) == 'true');
checkbox.onchange = ((e) => {
let key = e.target.getAttribute('config-key');
let cfg = g_configCheckboxMap.get(key);
if (cfg != null) {
localStorage.setItem(key, e.target.checked);
if (cfg.postProcess != null) {
cfg.postProcess(e.target.checked, cfg.params);
}
}
});
return checkbox.checked;
}
// HTTP requests
var g_httpRequests = [];
function httpRequestRegister(request) {
if (request != null) {
g_httpRequests.push(request);
}
}
function httpRequestAbortAll() {
while (g_httpRequests.length > 0) {
g_httpRequests.pop().abort();
}
g_httpRequests = [];
}
function httpRequestClearAll() {
g_httpRequests = [];
}
// read objects from bag and store with title filter
const g_postMethod = 'POST'
const g_readUrl = 'https://www.guguzhen.com/fyg_read.php'
const g_postUrl = 'https://www.guguzhen.com/fyg_click.php'
const g_postHeader = { 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8' , 'Cookie' : document.cookie };
const g_networkTimeoutMS = 120 * 1000;
function beginReadObjects(bag, store, fnFurtherProcess, fnParams) {
if (bag != null || store != null) {
let request = GM_xmlhttpRequest({
method: g_postMethod,
url: g_readUrl,
headers: g_postHeader,
data: 'f=7',
onload: response => {
let div = document.createElement('div');
div.innerHTML = response.responseText;
if (bag != null) {
div.querySelectorAll('div.alert-danger > div.content > button.fyg_mp3')?.forEach((e) => { bag.push(e); });
}
if (store != null) {
div.querySelectorAll('div.alert-success > div.content > button.fyg_mp3')?.forEach((e) => { store.push(e); });
}
if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
});
httpRequestRegister(request);
}
else if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
function beginReadObjectIds(bagIds, storeIds, key, ignoreEmptyCell, fnFurtherProcess, fnParams) {
function parseObjectIds() {
if (bagIds != null) {
objectIdParseNodes(bag, bagIds, key, ignoreEmptyCell);
}
if (storeIds != null) {
objectIdParseNodes(store, storeIds, key, ignoreEmptyCell);
}
if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
let bag, store;
if (bagIds != null || storeIds != null) {
beginReadObjects(bag = [], store = [], parseObjectIds, null);
}
else if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
function objectIdParseNodes(nodes, ids, key, ignoreEmptyCell) {
for (let node of nodes) {
if (node.className?.endsWith('fyg_mp3')) {
let id = node.getAttribute('onclick')?.match(/\d+/)[0];
if (id != undefined) {
if (objectMatchTitle(node, key)) {
ids.push(parseInt(id));
}
}
else if (!ignoreEmptyCell) {
ids.push(-1);
}
}
}
}
function objectMatchTitle(node, key){
return (!(key?.length > 0) || (node.getAttribute('data-original-title') ??
node.getAttribute('title'))?.indexOf(key) >= 0);
}
// we wait the response(s) of the previous batch of request(s) to send another batch of request(s)
// rather than simply send them all within an inside foreach - which could cause too many requests
// to server simultaneously, that can be easily treated as D.D.O.S attack and therefor leads server
// to returns http status 503: Service Temporarily Unavailable
// * caution * the parameter 'objects' is required been sorted by their indices in ascending order
const g_ConcurrentRequestCount = { min : 1 , max : 8 , default : 4 };
const g_object_move_path = { bag2store : 0 , store2bag : 1 , bag2beach : 2 , beach2bag : 3 };
const g_object_move_data = [ null, null, null, null ];
var g_maxConcurrentRequests = g_ConcurrentRequestCount.default;
var g_objectMoveRequestsCount = 0;
var g_objectMoveTargetSiteFull = false;
function beginMoveObjects(objects, path, fnFurtherProcess, fnParams) {
if (!g_objectMoveTargetSiteFull && objects?.length > 0) {
g_object_move_data[g_object_move_path.bag2store] ??= (getPostData(/puti\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');
g_object_move_data[g_object_move_path.store2bag] ??= (getPostData(/puto\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');
g_object_move_data[g_object_move_path.bag2beach] ??= (getPostData(/stdel\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');
g_object_move_data[g_object_move_path.beach2bag] ??= (getPostData(/stpick\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*"/)?.slice(7, -1) ?? '');
if (!(g_object_move_data[path]?.length > 0)) {
if (!(g_object_move_data[g_object_move_path.store2bag]?.length > 0) &&
g_object_move_data[g_object_move_path.bag2store]?.length > 0) {
g_object_move_data[g_object_move_path.store2bag] =
g_object_move_data[g_object_move_path.bag2store].replace('c=21&', 'c=22&');
}
if (!(g_object_move_data[g_object_move_path.bag2store]?.length > 0) &&
g_object_move_data[g_object_move_path.store2bag]?.length > 0) {
g_object_move_data[g_object_move_path.bag2store] =
g_object_move_data[g_object_move_path.store2bag].replace('c=22&', 'c=21&');
}
if (!(g_object_move_data[g_object_move_path.bag2beach]?.length > 0) &&
g_object_move_data[g_object_move_path.beach2bag]?.length > 0) {
g_object_move_data[g_object_move_path.bag2beach] =
g_object_move_data[g_object_move_path.beach2bag].replace('c=1&', 'c=7&');
}
if (!(g_object_move_data[g_object_move_path.beach2bag]?.length > 0) &&
g_object_move_data[g_object_move_path.bag2beach]?.length > 0) {
g_object_move_data[g_object_move_path.beach2bag] =
g_object_move_data[g_object_move_path.bag2beach].replace('c=7&', 'c=1&');
}
}
if (g_object_move_data[path].length > 0) {
let ids = [];
while (ids.length < g_maxConcurrentRequests && objects.length > 0) {
let id = objects.pop();
if (id >= 0) {
ids.push(id);
}
}
if ((g_objectMoveRequestsCount = ids.length) > 0) {
while (ids.length > 0) {
let request = GM_xmlhttpRequest({
method: g_postMethod,
url: g_postUrl,
headers: g_postHeader,
data: g_object_move_data[path].replace('"+id+"', ids.shift()),
onload: response => {
if (path != g_object_move_path.bag2beach && 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);
}
}
const g_pirl_verify_data = '124';
var g_pirl_data = null;
var g_objectPirlRequestsCount = 0;
function beginPirlObjects(objects, fnFurtherProcess, fnParams) {
if (objects?.length > 0) {
g_pirl_data ??= (getPostData(/pirl\(id\)\{[\s\S]*\}/m, /data: ".*\+id\+.*\+pirlyz\+.*"/)?.
slice(7, -1).replace('"+pirlyz+"', g_pirl_verify_data) ?? '');
if (g_pirl_data.length > 0) {
let ids = [];
while (ids.length < g_maxConcurrentRequests && objects.length > 0) {
ids.push(objects.pop());
}
if ((g_objectPirlRequestsCount = ids.length) > 0) {
while (ids.length > 0) {
let request = GM_xmlhttpRequest({
method: g_postMethod,
url: g_postUrl,
headers: g_postHeader,
data: g_pirl_data.replace('"+id+"', ids.shift()),
onload: response => {
if (!/\d+/.test(response.responseText.trim()) && response.responseText.indexOf('果核') < 0) {
console.log(response.responseText);
}
if (--g_objectPirlRequestsCount == 0) {
beginPirlObjects(objects, fnFurtherProcess, fnParams);
}
}
});
httpRequestRegister(request);
}
return;
}
}
}
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(/
[^\d]*(\d+)[^\d]*(\d+)[^<]+<\/h3>/);
if (haloPS?.length == 3) {
haloInfo.push(parseInt(haloPS[1]));
haloInfo.push(parseInt(haloPS[2]));
}
else {
haloInfo.push(0);
haloInfo.push(0);
}
let halo = [];
for (let item of response.responseText.matchAll(/halotfzt2\((\d+)\)/g)) {
halo.push(item[1]);
}
haloInfo.push(halo);
asyncOperations--;
},
onerror : err => {
error++;
asyncOperations--;
},
ontimeout : err => {
error++;
asyncOperations--;
}
});
}
let timeout = 0;
let timer = setInterval(() => {
if (asyncOperations == 0 || error > 0 || (++timeout * 200) >= g_networkTimeoutMS) {
clearInterval(timer);
if (asyncOperations > 0) {
requestRole?.abort();
requestHalo?.abort();
}
if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
}, 200);
}
function beginReadWishpool(points, misc, fnFurtherProcess, fnParams) {
GM_xmlhttpRequest({
method: g_postMethod,
url: g_readUrl,
headers: g_postHeader,
data: `f=19`,
onload: response => {
let a = response.responseText.split('#');
if (misc != null) {
misc[0] = a[0];
misc[1] = a[1];
misc[2] = a[2];
}
if (points != null) {
for (let i = a.length - 1; i >= 3; i--) {
points[i - 3] = a[i];
}
}
if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
});
}
function beginReadStoneProgress(progresses, stones, fnFurtherProcess, fnParams) {
GM_xmlhttpRequest({
method: g_postMethod,
url: g_readUrl,
headers: g_postHeader,
data: `f=21`,
onload: response => {
let div = document.createElement('div');
div.innerHTML = response.responseText;
if (progresses != null) {
let equipProgresses = div.querySelectorAll('span.fyg_f24');
if (equipProgresses?.length == 3) {
for (let prog of equipProgresses) {
progresses.push(prog.innerText ?? '');
}
}
}
if (stones != null) {
let stoneInfos = div.querySelectorAll('div.col-sm-2.fyg_tc > button.btn');
if (stoneInfos?.length == 6) {
for (let stone of stoneInfos) {
let type = (stone.getAttribute('onclick')?.match(/\d+/)?.[0] ?? '0');
let infos = stone.innerHTML?.match(/(.石)(上限(\d+)).+?>(\d+) {
item.index = index;
g_amuletBuffMap.set(item.index, item);
g_amuletBuffMap.set(item.name, item);
g_amuletBuffMap.set(item.shortMark, item);
});
function Amulet() {
this.id = -1;
this.type = -1;
this.level = 0;
this.enhancement = 0;
this.buffCode = 0;
this.text = null;
this.reset = (() => {
this.id = -1;
this.type = -1;
this.level = 0;
this.enhancement = 0;
this.buffCode = 0;
this.text = null;
});
this.isValid = (() => {
return (this.type >= 0 && this.type <= 2 && this.buffCode > 0);
});
this.fromCode = ((code) => {
if (!isNaN(code)) {
this.type = Math.trunc(code / AMULET_TYPE_ID_FACTOR) % 10;
this.level = Math.trunc(code / AMULET_LEVEL_ID_FACTOR) % 10;
this.enhancement = Math.trunc(code / AMULET_ENHANCEMENT_FACTOR) % 10;
this.buffCode = code % AMULET_BUFF_MAX_FACTOR;
}
else {
this.reset();
}
return (this.isValid() ? this : null);
});
this.fromBuffText = ((text) => {
if (text?.length > 0) {
let nb = text.split(' = ');
if (nb.length == 2) {
this.id = -1;
this.type = g_amuletTypeIds[nb[0].slice(2, 4)];
this.level = g_amuletLevelIds[nb[0].slice(0, 2)];
this.enhancement = parseInt(nb[0].match(/\d+/)[0]);
this.buffCode = 0;
nb[1].replaceAll(/(\+)|( 点)|( %)/g, '').split(',').forEach((buff) => {
let nv = buff.trim().split(' ');
this.buffCode += ((100 ** (g_amuletBuffMap.get(nv[0]).index % 6)) * parseInt(nv[1]));
});
if (this.isValid()) {
this.text = nb[1];
return this;
}
}
}
this.reset();
return null;
});
this.fromNode = ((node) => {
if (node?.className?.endsWith('fyg_mp3') && node.innerText.indexOf('+') >= 0) {
let id = node.getAttribute('onclick');
let typeName = (node.getAttribute('data-original-title') ?? node.getAttribute('title'));
let content = node.getAttribute('data-content');
if (id != null && typeName?.length > 4 && content?.length > 0 &&
!isNaN(this.type = g_amuletTypeIds[typeName.slice(2, 4)]) &&
!isNaN(this.level = g_amuletLevelIds[typeName.slice(0, 2)]) &&
!isNaN(this.id = parseInt(id.match(/\d+/)[0])) &&
!isNaN(this.enhancement = parseInt(node.innerText.match(/\d+/)[0]))) {
this.buffCode = 0;
this.text = '';
let attr = null;
let regex = /
]*>([^<]+)<[^>]*>\+(\d+)[^<]*<\/span><\/p>/g;
while ((attr = regex.exec(content))?.length == 3) {
let buffMeta = g_amuletBuffMap.get(attr[1]);
if (buffMeta != null) {
this.buffCode += ((100 ** (buffMeta.index % 6)) * attr[2]);
this.text += `${this.text.length > 0 ? ', ' : ''}${attr[1]} +${attr[2]} ${buffMeta.unit}`;
}
}
if (this.isValid()) {
return this;
}
}
}
this.reset();
return null;
});
this.fromAmulet = ((amulet) => {
if (amulet?.isValid()) {
this.id = amulet.id;
this.type = amulet.type;
this.level = amulet.level;
this.enhancement = amulet.enhancement;
this.buffCode = amulet.buffCode;
this.text = amulet.text;
}
else {
this.reset();
}
return (this.isValid() ? this : null);
});
this.getCode = (() => {
if (this.isValid()) {
return (this.type * AMULET_TYPE_ID_FACTOR +
this.level * AMULET_LEVEL_ID_FACTOR +
this.enhancement * AMULET_ENHANCEMENT_FACTOR +
this.buffCode);
}
return -1;
});
this.getBuff = (() => {
let buffs = {};
if (this.isValid()) {
let code = this.buffCode;
let type = this.type * 6;
g_amuletBuffs.slice(type, type + 6).forEach((buff) => {
let v = (code % 100);
if (v > 0) {
buffs[buff.name] = v;
}
code = Math.trunc(code / 100);
});
}
return buffs;
});
this.getTotalPoints = (() => {
let points = 0;
if (this.isValid()) {
let code = this.buffCode;
for(let i = 0; i < 6; i++) {
points += (code % 100);
code = Math.trunc(code / 100);
}
}
return points;
});
this.formatName = (() => {
if (this.isValid()) {
return `${g_amuletLevelNames[this.level]}${g_amuletTypeNames[this.type]} (+${this.enhancement})`;
}
return null;
});
this.formatBuff = (() => {
if (this.isValid()) {
if (this.text?.length > 0) {
return this.text;
}
this.text = '';
let buffs = this.getBuff();
for (let buff in buffs) {
this.text += `${this.text.length > 0 ? ', ' : ''}${buff} +${buffs[buff]} ${g_amuletBuffMap.get(buff).unit}`;
}
}
return this.text;
});
this.formatBuffText = (() => {
if (this.isValid()) {
return this.formatName() + ' = ' + this.formatBuff();
}
return null;
});
this.formatShortMark = (() => {
let text = this.formatBuff()?.replaceAll(/(\+)|( 点)|( %)/g, '');
if (text?.length > 0) {
for (let buff in this.getBuff()) {
text = text.replaceAll(buff, g_amuletBuffMap.get(buff).shortMark);
}
return this.formatName() + ' = ' + text;
}
return null;
});
this.compareMatch = ((other, ascType) => {
if (!this.isValid()) {
return 1;
}
else if (!other?.isValid()) {
return -1;
}
let delta = other.type - this.type;
if (delta != 0) {
return (ascType ? -delta : delta);
}
return (other.buffCode - this.buffCode);
});
this.compareTo = ((other, ascType) => {
if (!this.isValid()) {
return 1;
}
else if (!other?.isValid()) {
return -1;
}
let delta = other.type - this.type;
if (delta != 0) {
return (ascType ? -delta : delta);
}
let tbuffs = this.formatBuffText().split(' = ')[1].replaceAll(/(\+)|( 点)|( %)/g, '').split(', ');
let obuffs = other.formatBuffText().split(' = ')[1].replaceAll(/(\+)|( 点)|( %)/g, '').split(', ');
let bl = Math.min(tbuffs.length, obuffs.length);
for (let i = 0; i < bl; i++) {
let tbuff = tbuffs[i].split(' ');
let obuff = obuffs[i].split(' ');
if ((delta = g_amuletBuffMap.get(tbuff[0]).index - g_amuletBuffMap.get(obuff[0]).index) != 0 ||
(delta = parseInt(obuff[1]) - parseInt(tbuff[1])) != 0) {
return delta;
}
}
if ((delta = obuffs.length - tbuffs.length) != 0 ||
(delta = other.level - this.level) != 0 ||
(delta = other.enhancement - this.enhancement) != 0) {
return delta;
}
return 0;
});
}
function AmuletGroup(persistenceString) {
this.buffSummary = {
力量 : 0,
敏捷 : 0,
智力 : 0,
体魄 : 0,
精神 : 0,
意志 : 0,
物理攻击 : 0,
魔法攻击 : 0,
速度 : 0,
生命护盾回复效果 : 0,
最大生命值 : 0,
最大护盾值 : 0,
固定生命偷取 : 0,
固定反伤 : 0,
固定暴击几率 : 0,
固定技能几率 : 0,
物理防御效果 : 0,
魔法防御效果 : 0
};
this.name = null;
this.items = [];
this.isValid = (() => {
return (this.items.length > 0 && amuletIsValidGroupName(this.name));
});
this.count = (() => {
return this.items.length;
});
this.clear = (() => {
this.items = [];
for (let buff in this.buffSummary) {
this.buffSummary[buff] = 0;
}
});
this.add = ((amulet) => {
if (amulet?.isValid()) {
let buffs = amulet.getBuff();
for (let buff in buffs) {
this.buffSummary[buff] += buffs[buff];
}
return insertElement(this.items, amulet, (a, b) => a.compareTo(b, true));
}
return -1;
});
this.remove = ((amulet) => {
if (this.isValid() && amulet?.isValid()) {
let i = searchElement(this.items, amulet, (a, b) => a.compareTo(b, true));
if (i >= 0) {
let buffs = amulet.getBuff();
for (let buff in buffs) {
this.buffSummary[buff] -= buffs[buff];
}
this.items.splice(i, 1);
return true;
}
}
return false;
});
this.removeId = ((id) => {
if (this.isValid()) {
let i = this.items.findIndex((a) => a.id == id);
if (i >= 0) {
let amulet = this.items[i];
let buffs = amulet.getBuff();
for (let buff in buffs) {
this.buffSummary[buff] -= buffs[buff];
}
this.items.splice(i, 1);
return amulet;
}
}
return null;
});
this.merge = ((group) => {
group?.items?.forEach((am) => { this.add(am); });
return this;
});
this.validate = ((amulets) => {
if (this.isValid()) {
let mismatch = 0;
let al = this.items.length;
let i = 0;
if (amulets?.length > 0) {
amulets = amulets.slice().sort((a, b) => a.compareMatch(b));
for ( ; amulets.length > 0 && i < al; i++) {
let mi = searchElement(amulets, this.items[i], (a, b) => a.compareMatch(b));
if (mi >= 0) {
// remove a matched amulet from the amulet pool can avoid one single amulet matches all
// the equivalent objects in the group.
// let's say two (or even more) AGI +5 apples in one group is fairly normal, if we just
// have only one equivalent apple in the amulet pool and we don't remove it when the
// first match happens, then the 2nd apple will get matched later, the consequence would
// be we can never find the mismatch which should be encountered at the 2nd apple
this.items[i].fromAmulet(amulets[mi]);
amulets.splice(mi, 1);
}
else {
mismatch++;
}
}
}
if (i > mismatch) {
this.items.sort((a, b) => a.compareTo(b, true));
}
if (i < al) {
mismatch += (al - i);
}
return (mismatch == 0);
}
return false;
});
this.findIndices = ((amulets) => {
let indices;
let al;
if (this.isValid() && (al = (amulets?.length ?? 0)) > 0) {
let items = this.items.slice().sort((a, b) => a.compareMatch(b));
for (let i = 0; items.length > 0 && i < al; i++) {
let mi;
if (amulets[i]?.id >= 0 && (mi = searchElement(items, amulets[i], (a, b) => a.compareMatch(b))) >= 0) {
// similar to the 'validate', remove the amulet from the search list when we found
// a match item in first time to avoid the duplicate founding, e.g. say we need only
// one AGI +5 apple in current group and we actually have 10 of AGI +5 apples in store,
// if we found the first matched itme in store and record it's index but not remove it
// from the temporary searching list, then we will continuously reach this kind of
// founding and recording until all those 10 AGI +5 apples are matched and processed,
// this obviously ain't the result what we expected
(indices ??= []).push(i);
items.splice(mi, 1);
}
}
}
return indices;
});
this.parse = ((persistenceString) => {
this.clear();
if (persistenceString?.length > 0) {
let elements = persistenceString.split(AMULET_STORAGE_GROUPNAME_SEPARATOR);
if (elements.length == 2) {
let name = elements[0].trim();
if (amuletIsValidGroupName(name)) {
let items = elements[1].split(AMULET_STORAGE_AMULET_SEPARATOR);
let il = items.length;
for (let i = 0; i < il; i++) {
if (this.add((new Amulet()).fromCode(parseInt(items[i]))) < 0) {
this.clear();
break;
}
}
if (this.count() > 0) {
this.name = name;
}
}
}
}
return (this.count() > 0);
});
this.formatBuffSummary = ((linePrefix, lineSuffix, lineSeparator, ignoreMaxValue) => {
if (this.isValid()) {
let str = '';
let nl = '';
g_amuletBuffs.forEach((buff) => {
let v = this.buffSummary[buff.name];
if (v > 0) {
str += `${nl}${linePrefix}${buff.name} +${ignoreMaxValue ? v : Math.min(v, buff.maxValue)} ${buff.unit}${lineSuffix}`;
nl = lineSeparator;
}
});
return str;
}
return '';
});
this.formatBuffShortMark = ((keyValueSeparator, itemSeparator, ignoreMaxValue) => {
if (this.isValid()) {
let str = '';
let sp = '';
g_amuletBuffs.forEach((buff) => {
let v = this.buffSummary[buff.name];
if (v > 0) {
str += `${sp}${buff.shortMark}${keyValueSeparator}${ignoreMaxValue ? v : Math.min(v, buff.maxValue)}`;
sp = itemSeparator;
}
});
return str;
}
return '';
});
this.formatItems = ((linePrefix, erroeLinePrefix, lineSuffix, errorLineSuffix, lineSeparator) => {
if (this.isValid()) {
let str = '';
let nl = '';
this.items.forEach((amulet) => {
str += `${nl}${amulet.id < 0 ? erroeLinePrefix : linePrefix}${amulet.formatBuffText()}` +
`${amulet.id < 0 ? errorLineSuffix : lineSuffix}`;
nl = lineSeparator;
});
return str;
}
return '';
});
this.getDisplayStringLineCount = (() => {
if (this.isValid()) {
let lines = 0;
g_amuletBuffs.forEach((buff) => {
if (this.buffSummary[buff.name] > 0) {
lines++;
}
});
return lines + this.items.length;
}
return 0;
});
this.formatPersistenceString = (() => {
if (this.isValid()) {
let codes = [];
this.items.forEach((amulet) => {
codes.push(amulet.getCode());
});
return `${this.name}${AMULET_STORAGE_GROUPNAME_SEPARATOR}${codes.join(AMULET_STORAGE_AMULET_SEPARATOR)}`;
}
return '';
});
this.parse(persistenceString);
}
function AmuletGroupCollection(persistenceString) {
this.items = {};
this.itemCount = 0;
this.count = (() => {
return this.itemCount;
});
this.contains = ((name) => {
return (this.items[name] != undefined);
});
this.add = ((item) => {
if (item?.isValid()) {
if (!this.contains(item.name)) {
this.itemCount++;
}
this.items[item.name] = item;
return true;
}
return false;
});
this.remove = ((name) => {
if (this.contains(name)) {
delete this.items[name];
this.itemCount--;
return true;
}
return false;
});
this.clear = (() => {
for (let name in this.items) {
delete this.items[name];
}
this.itemCount = 0;
});
this.get = ((name) => {
return this.items[name];
});
this.rename = ((oldName, newName) => {
if (amuletIsValidGroupName(newName)) {
let group = this.items[oldName];
if (this.remove(oldName)) {
group.name = newName;
return this.add(group);
}
}
return false;
});
this.toArray = (() => {
let groups = [];
for (let name in this.items) {
groups.push(this.items[name]);
}
return groups;
});
this.parse = ((persistenceString) => {
this.clear();
if (persistenceString?.length > 0) {
let groupStrings = persistenceString.split(AMULET_STORAGE_GROUP_SEPARATOR);
let gl = groupStrings.length;
for (let i = 0; i < gl; i++) {
if (!this.add(new AmuletGroup(groupStrings[i]))) {
this.clear();
break;
}
}
}
return (this.count() > 0);
});
this.formatPersistenceString = (() => {
let str = '';
let ns = '';
for (let name in this.items) {
str += (ns + this.items[name].formatPersistenceString());
ns = AMULET_STORAGE_GROUP_SEPARATOR;
}
return str;
});
this.parse(persistenceString);
}
function amuletIsValidGroupName(groupName) {
return (groupName?.length > 0 && groupName.length < 32 && groupName.search(USER_STORAGE_RESERVED_SEPARATORS) < 0);
}
function amuletSaveGroups(groups) {
if (groups?.count() > 0) {
localStorage.setItem(g_amuletGroupsStorageKey, groups.formatPersistenceString());
}
else {
localStorage.removeItem(g_amuletGroupsStorageKey);
}
}
function amuletLoadGroups() {
return new AmuletGroupCollection(localStorage.getItem(g_amuletGroupsStorageKey));
}
function amuletClearGroups() {
localStorage.removeItem(g_amuletGroupsStorageKey);
}
function amuletSaveGroup(group) {
if (group?.isValid()) {
let groups = amuletLoadGroups();
if (groups.add(group)) {
amuletSaveGroups(groups);
}
}
}
function amuletLoadGroup(groupName) {
return amuletLoadGroups().get(groupName);
}
function amuletDeleteGroup(groupName) {
let groups = amuletLoadGroups();
if (groups.remove(groupName)) {
amuletSaveGroups(groups);
}
}
function amuletCreateGroupFromArray(groupName, amulets) {
if (amulets?.length > 0 && amuletIsValidGroupName(groupName)) {
let group = new AmuletGroup(null);
for (let amulet of amulets) {
if (group.add(amulet) < 0) {
group.clear();
break;
}
}
if (group.count() > 0) {
group.name = groupName;
return group;
}
}
return null;
}
function amuletNodesToArray(nodes, array, key) {
array ??= [];
let amulet;
for (let node of nodes) {
if (objectMatchTitle(node, key) && (amulet ??= new Amulet()).fromNode(node)?.isValid()) {
array.push(amulet);
amulet = null;
}
}
return array;
}
function beginReadAmulets(bagAmulets, storeAmulets, key, fnFurtherProcess, fnParams) {
function parseAmulets() {
if (bagAmulets != null) {
amuletNodesToArray(bag, bagAmulets, key);
}
if (storeAmulets != null) {
amuletNodesToArray(store, storeAmulets, key);
}
if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
let bag, store;
if (bagAmulets != null || storeAmulets != null) {
beginReadObjects(bag = [], store = [], parseAmulets, null);
}
else if (fnFurtherProcess != null) {
fnFurtherProcess(fnParams);
}
}
function beginMoveAmulets({ groupName, amulets, path, proc, params }) {
let indices = amuletLoadGroup(groupName)?.findIndices(amulets)?.sort((a, b) => b - a);
let ids;
while (indices?.length > 0) {
(ids ??= []).push(amulets[indices.pop()].id);
}
beginMoveObjects(ids, path, proc, params);
}
function beginLoadAmuletGroupFromStore(amulets, groupName, fnFurtherProcess, fnParams) {
if (amulets?.length > 0) {
let store = amuletNodesToArray(amulets);
beginMoveAmulets({ groupName : groupName, amulets : store, path : g_object_move_path.store2bag,
proc : fnFurtherProcess, params : fnParams });
}
else {
beginReadAmulets(null, amulets = [], null, beginMoveAmulets,
{ groupName : groupName, amulets : amulets, path : g_object_move_path.store2bag,
proc : fnFurtherProcess, params : fnParams });
}
}
function beginUnloadAmuletGroupFromBag(amulets, groupName, fnFurtherProcess, fnParams) {
if (amulets?.length > 0) {
let bag = amuletNodesToArray(amulets);
beginMoveAmulets({ groupName : groupName, amulets : bag, path : g_object_move_path.bag2store,
proc : fnFurtherProcess, params : fnParams });
}
else {
beginReadAmulets(amulets, null, null, beginMoveAmulets,
{ groupName : groupName, amulets : amulets, path : g_object_move_path.bag2store,
proc : fnFurtherProcess, params : fnParams });
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// equipment utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
function equipmentInfoParseNode(node) {
if (node?.className?.split(' ').length > 2 && node.className.endsWith('fyg_mp3') && node.innerText.indexOf('+') < 0) {
let title = (node.getAttribute('data-original-title') ?? node.getAttribute('title'));
if (title?.length > 0) {
let name = title?.substring(title.lastIndexOf('>') + 1).trim();
name = (g_equipMap.get(name)?.shortMark ?? g_equipMap.get(name.substring(2))?.shortMark);
if (name?.length > 0) {
let attr = node.getAttribute('data-content')?.match(/>\s*\d+\s?%\s*(\d+));
if (attr?.length > 0 && lv?.length > 0) {
let mys = (node.getAttribute('data-content')?.match(/\[神秘属性\]/) == null ? 0 : 1);
let id = node.getAttribute('onclick')?.match(/\d+/)[0];
return [ name, lv[1],
attr[0].match(/\d+/)[0], attr[1].match(/\d+/)[0],
attr[2].match(/\d+/)[0], attr[3].match(/\d+/)[0],
mys, id ];
}
}
}
}
return null;
}
function equipmentNodesToInfoArray(nodes, array) {
array ??= [];
for (let i = (nodes?.length ?? 0) - 1; i >= 0; i--) {
let e = equipmentInfoParseNode(nodes[i]);
if (e != null) {
array.unshift(e);
}
}
return array;
}
const g_equipmentLevelPoints = [ 200, 321, 419, 516, 585 ];
const g_equipmentLevelName = [ '普通', '幸运', '稀有', '史诗', '传奇' ];
const g_equipmentLevelBGColor = [ '#e0e8e8', '#c0e0ff', '#c0ffc0', '#ffffc0', '#ffd0d0' ];
const g_equipmentLevelTipClass = [ 'popover-primary', 'popover-info', 'popover-success', 'popover-warning', 'popover-danger' ];
function equipmentGetLevel(e) {
let eq = (Array.isArray(e) ? e : equipmentInfoParseNode(e));
if (eq != null) {
let p = parseInt(eq[2]) + parseInt(eq[3]) + parseInt(eq[4]) + parseInt(eq[5]) + (parseInt(eq[6]) * 100);
for (var i = g_equipmentLevelPoints.length - 1; i > 0 && p < g_equipmentLevelPoints[i]; i--);
return i;
}
else if ((eq = (new Amulet()).fromNode(e))?.isValid()) {
return (eq.level + 2)
}
return -1;
}
function equipmentInfoComparer(e1, e2) {
let delta = g_equipMap.get(e1[0]).index - g_equipMap.get(e2[0]).index;
for (let i = 1; i < 7 && delta == 0; delta = parseInt(e1[i]) - parseInt(e2[i++]));
return delta;
}
function objectNodeComparer(e1, e2) {
let eq1 = equipmentInfoParseNode(e1);
if (eq1 != null) {
e1.setAttribute('data-meta-index', g_equipMap.get(eq1[0]).index);
}
let eq2 = equipmentInfoParseNode(e2);
if (eq2 != null) {
e2.setAttribute('data-meta-index', g_equipMap.get(eq2[0]).index);
}
if (eq1 == null && eq2 == null) {
return ((new Amulet()).fromNode(e1)?.compareTo((new Amulet()).fromNode(e2)) ?? 1);
}
else if (eq1 == null) {
return 1;
}
else if (eq2 == null) {
return -1;
}
return equipmentInfoComparer(eq1, eq2);
}
function objectIsEmptyNode(node) {
return (node?.innerText == '空');
}
function objectEmptyNodesCount(nodes) {
let nl = (nodes?.length ?? 0);
for (var i = nl - 1; i >= 0 && nodes[i].innerText == '空'; i--);
return (nl - 1 - i);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// bag & store utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
function findAmuletIds(container, amulets, ids, maxCount) {
ids ??= [];
let cl = (container?.length ?? 0);
if (cl > 0 && amulets?.length > 0) {
let ams = amuletNodesToArray(container);
for (let i = ams.length - 1; i >= 0 && amulets.length > 0 && ids.length < (maxCount ?? cl); i--) {
for (let j = amulets.length - 1; j >= 0; j--) {
if (ams[i].compareTo(amulets[j]) == 0) {
amulets.splice(j, 1);
ids.unshift(ams[i].id);
break;
}
}
}
}
return ids;
}
function findEquipmentIds(container, equips, ids, maxCount) {
ids ??= [];
let cl = (container?.length ?? 0);
if (cl > 0 && equips?.length > 0) {
let eqs = equipmentNodesToInfoArray(container);
for (let i = eqs.length - 1; i >= 0 && equips.length > 0 && ids.length < (maxCount ?? cl); i--) {
for (let j = equips.length - 1; j >= 0; j--) {
if (equipmentInfoComparer(eqs[i], equips[j]) == 0) {
equips.splice(j, 1);
ids.unshift(parseInt(eqs[i][7]));
break;
}
}
}
}
return ids;
}
function beginClearBag(bag, key, fnFurtherProcess, fnParams) {
function beginClearBagObjects(objects) {
beginMoveObjects(objects, g_object_move_path.bag2store, fnFurtherProcess, fnParams);
}
let objects = [];
if (bag?.length > 0) {
objectIdParseNodes(bag, objects, key, true);
beginClearBagObjects(objects);
}
else {
beginReadObjectIds(objects, null, key, true, beginClearBagObjects, objects);
}
}
function beginRestoreObjects(store, amulets, equips, fnFurtherProcess, fnParams) {
function readStoreCompletion() {
beginRestoreObjects(store, amulets, equips, fnFurtherProcess, fnParams);
}
if (store == null) {
beginReadObjects(null, store = [], readStoreCompletion, null);
}
else {
let ids = findAmuletIds(store, amulets);
findEquipmentIds(store, equips, ids);
beginMoveObjects(ids, g_object_move_path.store2bag, fnFurtherProcess, fnParams);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// generic popups
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const g_genericPopupContainerId = 'generic-popup-container';
const g_genericPopupClass = 'generic-popup';
const g_genericPopupId = g_genericPopupClass;
const g_genericPopupContentContainerId = 'generic-popup-content-container';
const g_genericPopupContentClass = 'generic-popup-content';
const g_genericPopupContentId = g_genericPopupContentClass;
const g_genericPopupFixedContentId = 'generic-popup-content-fixed';
const g_genericPopupInformationTipsId = 'generic-popup-information-tips';
const g_genericPopupProgressClass = g_genericPopupClass;
const g_genericPopupProgressId = 'generic-popup-progress';
const g_genericPopupProgressContentClass = 'generic-popup-content-progress';
const g_genericPopupProgressContentId = g_genericPopupProgressContentClass;
const g_genericPopupTopLineDivClass = 'generic-popup-top-line-container';
const g_genericPopupTitleTextClass = 'generic-popup-title-text';
const g_genericPopupTitleTextId = g_genericPopupTitleTextClass;
const g_genericPopupTitleButtonContainerId = 'generic-popup-title-button-container';
const g_genericPopupFootButtonContainerId = 'generic-popup-foot-button-container';
const g_genericPopupBackgroundColor = '#ebf2f9';
const g_genericPopupBackgroundColorAlt = '#dbe2e9';
const g_genericPopupBorderColor = '#3280fc';
const g_genericPopupTitleTextColor = '#ffffff';
const g_genericPopupStyle =
``;
const g_genericPopupHTML =
`${g_genericPopupStyle}