// ==UserScript==
// @name GuguTown Helper
// @name:zh-CN 咕咕镇助手
// @name:zh-TW 咕咕鎮助手
// @name:ja 咕咕镇助手
// @namespace https://github.com/GuguTown/GuguTownHelper
// @homepage https://github.com/GuguTown/GuguTownHelper
// @version 2.3.4
// @description WebGame GuguTown Helper
// @description:zh-CN 气人页游 咕咕镇助手
// @description:zh-TW 氣人頁遊 咕咕鎮助手
// @description:ja オンラインゲーム 咕咕镇助手
// @author paraii & zyxboy
// @match https://www.guguzhen.com/*
// @match https://www.momozhen.com/*
// @license MIT License
// @downloadURL none
// ==/UserScript==
/* eslint-env jquery */
function gudaq(){
'use strict'
const g_version = '2.3.4 (RP)';
const g_modiTime = '2023-06-03 19:00:00';
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// common utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const g_navigatorSelector = 'body > div > div.row > div.panel > div.panel-body > div';
const g_kfUser = document.querySelector(g_navigatorSelector + ' > button.btn.btn-lg')?.innerText;
if (!(g_kfUser?.length > 0)) {
console.log('数据采集: 咕咕镇版本不匹配或正在测试');
return;
}
console.log('数据采集: ' + g_kfUser);
const g_guguzhenHome = '/fyg_index.php';
const g_guguzhenBeach = '/fyg_beach.php';
const g_guguzhenPK = '/fyg_pk.php';
const g_guguzhenEquip = '/fyg_equip.php';
const g_guguzhenWish = '/fyg_wish.php';
const g_guguzhenGem = '/fyg_gem.php';
const g_guguzhenShop = '/fyg_shop.php';
const g_showSolutionPanelStorageKey = g_kfUser + '_showSolutionPanel';
const g_indexRallyStorageKey = g_kfUser + '_indexRally';
const g_keepPkRecordStorageKey = g_kfUser + '_keepPkRecord';
const g_amuletGroupCollectionStorageKey = 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_beachForceExpandStorageKey = g_kfUser + '_beach_forceExpand';
const g_beachBGStorageKey = g_kfUser + '_beach_BG';
const g_gemConfigStorageKey = g_kfUser + '_gem_Config';
const g_forgeHistoryStorageKey = g_kfUser + '_forgeHistory';
const g_userDataStorageKeyConfig = [ g_kfUser, g_showSolutionPanelStorageKey, g_indexRallyStorageKey, g_keepPkRecordStorageKey,
g_amuletGroupCollectionStorageKey, g_equipmentExpandStorageKey, g_equipmentStoreExpandStorageKey,
g_equipmentBGStorageKey, g_beachForceExpandStorageKey, g_beachBGStorageKey, g_gemConfigStorageKey,
g_forgeHistoryStorageKey ];
// deprecated
const g_amuletGroupsStorageKey = g_kfUser + '_amulet_groups';
const g_autoTaskEnabledStorageKey = g_kfUser + '_autoTaskEnabled';
const g_autoTaskCheckStoneProgressStorageKey = g_kfUser + '_autoTaskCheckStoneProgress';
const g_ignoreWishpoolExpirationStorageKey = g_kfUser + '_ignoreWishpoolExpiration';
const g_stoneProgressEquipTipStorageKey = g_kfUser + '_stone_ProgressEquipTip';
const g_stoneProgressCardTipStorageKey = g_kfUser + '_stone_ProgressCardTip';
const g_stoneProgressHaloTipStorageKey = g_kfUser + '_stone_ProgressHaloTip';
const g_stoneOperationStorageKey = g_kfUser + '_stoneOperation';
const g_forgeBoxUsageStorageKey = g_kfUser + '_forgeBoxUsageStorageKey';
const g_beachIgnoreStoreMysEquipStorageKey = g_kfUser + '_beach_ignoreStoreMysEquip';
const g_userDataStorageKeyExtra = [ g_amuletGroupsStorageKey, g_autoTaskEnabledStorageKey, g_autoTaskCheckStoneProgressStorageKey,
g_ignoreWishpoolExpirationStorageKey, g_stoneProgressEquipTipStorageKey,
g_stoneProgressCardTipStorageKey, g_stoneProgressHaloTipStorageKey,
g_stoneOperationStorageKey, g_forgeBoxUsageStorageKey, g_beachIgnoreStoreMysEquipStorageKey,
'attribute', 'cardName', 'title', 'over', 'halo_max', 'beachcheck', 'dataReward', 'keepcheck' ];
// deprecated
const USER_STORAGE_RESERVED_SEPARATORS = /[:;,|=+*%!#$&?<>{}^`"\\\/\[\]\r\n\t\v\s]/;
const USER_STORAGE_KEY_VALUE_SEPARATOR = ':';
const g_userMessageDivId = 'user-message-div';
const g_userMessageBtnId = 'user-message-btn';
var g_msgCount = 0;
function addUserMessage(msgs, noNotification) {
if (msgs?.length > 0) {
let div = document.getElementById(g_userMessageDivId);
if (div == null) {
function clearNotification() {
g_msgCount = 0;
let btn = document.getElementById(g_userMessageBtnId);
if (btn != null) {
btn.style.display = 'none';
}
}
let div_row = document.createElement('div');
div_row.className = 'row';
document.querySelector('div.row.fyg_lh60.fyg_tr').parentNode.appendChild(div_row);
let div_pan = document.createElement('div');
div_pan.className = 'panel panel-info';
div_row.appendChild(div_pan);
let div_head = document.createElement('div');
div_head.className = 'panel-heading';
div_head.innerText = '页面消息';
div_pan.appendChild(div_head);
let div_op = document.createElement('div');
div_op.style.float = 'right';
div_head.appendChild(div_op);
let link_mark = document.createElement('a');
link_mark.style.marginRight = '20px';
link_mark.innerText = '〇 已读';
link_mark.href = '###';
link_mark.onclick = (() => {
clearNotification();
let m = document.getElementById(g_userMessageDivId).children;
for (let e of m) {
let name = e.firstElementChild;
if (name.getAttribute('item-readed') != 'true') {
name.setAttribute('item-readed', 'true');
name.style.color = 'grey';
name.innerText = name.innerText.substring(1);
}
}
});
div_op.appendChild(link_mark);
let link_clear = document.createElement('a');
link_clear.style.marginRight = '20px';
link_clear.innerText = '〇 清空';
link_clear.href = '###';
link_clear.onclick = (() => {
clearNotification();
document.getElementById(g_userMessageDivId).innerHTML = '';
});
div_op.appendChild(link_clear);
let link_top = document.createElement('a');
link_top.innerText = '〇 回到页首 ▲';
link_top.href = '###';
link_top.onclick = (() => { document.body.scrollIntoView(true); });
div_op.appendChild(link_top);
div = document.createElement('div');
div.className = 'panel-body';
div.id = g_userMessageDivId;
div_pan.appendChild(div);
}
if (!noNotification) {
let btn = document.getElementById(g_userMessageBtnId);
if (btn == null) {
let navBar = document.querySelector(g_navigatorSelector);
btn = navBar.firstElementChild.cloneNode(true);
btn.id = g_userMessageBtnId;
btn.className += ' btn-danger';
btn.setAttribute('onclick', `window.location.href='#${g_userMessageDivId}'`);
navBar.appendChild(btn);
}
btn.innerText = `查看消息(${g_msgCount += msgs.length})`;
btn.style.display = 'inline-block';
}
let timeStamp = getTimeStamp();
timeStamp = timeStamp.date + ' ' + timeStamp.time;
let alt = (div.firstElementChild?.className?.length > 0);
msgs.forEach((msg) => {
let div_info = document.createElement('div');
div_info.className = (alt = !alt ? 'alt' : '');
div_info.style.backgroundColor = (alt ? '#f0f0f0' : '');
div_info.style.padding = '5px';
div_info.innerHTML =
`★【${timeStamp}】${msg[0]}:` +
`
${msg[1]}
`;
div.insertBefore(div_info, div.firstElementChild);
});
}
}
function addUserMessageSingle(title, msg, noNotification) {
addUserMessage([[title, msg]], noNotification)
}
function getTimeStamp(date, dateSeparator, timeSeparator) {
date ??= new Date();
dateSeparator ??= '-';
timeSeparator ??= ':';
return {
date : `${('000' + date.getFullYear()).slice(-4)}${dateSeparator}${('0' + (date.getMonth() + 1))
.slice(-2)}${dateSeparator}${('0' + date.getDate()).slice(-2)}`,
time : `${('0' + date.getHours()).slice(-2)}${timeSeparator}${('0' + date.getMinutes())
.slice(-2)}${timeSeparator}${('0' + date.getSeconds()).slice(-2)}`
};
}
function loadUserConfigData() {
return JSON.parse(localStorage.getItem(g_kfUser));
}
function saveUserConfigData(json) {
localStorage.setItem(g_kfUser, JSON.stringify(json));
}
// generic configuration items represented using checkboxes
function setupConfigCheckbox(checkbox, configKey, fnPostProcess, fnParams) {
checkbox.checked = (localStorage.getItem(configKey) == 'true');
checkbox.onchange = ((e) => {
localStorage.setItem(configKey, e.currentTarget.checked);
if (fnPostProcess != null) {
fnPostProcess(e.currentTarget.checked, fnParams);
}
});
return checkbox.checked;
}
// HTTP requests
const GuGuZhenRequest = {
read : { method : 'POST' , url : '/fyg_read.php' },
update : { method : 'POST' , url : '/fyg_click.php' },
user : { method : 'GET' , url : g_guguzhenHome },
beach : { method : 'GET' , url : g_guguzhenBeach },
pk : { method : 'GET' , url : g_guguzhenPK },
equip : { method : 'GET' , url : g_guguzhenEquip },
wish : { method : 'GET' , url : g_guguzhenWish },
gem : { method : 'GET' , url : g_guguzhenGem },
shop : { method : 'GET' , url : g_guguzhenShop }
};
const MoMoZhenRequest = GuGuZhenRequest;
const g_postHeader = { 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8' /*, 'Cookie' : document.cookie*/ };
const g_networkTimeoutMS = 120 * 1000;
var g_httpRequests = [];
function httpRequestBegin(request, queryString, fnLoad, fnError, fnTimeout) {
let requestObj;
const g_readUrl = window.location.origin + '/fyg_read.php'
const g_postUrl = window.location.origin + '/fyg_click.php'
requestObj = new XMLHttpRequest();
requestObj.onload = requestObj.onerror = requestObj.ontimeout = httpRequestEventHandler;
requestObj.open(request.method, window.location.origin + request.url);
for (let name in g_postHeader) {
requestObj.setRequestHeader(name, g_postHeader[name]);
}
requestObj.send(queryString);
function httpRequestEventHandler(e) {
switch (e.type) {
case 'load':
if (fnLoad != null) {
fnLoad(e.currentTarget);
}
break;
case 'error':
if (fnError != null) {
fnError(e.currentTarget);
}
break;
case 'timeout':
if (fnTimeout != null) {
fnTimeout(e.currentTarget);
}
break;
}
}
g_httpRequests.push(requestObj);
return requestObj;
}
function httpRequestAbortAll() {
while (g_httpRequests.length > 0) {
g_httpRequests.pop().abort();
}
g_httpRequests = [];
}
function httpRequestClearAll() {
g_httpRequests = [];
}
// request data
const g_httpRequestMap = new Map();
function getRequestInfoAsync(name, location) {
return new Promise((resolve) => {
let r = g_httpRequestMap.get(name);
if (r != null || !(name?.length > 0) || location == null) {
resolve(r);
}
else {
beginGetRequestMap(
location,
async () => {
resolve(await getRequestInfoAsync(name, null));
});
}
});
}
function beginGetRequestMap(location, fnPostProcess, fnParams) {
function searchScript(text) {
let regex = //gms;
let script;
while ((script = regex.exec(text))?.length > 0) {
searchFunction(script[0]);
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
function searchFunction(text) {
let regex = /^\s*function\s+(.+?)\s*\(.+?\{(.+?)((^\s*function)|(<\/script>))/gms;
let func;
while ((func = regex.exec(text))?.length == 6 && func[1]?.length > 0 && func[2]?.length > 0) {
let request = searchRequest(func[2]);
if (request != null) {
g_httpRequestMap.set(func[1], request);
}
if (func[3] != '<\/script>') {
regex.lastIndex -= func[3].length;
}
else {
break;
}
}
}
function searchRequest(text) {
let method = text.match(/^\s*type\s*:\s*"(.+)"\s*,\s*$/m);
let url = text.match(/^\s*url\s*:\s*"(.+)"\s*,\s*$/m);
let data = text.match(/^\s*data\s*:\s*"(.+),\s*$/m);
if (method?.length > 1 && url?.length > 1 && data?.length > 1) {
return {
request : {
method : method[1],
url : (url[1].startsWith('/') ? '' : '/') + url[1]
},
data : data[1].endsWith('"') ? data[1].slice(0, -1) : data[1]
};
}
return null;
}
if (location == null) {
searchScript(document.documentElement.innerHTML);
}
else {
httpRequestBegin(location, '', (response) => { searchScript(response.responseText); });
}
}
// beginGetRequestMap(null, () => { console.log(g_httpRequestMap); });
beginGetRequestMap();
// read objects from bag and store with title filter
function beginReadObjects(bag, store, fnPostProcess, fnParams) {
if (bag != null || store != null) {
httpRequestBegin(
GuGuZhenRequest.read,
'f=7',
(response) => {
let div = document.createElement('div');
div.innerHTML = response.responseText;
if (bag != null) {
div.querySelectorAll('div.alert-danger > button.btn.fyg_mp3')?.forEach((e) => { bag.push(e); });
}
if (store != null) {
div.querySelectorAll('div.alert-success > button.btn.fyg_mp3')?.forEach((e) => { store.push(e); });
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
});
}
else if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
function beginReadObjectIds(bagIds, storeIds, key, ignoreEmptyCell, fnPostProcess, fnParams) {
function parseObjectIds() {
if (bagIds != null) {
objectIdParseNodes(bag, bagIds, key, ignoreEmptyCell);
}
if (storeIds != null) {
objectIdParseNodes(store, storeIds, key, ignoreEmptyCell);
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
let bag = (bagIds != null ? [] : null);
let store = (storeIds != null ? [] : null);
if (bag != null || store != null) {
beginReadObjects(bag, store, parseObjectIds, null);
}
else if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
function objectIdParseNodes(nodes, ids, key, ignoreEmptyCell) {
for (let node of nodes) {
if (node.className?.indexOf('fyg_mp3') >= 0) {
let click = node.getAttribute('onclick');
let id = click?.match(/\d+/g);
if (id?.length > 0) {
id = id[click?.match(/omenu/)?.length > 0 ? id.length - 1 : 0];
if (id != null) {
if (objectMatchTitle(node, key)) {
ids.push(parseInt(id));
continue;
}
}
}
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 ConcurrentRequestCount = { min : 1 , max : 8 , default : 4 };
const ObjectMovePath = { bag2store : 0 , store2bag : 1 , store2beach : 2 , beach2store : 3 };
const ObjectMoveRequestLocation = [
{ location : GuGuZhenRequest.equip , name : 'puti' },
{ location : GuGuZhenRequest.equip , name : 'puto' },
{ location : GuGuZhenRequest.beach , name : 'stdel' },
{ location : GuGuZhenRequest.beach , name : 'stpick' }
];
const ObjectMoveRequest = [ null, null, null, null ];
var g_maxConcurrentRequests = ConcurrentRequestCount.default;
var g_objectMoveRequestsCount = 0;
var g_objectMoveTargetSiteFull = false;
async function beginMoveObjects(objects, path, fnPostProcess, fnParams) {
if (!g_objectMoveTargetSiteFull && objects?.length > 0) {
ObjectMoveRequest[path] ??= await getRequestInfoAsync(ObjectMoveRequestLocation[path].name,
ObjectMoveRequestLocation[path].location);
if (ObjectMoveRequest[path] == null) {
console.log('missing function:', ObjectMoveRequestLocation[path].name);
return;
}
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) {
httpRequestBegin(
ObjectMoveRequest[path].request,
ObjectMoveRequest[path].data.replace('"+id+"', ids.shift()),
(response) => {
if (path != ObjectMovePath.store2beach && response.responseText != 'ok') {
g_objectMoveTargetSiteFull = true;
console.log(response.responseText);
}
if (--g_objectMoveRequestsCount == 0) {
beginMoveObjects(objects, path, fnPostProcess, fnParams);
}
});
}
return;
}
}
g_objectMoveTargetSiteFull = false;
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
const g_beach_pirl_verify_data = '85797';
const g_store_pirl_verify_data = '124';
const g_objectPirlRequest = g_httpRequestMap.get('pirl');
var g_objectPirlRequestsCount = 0;
function beginPirlObjects(storePirl, objects, fnPostProcess, fnParams) {
if (objects?.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) {
httpRequestBegin(
g_objectPirlRequest.request,
g_objectPirlRequest.data.replace('"+id+"', ids.shift()).replace(
'"+pirlyz+"', storePirl ? g_store_pirl_verify_data : g_beach_pirl_verify_data),
(response) => {
if (!/\d+/.test(response.responseText.trim()) && response.responseText.indexOf('果核') < 0) {
console.log(response.responseText);
}
if (--g_objectPirlRequestsCount == 0) {
beginPirlObjects(storePirl, objects, fnPostProcess, fnParams);
}
});
}
return;
}
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
// roleInfo = [ roleId, roleName, [ equips ] ]
function beginReadRoleInfo(roleInfo, fnPostProcess, fnParams) {
function parseCarding(carding) {
if (roleInfo != null) {
let role = g_roleMap.get(carding.querySelector('div.text-info.fyg_f24.fyg_lh60')?.children[0]?.innerText);
if (role != null) {
roleInfo.push(role.id);
roleInfo.push(role.name);
roleInfo.push(carding.querySelectorAll('div.row > div.fyg_tc > button.btn.fyg_mp3'));
}
}
}
let div = document.getElementById('carding');
if (div == null) {
httpRequestBegin(
GuGuZhenRequest.read,
'f=9',
(response) => {
div = document.createElement('div');
div.innerHTML = response.responseText;
parseCarding(div);
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
});
return;
}
parseCarding(div);
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
// haloInfo = [ haloPoints, haloSlots, [ haloItem1, haloItem2, ... ] ]
function beginReadHaloInfo(haloInfo, fnPostProcess, fnParams) {
httpRequestBegin(
GuGuZhenRequest.read,
'f=5',
(response) => {
if (haloInfo != null) {
let haloPS = response.responseText.match(/.+?(\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);
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
});
}
function beginReadWishpool(points, misc, fnPostProcess, fnParams) {
httpRequestBegin(
GuGuZhenRequest.read,
'f=19',
(response) => {
let a = response.responseText.split('#');
if (misc != null) {
misc[0] = a[0];
misc[1] = a[1];
}
if (points != null) {
for (let i = a.length - 1; i >= 2; i--) {
points[i - 2] = a[i];
}
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
});
}
// userInfo = [ kfUser, grade, level, seashell, bvip, svip ]
function beginReadUserInfo(userInfo, fnPostProcess, fnParams) {
httpRequestBegin(
GuGuZhenRequest.user,
'',
(response) => {
if (userInfo != null) {
userInfo.push(g_kfUser);
userInfo.push(response.responseText.match(/
\s*段位\s*<.+?>\s*(.+?)\s*<.+?<\/p>/)?.[1] ?? '');
userInfo.push(response.responseText.match(/\s*等级\s*<.+?>\s*(\d+).*?<.+?<\/p>/)?.[1] ?? '');
userInfo.push(response.responseText.match(/\s*贝壳\s*<.+?>\s*(\d+).*?<.+?<\/p>/)?.[1] ?? '');
userInfo.push(response.responseText.match(/\s*BVIP\s*<.+?>\s*(.+?)\s*<.+?<\/p>/)?.[1] ?? '');
userInfo.push(response.responseText.match(/\s*SVIP\s*<.+?>\s*(.+?)\s*<.+?<\/p>/)?.[1] ?? '');
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// amulet management
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const AMULET_STORAGE_GROUP_SEPARATOR = '|';
const AMULET_STORAGE_GROUPNAME_SEPARATOR = '=';
const AMULET_STORAGE_AMULET_SEPARATOR = ',';
const AMULET_STORAGE_CODEC_SEPARATOR = '-';
const AMULET_STORAGE_ITEM_SEPARATOR = ' ';
const AMULET_STORAGE_ITEM_NV_SEPARATOR = '+';
// deprecated
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;
// deprecated
const g_amuletDefaultLevelIds = {
start : 0,
end : 2,
稀有 : 0,
史诗 : 1,
传奇 : 2
};
const g_amuletDefaultTypeIds = {
start : 2,
end : 6,
星铜苹果 : 0,
蓝银葡萄 : 1,
紫晶樱桃 : 2
};
const g_amuletDefaultLevelNames = [ '稀有', '史诗', '传奇' ];
const g_amuletDefaultTypeNames = [ '星铜苹果', '蓝银葡萄', '紫晶樱桃' ];
const g_amuletBuffs = [
{ index : -1 , name : '力量' , type : 0 , maxValue : 80 , unit : '点' , shortMark : 'STR' },
{ index : -1 , name : '敏捷' , type : 0 , maxValue : 80 , unit : '点' , shortMark : 'AGI' },
{ index : -1 , name : '智力' , type : 0 , maxValue : 80 , unit : '点' , shortMark : 'INT' },
{ index : -1 , name : '体魄' , type : 0 , maxValue : 80 , unit : '点' , shortMark : 'VIT' },
{ index : -1 , name : '精神' , type : 0 , maxValue : 80 , unit : '点' , shortMark : 'SPR' },
{ index : -1 , name : '意志' , type : 0 , maxValue : 80 , unit : '点' , shortMark : 'MND' },
{ index : -1 , name : '物理攻击' , type : 1 , maxValue : 10 , unit : '%' , shortMark : 'PATK' },
{ index : -1 , name : '魔法攻击' , type : 1 , maxValue : 10 , unit : '%' , shortMark : 'MATK' },
{ index : -1 , name : '速度' , type : 1 , maxValue : 10 , unit : '%' , shortMark : 'SPD' },
{ index : -1 , name : '生命护盾回复效果' , type : 1 , maxValue : 10 , unit : '%' , shortMark : 'REC' },
{ index : -1 , name : '最大生命值' , type : 1 , maxValue : 10 , unit : '%' , shortMark : 'HP' },
{ index : -1 , name : '最大护盾值' , type : 1 , maxValue : 10 , unit : '%' , shortMark : 'SLD' },
{ index : -1 , name : '固定生命偷取' , type : 2 , maxValue : 10 , unit : '%' , shortMark : 'LCH' },
{ index : -1 , name : '固定反伤' , type : 2 , maxValue : 10 , unit : '%' , shortMark : 'RFL' },
{ index : -1 , name : '固定暴击几率' , type : 2 , maxValue : 10 , unit : '%' , shortMark : 'CRT' },
{ index : -1 , name : '固定技能几率' , type : 2 , maxValue : 10 , unit : '%' , shortMark : 'SKL' },
{ index : -1 , name : '物理防御效果' , type : 2 , maxValue : 10 , unit : '%' , shortMark : 'PDEF' },
{ index : -1 , name : '魔法防御效果' , type : 2 , maxValue : 10 , unit : '%' , shortMark : 'MDEF' },
{ index : -1 , name : '全属性' , type : 2 , maxValue : 10 , unit : '点' , shortMark : 'AAA' },
{ index : -1 , name : '暴击抵抗' , type : 2 , maxValue : 5 , unit : '%' , shortMark : 'CRTR' },
{ index : -1 , name : '技能抵抗' , type : 2 , maxValue : 5 , unit : '%' , shortMark : 'SKLR' }
];
const g_amuletBuffMap = new Map();
g_amuletBuffs.forEach((item, index) => {
item.index = index;
g_amuletBuffMap.set(item.index, item);
g_amuletBuffMap.set(item.index.toString(), item);
g_amuletBuffMap.set(item.name, item);
g_amuletBuffMap.set(item.shortMark, item);
});
var g_amuletLevelIds = g_amuletDefaultLevelIds;
var g_amuletTypeIds = g_amuletDefaultTypeIds;
var g_amuletLevelNames = g_amuletDefaultLevelNames;
var g_amuletTypeNames = g_amuletDefaultTypeNames;
function amuletLoadTheme(theme) {
if (theme.dessertlevel?.length > 0 && theme.dessertname?.length > 0) {
g_amuletLevelNames = theme.dessertlevel;
g_amuletTypeNames = theme.dessertname;
g_amuletLevelIds = {
start : 0,
end : theme.dessertlevel[0].length
};
g_amuletTypeIds = {
start : theme.dessertlevel[0].length,
end : theme.dessertlevel[0].length + theme.dessertname[0].length
};
for (let i = g_amuletLevelNames.length - 1; i >= 0; i--) {
g_amuletLevelIds[g_amuletLevelNames[i].slice(0, g_amuletLevelIds.end - g_amuletLevelIds.start)] = i;
}
for (let i = g_amuletTypeNames.length - 1; i >= 0; i--) {
g_amuletTypeIds[g_amuletTypeNames[i].slice(0, g_amuletTypeIds.end - g_amuletTypeIds.start)] = i;
}
}
}
function Amulet() {
this.isAmulet = true;
this.id = -1;
this.type = -1;
this.level = 0;
this.enhancement = 0;
this.buffs = [];
this.buffCode = null;
this.text = null;
this.reset = (() => {
this.id = -1;
this.type = -1;
this.level = 0;
this.enhancement = 0;
this.buffs = [];
this.buffCode = null;
this.text = null;
});
this.isValid = (() => {
return (this.type >= 0 && this.type < g_amuletDefaultTypeNames.length);
});
this.addItem = ((item, buff) => {
if (this.isValid()) {
let meta = g_amuletBuffMap.get(item);
if (meta?.type == this.type && (buff = parseInt(buff)) > 0) {
this.buffs[meta.index] = (this.buffs[meta.index] ?? 0) + buff;
this.buffCode = null;
return true;
}
else {
this.reset();
}
}
return false;
});
this.fromCode = ((code) => {
this.reset();
let e = code?.split(AMULET_STORAGE_CODEC_SEPARATOR);
if (e?.length == 4) {
this.type = parseInt(e[0]);
this.level = parseInt(e[1]);
this.enhancement = parseInt(e[2]);
e[3].split(AMULET_STORAGE_ITEM_SEPARATOR).forEach((item) => {
let nv = item.split(AMULET_STORAGE_ITEM_NV_SEPARATOR);
this.addItem(nv[0], nv[1]);
});
this.getCode();
}
return (this.isValid() ? this : null);
});
this.fromBuffText = ((text) => {
this.reset();
let nb = text?.split(' = ');
if (nb?.length == 2) {
this.type = (g_amuletTypeIds[nb[0].slice(g_amuletTypeIds.start, g_amuletTypeIds.end)] ??
g_amuletDefaultTypeIds[nb[0].slice(g_amuletDefaultTypeIds.start, g_amuletDefaultTypeIds.end)]);
this.level = (g_amuletLevelIds[nb[0].slice(g_amuletLevelIds.start, g_amuletLevelIds.end)] ??
g_amuletDefaultLevelIds[nb[0].slice(g_amuletDefaultLevelIds.start, g_amuletDefaultLevelIds.end)]);
this.enhancement = parseInt(nb[0].match(/\d+/)[0]);
this.buffCode = 0;
nb[1].replaceAll(/(\+)|( 点)|( %)/g, '').split(',').forEach((buff) => {
let nv = buff.trim().split(' ');
this.addItem(nv[0], nv[1]);
});
if (this.isValid()) {
this.text = nb[1];
this.getCode();
return this;
}
}
this.reset();
return null;
});
this.fromNode = ((node) => {
if (node?.nodeType == Node.ELEMENT_NODE) {
if (this.fromBuffText(node.getAttribute('amulate-string')) != null &&
!isNaN(this.id = parseInt(node.getAttribute('onclick').match(/\d+/)?.[0]))) {
return this;
}
else if (node.className?.indexOf('fyg_mp3') >= 0) {
this.reset();
let typeName = (node.getAttribute('data-original-title') ?? node.getAttribute('title'));
if (!/Lv.+?>\d+ 0 && content?.length > 0 &&
!isNaN(this.type = (g_amuletTypeIds[typeName.slice(g_amuletTypeIds.start, g_amuletTypeIds.end)] ??
g_amuletDefaultTypeIds[typeName.slice(g_amuletDefaultTypeIds.start,
g_amuletDefaultTypeIds.end)])) &&
!isNaN(this.level = (g_amuletLevelIds[typeName.slice(g_amuletLevelIds.start, g_amuletLevelIds.end)] ??
g_amuletDefaultLevelIds[typeName.slice(g_amuletDefaultLevelIds.start,
g_amuletDefaultLevelIds.end)])) &&
!isNaN(this.id = parseInt(id.match(/\d+/)?.[0])) &&
!isNaN(this.enhancement = parseInt(node.innerText))) {
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) {
if (!this.addItem(attr[1], attr[2])) {
break;
}
this.text += `${this.text.length > 0 ? ', ' : ''}${attr[1]} +${attr[2]} ${buffMeta.unit}`;
}
}
if (this.isValid()) {
node.setAttribute('amulet-string', this.formatBuffText());
this.getCode();
return this;
}
}
}
}
}
this.reset();
return null;
});
this.fromAmulet = ((amulet) => {
this.reset();
if (amulet?.isValid()) {
this.id = amulet.id;
this.type = amulet.type;
this.level = amulet.level;
this.enhancement = amulet.enhancement;
this.buffs = amulet.buffs.slice();
this.buffCode = amulet.buffCode;
this.text = amulet.text;
}
return (this.isValid() ? this : null);
});
this.getCode = (() => {
if (this.isValid()) {
if (!(this.buffCode?.length > 0)) {
let bc = [];
this.buffs.forEach((e, i) => {
bc.push(i + AMULET_STORAGE_ITEM_NV_SEPARATOR + e);
});
this.buffCode = bc.join(AMULET_STORAGE_ITEM_SEPARATOR);
}
return (this.type + AMULET_STORAGE_CODEC_SEPARATOR +
this.level + AMULET_STORAGE_CODEC_SEPARATOR +
this.enhancement + AMULET_STORAGE_CODEC_SEPARATOR +
this.buffCode);
}
return null;
});
this.getBuff = (() => {
return this.buffs;
});
this.getTotalPoints = (() => {
let points = 0;
this.buffs?.forEach((e) => {
points += e;
});
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;
}
let bi = [];
this.buffs.forEach((e, i) => {
let meta = g_amuletBuffMap.get(i);
bi.push(`${meta.name} +${e} ${meta.unit}`);
});
this.text = bi.join(', ');
}
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) {
this.buffs.forEach((e, i) => {
let meta = g_amuletBuffMap.get(i);
text = text.replaceAll(meta.name, meta.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 ? -1 : (other.buffCode < this.buffCode ? 1 : 0));
});
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.isAmuletGroup = true;
this.name = null;
this.items = [];
this.buffSummary = [];
this.isValid = (() => {
return (this.items.length > 0 && amuletIsValidGroupName(this.name));
});
this.count = (() => {
return this.items.length;
});
this.clear = (() => {
this.items = [];
this.buffSummary = [];
});
this.add = ((amulet) => {
if (amulet?.isValid()) {
amulet.buffs.forEach((e, i) => {
this.buffSummary[i] = (this.buffSummary[i] ?? 0) + e;
});
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) {
amulet.buffs.forEach((e, i) => {
this.buffSummary[i] -= e;
if (this.buffSummary[i] <= 0) {
delete this.buffSummary[i];
}
});
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];
amulet.buffs.forEach((e, i) => {
this.buffSummary[i] -= e;
if (this.buffSummary[i] <= 0) {
delete this.buffSummary[i];
}
});
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) {
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(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.index];
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.index];
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.index] > 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.isAmuletGroupCollection = true;
this.items = {};
this.itemCount = 0;
this.count = (() => {
return this.itemCount;
});
this.contains = ((name) => {
return (this.items[name] != null);
});
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);
}
// deprecated
// 2023-05-04: new amulet items added
function AmuletOld() {
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 < g_amuletDefaultTypeNames.length && 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(g_amuletTypeIds.start, g_amuletTypeIds.end)] ??
g_amuletDefaultTypeIds[nb[0].slice(g_amuletDefaultTypeIds.start, g_amuletDefaultTypeIds.end)]);
this.level = (g_amuletLevelIds[nb[0].slice(g_amuletLevelIds.start, g_amuletLevelIds.end)] ??
g_amuletDefaultLevelIds[nb[0].slice(g_amuletDefaultLevelIds.start, g_amuletDefaultLevelIds.end)]);
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?.nodeType == Node.ELEMENT_NODE) {
if (this.fromBuffText(node.getAttribute('amulate-string'))?.isValid() &&
!isNaN(this.id = parseInt(node.getAttribute('onclick').match(/\d+/)?.[0]))) {
return this;
}
else if (node.className?.indexOf('fyg_mp3') >= 0) {
let typeName = (node.getAttribute('data-original-title') ?? node.getAttribute('title'));
if (!/Lv.+?>\d+ 0 && content?.length > 0 &&
!isNaN(this.type = (g_amuletTypeIds[typeName.slice(g_amuletTypeIds.start, g_amuletTypeIds.end)] ??
g_amuletDefaultTypeIds[typeName.slice(g_amuletDefaultTypeIds.start,
g_amuletDefaultTypeIds.end)])) &&
!isNaN(this.level = (g_amuletLevelIds[typeName.slice(g_amuletLevelIds.start, g_amuletLevelIds.end)] ??
g_amuletDefaultLevelIds[typeName.slice(g_amuletDefaultLevelIds.start,
g_amuletDefaultLevelIds.end)])) &&
!isNaN(this.id = parseInt(id.match(/\d+/)?.[0])) &&
!isNaN(this.enhancement = parseInt(node.innerText))) {
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()) {
node.setAttribute('amulate-string', this.formatBuffText());
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 AmuletGroupOld(persistenceString) {
this.name = null;
this.items = [];
this.buffSummary = [];
this.isValid = (() => {
return (this.items.length > 0 && amuletIsValidGroupName(this.name));
});
this.count = (() => {
return this.items.length;
});
this.clear = (() => {
this.items = [];
this.buffSummary = [];
});
this.add = ((amulet) => {
if (amulet?.isValid()) {
let buffs = amulet.getBuff();
for (let buff in buffs) {
this.buffSummary[buff] = (this.buffSummary[buff] ?? 0) + 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] = (this.buffSummary[buff] ?? 0) - buffs[buff];
if (this.buffSummary[buff] <= 0) {
delete this.buffSummary[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] = (this.buffSummary[buff] ?? 0) - buffs[buff];
if (this.buffSummary[buff] <= 0) {
delete this.buffSummary[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) {
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 AmuletOld()).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 AmuletGroupCollectionOld(persistenceString) {
this.items = {};
this.itemCount = 0;
this.count = (() => {
return this.itemCount;
});
this.contains = ((name) => {
return (this.items[name] != null);
});
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 AmuletGroupOld(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 amuletGroupStorageConvert() {
if (localStorage.getItem(g_amuletGroupCollectionStorageKey) == null) {
console.log('Begin convert amulet groups ...');
let groups = (new AmuletGroupCollectionOld(localStorage.getItem(g_amuletGroupsStorageKey))).toArray();
if (groups?.length > 0) {
console.log(groups.length + ' amulet groups loaded');
let ngs = new AmuletGroupCollection();
groups.forEach((g, i) => {
console.log('amulet group #' + i + ' - ' + g.name + ' : ' + g.count() + ' amulets included');
let ng = new AmuletGroup();
ng.name = g.name;
g.items.forEach((a, j) => {
let na = (new Amulet()).fromBuffText(a.formatBuffText());
if (ng.add(na) < 0) {
console.log('amulet group #' + i + ' - ' + ng.name + ' : add amulet #' + j + ' failed');
console.log(na);
}
});
console.log('amulet group #' + i + ' - ' + ng.name + ' : ' + ng.count() + ' amulets converted');
if (!ngs.add(ng)) {
console.log('amulet group #' + i + ' - ' + ng.name + ' : add group failed');
}
});
console.log(ngs.count() + ' amulet groups converted');
amuletSaveGroups(ngs);
}
}
}
amuletGroupStorageConvert();
// deprecated
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_amuletGroupCollectionStorageKey, groups.formatPersistenceString());
}
else {
localStorage.removeItem(g_amuletGroupCollectionStorageKey);
}
}
function amuletLoadGroups() {
return new AmuletGroupCollection(localStorage.getItem(g_amuletGroupCollectionStorageKey));
}
function amuletClearGroups() {
localStorage.removeItem(g_amuletGroupCollectionStorageKey);
}
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, fnPostProcess, fnParams) {
function parseAmulets() {
if (bagAmulets != null) {
amuletNodesToArray(bag, bagAmulets, key);
}
if (storeAmulets != null) {
amuletNodesToArray(store, storeAmulets, key);
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
let bag = (bagAmulets != null ? [] : null);
let store = (storeAmulets != null ? [] : null);
if (bag != null || store != null) {
beginReadObjects(bag, store, parseAmulets, null);
}
else if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
function beginLoadAmuletGroupsDiff(groupNames, fnProgress, fnPostProcess, fnParams) {
let bag, store, loading;
if (groupNames?.length > 0) {
loading = [];
groupNames.forEach((gn) => {
let g = amuletLoadGroup(gn);
if (g?.isValid()) {
loading = loading.concat(g.items);
}
});
if (loading.length > 0) {
if (fnProgress != null) {
fnProgress(4, 1, loading.length);
}
beginReadAmulets(bag = [], store = [], null, beginUnload);
return;
}
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
function beginUnload() {
let ids = [];
if (bag.length > 0) {
let indices = findNewObjects(bag, loading.sort((a, b) => a.compareMatch(b)), true, true,
(a, b) => a.compareMatch(b)).sort((a, b) => b - a);
while (indices?.length > 0) {
ids.push(bag[indices.pop()].id);
}
}
if (fnProgress != null) {
fnProgress(4, 2, ids.length);
}
beginMoveObjects(ids, ObjectMovePath.bag2store, beginLoad, ids.length);
}
function beginLoad(unloadedCount) {
if (loading.length > 0 && store.length > 0) {
if (unloadedCount == 0) {
let indices = amuletCreateGroupFromArray('_', loading)?.findIndices(store)?.sort((a, b) => b - a);
let ids = [];
while (indices?.length > 0) {
ids.push(store[indices.pop()].id);
}
if (fnProgress != null) {
fnProgress(4, 4, ids.length);
}
beginMoveObjects(ids, ObjectMovePath.store2bag, fnPostProcess, fnParams);
}
else {
if (fnProgress != null) {
fnProgress(4, 3, loading.length);
}
beginReadAmulets(null, store = [], null, beginLoad, 0);
}
}
else if (fnPostProcess != null) {
fnPostProcess(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, fnPostProcess, fnParams) {
if (amulets?.length > 0) {
let store = amuletNodesToArray(amulets);
beginMoveAmulets({ groupName : groupName, amulets : store, path : ObjectMovePath.store2bag,
proc : fnPostProcess, params : fnParams });
}
else {
beginReadAmulets(null, amulets = [], null, beginMoveAmulets,
{ groupName : groupName, amulets : amulets, path : ObjectMovePath.store2bag,
proc : fnPostProcess, params : fnParams });
}
}
function beginUnloadAmuletGroupFromBag(amulets, groupName, fnPostProcess, fnParams) {
if (amulets?.length > 0) {
let bag = amuletNodesToArray(amulets);
beginMoveAmulets({ groupName : groupName, amulets : bag, path : ObjectMovePath.bag2store,
proc : fnPostProcess, params : fnParams });
}
else {
beginReadAmulets(amulets, null, null, beginMoveAmulets,
{ groupName : groupName, amulets : amulets, path : ObjectMovePath.bag2store,
proc : fnPostProcess, params : fnParams });
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// property utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const g_equipmentDefaultLevelName = [ '普通', '幸运', '稀有', '史诗', '传奇' ];
const g_equipmentLevelStyleClass = [ 'primary', 'info', 'success', 'warning', 'danger' ];
const g_equipmentLevelPoints = [ 200, 321, 419, 516, 585 ];
const g_equipmentLevelBGColor = [ '#e0e8e8', '#c0e0ff', '#c0ffc0', '#ffffc0', '#ffd0d0' ];
function propertyInfoParseNode(node) {
function formatPropertyString(p) {
return `${p.metaIndex},${p.level},${p.amount}`;
}
function parsePropertyString(s) {
let a = s.split(',');
console.log(s);
return {
isProperty : true,
metaIndex : parseInt(a[0]),
level : parseInt(a[1]),
amount : parseInt(a[2])
};
}
if (node?.nodeType == Node.ELEMENT_NODE) {
let s = node.getAttribute('property-string');
if (s?.length > 0) {
return parsePropertyString(s);
}
else if (node.className?.split(' ').length >= 2 && node.className.indexOf('fyg_mp3') >= 0) {
let title = (node.getAttribute('data-original-title') ?? node.getAttribute('title'))?.split(' ');
let meta = g_propertyMap.get(title?.[0]?.trim());
if (meta != null) {
let text = node.getAttribute('data-content');
let lv = node.getAttribute('data-tip-class');
if (text?.length > 0 && lv?.length > 0) {
if (meta.discription == null) {
meta.discription = text;
}
let p = {
isProperty : true,
metaIndex : meta.index,
level : g_equipmentLevelStyleClass.indexOf(lv.substring(lv.indexOf('-') + 1)),
amount : parseInt(title?.[1]?.match(/\d+/)?.[0] ?? 0)
};
node.setAttribute('property-string', formatPropertyString(p));
return p;
}
}
}
}
return null;
}
function propertyNodesToInfoArray(nodes, array, key) {
array ??= [];
let e;
for (let i = nodes?.length - 1; i >= 0; i--) {
if (objectMatchTitle(nodes[i], key) && (e = propertyInfoParseNode(nodes[i])) != null) {
array.unshift(e);
}
}
return array;
}
function propertyInfoComparer(p1, p2) {
return p1.metaIndex - p2.metaIndex;
}
function beginReadProperties(properties, key, fnPostProcess, fnParams) {
if (properties != null) {
let store = [];
beginReadObjects(
null,
store,
() => {
propertyNodesToInfoArray(store, properties, key);
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
},
store);
}
else if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// equipment utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
var g_equipmentLevelName = g_equipmentDefaultLevelName;
function equipmentInfoParseNode(node, ignoreIllegalAttributes) {
if (node?.nodeType == Node.ELEMENT_NODE) {
let eq = node.getAttribute('equip-string')?.split(',');
if (eq?.length >= 8 && g_equipMap.has(eq[0])) {
return eq;
}
else if (node.className?.split(' ').length >= 2 && node.className.indexOf('fyg_mp3') >= 0) {
let attrTitle = node.hasAttribute('data-original-title') ? 'data-original-title' : 'title';
let title = node.getAttribute(attrTitle);
let eqInfo = title?.match(/Lv.+?>\s*(\d+)\s*<.+?\/i>\s*(\d+)\s*<.+?
(.+)/);
if (eqInfo?.length == 4) {
let name = eqInfo[3].trim();
let meta = (g_equipMap.get(name) ?? g_equipMap.get(name.substring(g_equipmentLevelName[0].length)));
if(meta==undefined){
let tempEqName,tempEqNameC;
let tempEqType=node.innerHTML;
if(tempEqType.indexOf('z21')>-1){tempEqType=1;tempEqName='NEWEQA';tempEqNameC='待更新的未知新武器'}
else if(tempEqType.indexOf('z22')>-1){tempEqType=2;tempEqName='NEWEQB';;tempEqNameC='待更新的未知新手饰'}
else if(tempEqType.indexOf('z23')>-1){tempEqType=3;tempEqName='NEWEQC';;tempEqNameC='待更新的未知新防具'}
else if(tempEqType.indexOf('z24')>-1){tempEqType=4;tempEqName='NEWEQD';;tempEqNameC='待更新的未知新耳饰'};
meta={
"index": tempEqType-1,
"name": tempEqNameC,
"type": tempEqType,
"attributes": [
{
"attribute": {
"index": 33,
"type": 0,
"name": "未知属性"
},
"factor": 0,
"additive": 42
},
{
"attribute": {
"index": 33,
"type": 0,
"name": "未知属性"
},
"factor": 0,
"additive": 42
},
{
"attribute": {
"index": 15,
"type": 0,
"name": "未知属性"
},
"factor": 0,
"additive": 42
},
{
"attribute": {
"index": 33,
"type": 0,
"name": "未知属性"
},
"factor": 0,
"additive": 42
}
],
"shortMark": tempEqName,
"alias": tempEqNameC
};
};
name = meta?.shortMark;
if (name?.length > 0) {
let attr = node.getAttribute('data-content')?.match(/>\s*\d+\s?%\s* 0) && lv?.length > 0 && lvEx?.length > 0) {
let e = [ name, lv, meta.type < 2 ? lvEx : '0', meta.type < 2 ? '0' : lvEx,
(attr?.[0]?.match(/\d+/)?.[0] ?? '0'),
(attr?.[1]?.match(/\d+/)?.[0] ?? '0'),
(attr?.[2]?.match(/\d+/)?.[0] ?? '0'),
(attr?.[3]?.match(/\d+/)?.[0] ?? '0'),
(/\[神秘属性\]/.test(node.getAttribute('data-content')) ? '1' : '0'),
(node.getAttribute('onclick')?.match(/\d+/)?.[0] ?? '-1') ];
node.setAttribute(attrTitle, title.replace(/(Lv.+?>\s*\d+\s*)(<)/,
`$1(${equipmentQuality(e)}%)$2`));
node.setAttribute('equip-string', e.join(','));
return e;
}
}
}
}
}
return null;
}
function equipmentQuality(e) {
let eq = (Array.isArray(e) ? e : equipmentInfoParseNode(e, true));
if (eq?.length >= 9) {
return parseInt(eq[4]) + parseInt(eq[5]) + parseInt(eq[6]) + parseInt(eq[7]) + (parseInt(eq[8]) * 100);
}
return -1;
}
function equipmentNodesToInfoArray(nodes, array, ignoreIllegalAttributes) {
array ??= [];
for (let i = nodes?.length - 1; i >= 0; i--) {
let e = equipmentInfoParseNode(nodes[i], ignoreIllegalAttributes);
if (e != null) {
array.unshift(e);
}
}
return array;
}
function equipmentInfoComparer(e1, e2) {
let delta = g_equipMap.get(e1[0]).index - g_equipMap.get(e2[0]).index;
for (let i = 1; i < 9 && delta == 0; delta = parseInt(e1[i]) - parseInt(e2[i++]));
return delta;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// object utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
function objectGetLevel(e) {
let eq = equipmentQuality(e);
if (eq >= 0) {
for (var i = g_equipmentLevelPoints.length - 1; i > 0 && eq < g_equipmentLevelPoints[i]; i--);
return i;
}
else if((eq = e.isProperty ? e : propertyInfoParseNode(e))?.isProperty){
return eq.level;
}
else if ((eq = (e.isAmulet ? e : (new Amulet()).fromNode(e)))?.isValid()) {
return (eq.level + 2)
}
try {
let theme = JSON.parse(sessionStorage.getItem('ThemePack') ?? '{}');
if (theme?.url != null) {
amuletLoadTheme(theme);
propertyLoadTheme(theme);
equipLoadTheme(theme);
}
}
catch (ex) {
console.log('THEME:');
console.log(ex);
}
return -1;
}
function objectNodeComparer(e1, e2) {
let eq1 = equipmentInfoParseNode(e1, true);
let eq2 = equipmentInfoParseNode(e2, true);
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 (/空/.test(node?.innerText) || !(node?.getAttribute('data-content')?.length > 0));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// bag & store utilities
//
////////////////////////////////////////////////////////////////////////////////////////////////////
function findAmuletIds(container, amulets, ids, maxCount) {
ids ??= [];
let cl = container?.length;
if (cl > 0 && amulets?.length > 0) {
maxCount ??= cl;
let ams = amuletNodesToArray(container);
for (let i = ams.length - 1; i >= 0 && amulets.length > 0 && ids.length < maxCount; 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;
if (cl > 0 && equips?.length > 0) {
maxCount ??= cl;
let eqs = equipmentNodesToInfoArray(container);
for (let i = eqs.length - 1; i >= 0 && equips.length > 0 && ids.length < maxCount; 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][9]));
break;
}
}
}
}
return ids;
}
function beginClearBag(bag, key, fnPostProcess, fnParams) {
function beginClearBagObjects(objects) {
beginMoveObjects(objects, ObjectMovePath.bag2store, fnPostProcess, fnParams);
}
let objects = [];
if (bag?.length > 0) {
objectIdParseNodes(bag, objects, key, true);
beginClearBagObjects(objects);
}
else {
beginReadObjectIds(objects, null, key, true, beginClearBagObjects, objects);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// generic popups
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const g_genericPopupContainerId = 'generic-popup-container';
const g_genericPopupClass = 'generic-popup';
const g_genericPopupId = g_genericPopupClass;
const g_genericPopupWindowClass = 'generic-popup-window';
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}
`;
var g_genericPopupContainer = null;
function genericPopupInitialize() {
if (g_genericPopupContainer == null && (g_genericPopupContainer = document.getElementById(g_genericPopupContainerId)) == null) {
g_genericPopupContainer = document.createElement('div');
g_genericPopupContainer.id = g_genericPopupContainerId;
document.body.appendChild(g_genericPopupContainer);
}
g_genericPopupContainer.innerHTML = g_genericPopupHTML;
}
function genericPopupReset(initialize) {
if (initialize) {
g_genericPopupContainer.innerHTML = g_genericPopupHTML;
}
else {
let fixedContent = g_genericPopupContainer.querySelector('#' + g_genericPopupFixedContentId);
fixedContent.style.display = 'none';
fixedContent.innerHTML = '';
g_genericPopupContainer.querySelector('#' + g_genericPopupTitleTextId).innerText = '';
g_genericPopupContainer.querySelector('#' + g_genericPopupContentId).innerHTML = '';
g_genericPopupContainer.querySelector('#' + g_genericPopupTitleButtonContainerId).innerHTML = '';
g_genericPopupContainer.querySelector('#' + g_genericPopupFootButtonContainerId).innerHTML = '';
}
}
function genericPopupSetContent(title, content) {
g_genericPopupContainer.querySelector('#' + g_genericPopupTitleTextId).innerText = title;
g_genericPopupContainer.querySelector('#' + g_genericPopupContentId).innerHTML = content;
}
function genericPopupSetFixedContent(content) {
let fixedContent = g_genericPopupContainer.querySelector('#' + g_genericPopupFixedContentId);
fixedContent.style.display = 'block';
fixedContent.innerHTML = content;
}
function genericPopupAddButton(text, width, clickProc, addToTitle) {
let btn = document.createElement('button');
btn.innerText = text;
btn.onclick = clickProc;
if (width != null && width > 0) {
width = width.toString();
btn.style.width = width + (width.endsWith('px') || width.endsWith('%') ? '' : 'px');
}
else {
btn.style.width = 'auto';
}
g_genericPopupContainer.querySelector('#' + (addToTitle
? g_genericPopupTitleButtonContainerId
: g_genericPopupFootButtonContainerId)).appendChild(btn);
return btn;
}
function genericPopupAddCloseButton(width, text, addToTitle) {
return genericPopupAddButton(text?.length > 0 ? text : '关闭', width, (() => { genericPopupClose(true); }), addToTitle);
}
function genericPopupSetContentSize(height, width, scrollable) {
height = (height?.toString() ?? '100%');
width = (width?.toString() ?? '100%');
g_genericPopupContainer.querySelector('#' + g_genericPopupContentContainerId).style.width
= width + (width.endsWith('px') || width.endsWith('%') ? '' : 'px');
let content = g_genericPopupContainer.querySelector('#' + g_genericPopupContentId);
content.style.height = height + (height.endsWith('px') || height.endsWith('%') ? '' : 'px');
content.style.overflow = (scrollable ? 'auto' : 'hidden');
}
function genericPopupShowModal(clickOutsideToClose) {
genericPopupClose(false);
let popup = g_genericPopupContainer.querySelector('#' + g_genericPopupId);
if (clickOutsideToClose) {
popup.onclick = ((event) => {
if (event.target == popup) {
genericPopupClose(true);
}
});
}
else {
popup.onclick = null;
}
popup.style.display = "flex";
}
function genericPopupClose(reset, initialize) {
genericPopupCloseProgressMessage();
let popup = g_genericPopupContainer.querySelector('#' + g_genericPopupId);
popup.style.display = "none";
if (reset) {
genericPopupReset(initialize);
}
httpRequestClearAll();
}
function genericPopupOnClickOutside(fnProcess, fnParams) {
let popup = g_genericPopupContainer.querySelector('#' + g_genericPopupId);
if (fnProcess != null) {
popup.onclick = ((event) => {
if (event.target == popup) {
fnProcess(fnParams);
}
});
}
else {
popup.onclick = null;
}
}
function genericPopupQuerySelector(selectString) {
return g_genericPopupContainer.querySelector(selectString);
}
function genericPopupQuerySelectorAll(selectString) {
return g_genericPopupContainer.querySelectorAll(selectString);
}
let g_genericPopupInformationTipsTimer = null;
function genericPopupShowInformationTips(msg, time) {
if (g_genericPopupInformationTipsTimer != null) {
clearTimeout(g_genericPopupInformationTipsTimer);
g_genericPopupInformationTipsTimer = null;
}
let msgContainer = g_genericPopupContainer.querySelector('#' + g_genericPopupInformationTipsId);
if (msgContainer != null) {
msgContainer.innerText = (msg?.length > 0 ? `[ ${msg} ]` : '');
if ((time = parseInt(time)) > 0) {
g_genericPopupInformationTipsTimer = setTimeout(() => {
g_genericPopupInformationTipsTimer = null;
msgContainer.innerText = '';
}, time);
}
}
}
function genericPopupShowProgressMessage(progressMessage) {
genericPopupClose(false);
g_genericPopupContainer.querySelector('#' + g_genericPopupProgressContentId).innerText
= (progressMessage?.length > 0 ? progressMessage : '请稍候...');
g_genericPopupContainer.querySelector('#' + g_genericPopupProgressId).style.display = "flex";
}
function genericPopupUpdateProgressMessage(progressMessage) {
g_genericPopupContainer.querySelector('#' + g_genericPopupProgressContentId).innerText
= (progressMessage?.length > 0 ? progressMessage : '请稍候...');
}
function genericPopupCloseProgressMessage() {
g_genericPopupContainer.querySelector('#' + g_genericPopupProgressId).style.display = "none";
}
//
// generic task-list based progress popup
//
const g_genericPopupTaskListId = 'generic-popup-task-list';
const g_genericPopupTaskItemId = 'generic-popup-task-item-';
const g_genericPopupTaskWaiting = '×';
const g_genericPopupTaskCompleted = '√';
const g_genericPopupTaskCompletedWithError = '!';
const g_genericPopupColorTaskIncompleted = '#c00000';
const g_genericPopupColorTaskCompleted = '#0000c0';
const g_genericPopupColorTaskCompletedWithError = 'red';
var g_genericPopupIncompletedTaskCount = 0;
function genericPopupTaskListPopupSetup(title, popupWidth, tasks, fnCancelRoutine, cancelButtonText, cancelButtonWidth) {
g_genericPopupIncompletedTaskCount = tasks.length;
genericPopupSetContent(title, ``);
let indicatorList = g_genericPopupContainer.querySelector('#' + g_genericPopupTaskListId);
for (let i = 0; i < g_genericPopupIncompletedTaskCount; i++) {
let li = document.createElement('li');
li.id = g_genericPopupTaskItemId + i;
li.style.color = g_genericPopupColorTaskIncompleted;
li.innerHTML = `${g_genericPopupTaskWaiting} ${tasks[i]} `;
indicatorList.appendChild(li);
}
if (fnCancelRoutine != null) {
genericPopupAddButton(cancelButtonText?.length > 0 ? cancelButtonText : '取消', cancelButtonWidth, fnCancelRoutine, false);
}
genericPopupSetContentSize(Math.min(g_genericPopupIncompletedTaskCount * 20 + 30, window.innerHeight - 400), popupWidth, true);
}
function genericPopupTaskSetState(index, state) {
let item = g_genericPopupContainer.querySelector('#' + g_genericPopupTaskItemId + index)?.lastChild;
if (item != null) {
item.innerText = (state ?? '');
}
}
function genericPopupTaskComplete(index, error) {
let li = g_genericPopupContainer.querySelector('#' + g_genericPopupTaskItemId + index);
if (li?.firstChild?.innerText == g_genericPopupTaskWaiting) {
li.firstChild.innerText = (error ? g_genericPopupTaskCompletedWithError : g_genericPopupTaskCompleted);
li.style.color = (error ? g_genericPopupColorTaskCompletedWithError : g_genericPopupColorTaskCompleted);
g_genericPopupIncompletedTaskCount--;
}
}
function genericPopupTaskCheckCompletion() {
return (g_genericPopupIncompletedTaskCount == 0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// switch solution
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const BINDING_SEPARATOR = ';';
const BINDING_NAME_SEPARATOR = '=';
const BINDING_ELEMENT_SEPARATOR = '|';
const g_switchSolutionRequests = {};
async function switchBindingSolution(bindingString, fnPostProcess, fnParams) {
if ((g_switchSolutionRequests.card ??= await getRequestInfoAsync('upcard', GuGuZhenRequest.equip)) != null) {
g_switchSolutionRequests.halo ??= await getRequestInfoAsync('halosave', GuGuZhenRequest.equip);
g_switchSolutionRequests.equip ??= await getRequestInfoAsync('puton', GuGuZhenRequest.equip);
}
if (g_switchSolutionRequests.card == null ||
g_switchSolutionRequests.halo == null ||
g_switchSolutionRequests.equip == null) {
console.log('missing function:', g_switchSolutionRequests);
alert('无法获取服务请求格式,可能的原因是咕咕镇版本不匹配或正在测试。');
window.location.reload();
return;
}
let binding = bindingString?.split(BINDING_NAME_SEPARATOR);
let roleId = g_roleMap.get(binding?.[0]?.trim())?.id;
let bindInfo = binding?.[1]?.split(BINDING_ELEMENT_SEPARATOR)
if (roleId == null || bindInfo?.length != 6) {
console.log('missins format:', bindingString);
alert('无效的绑定信息,无法执行切换。');
window.location.reload();
return;
}
let bindingEquipments = bindInfo.slice(0, 4);
let bindingHalos = bindInfo[4].split(',');
let amuletGroups = bindInfo[5].split(',');
function roleSetupCompletion() {
httpRequestClearAll();
genericPopupClose(true, true);
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
function checkForRoleSetupCompletion() {
if (genericPopupTaskCheckCompletion()) {
// delay for the final state can be seen
genericPopupTaskSetState(0);
genericPopupTaskSetState(1);
genericPopupTaskSetState(2);
genericPopupTaskSetState(3);
setTimeout(roleSetupCompletion, 200);
}
}
function amuletLoadCompletion() {
genericPopupTaskComplete(3);
checkForRoleSetupCompletion();
}
let switchMethod = g_configMap.get('solutionSwitchMethod')?.value;
function beginAmuletLoadGroups() {
if (amuletGroups?.length > 0) {
if (switchMethod == 0) {
genericPopupTaskSetState(3, `- 加载护符...(${amuletGroups?.length})`);
beginLoadAmuletGroupFromStore(null, amuletGroups.shift(), beginAmuletLoadGroups, null);
}
else {
genericPopupTaskSetState(3, `- 加载护符...`);
beginLoadAmuletGroupsDiff(amuletGroups, amuletLoadProgress, amuletLoadCompletion, null);
}
}
else {
amuletLoadCompletion();
}
function amuletLoadProgress(total, current, amuletCount) {
genericPopupTaskSetState(3, `- 加载护符...(${amuletCount} - ${Math.trunc(current * 100 / total)}%)`);
}
}
function beginLoadAmulets() {
genericPopupTaskSetState(2);
genericPopupTaskComplete(2, equipmentOperationError > 0);
if (amuletGroups?.length > 0) {
if (switchMethod == 0) {
genericPopupTaskSetState(3, '- 清理饰品...');
beginClearBag(null, null, beginAmuletLoadGroups, null);
}
else {
beginAmuletLoadGroups();
}
}
else {
amuletLoadCompletion();
}
}
let equipmentOperationError = 0;
let putonRequestsCount;
function putonEquipments(objects, fnPostProcess, fnParams) {
if (objects?.length > 0) {
let ids = [];
while (ids.length < g_maxConcurrentRequests && objects.length > 0) {
ids.push(objects.pop());
}
if ((putonRequestsCount = ids.length) > 0) {
while (ids.length > 0) {
httpRequestBegin(
g_switchSolutionRequests.equip.request,
g_switchSolutionRequests.equip.data.replace('"+id+"', ids.shift()),
(response) => {
if (response.responseText.indexOf('已装备') < 0) {
equipmentOperationError++;
console.log(response.responseText);
}
if (--putonRequestsCount == 0) {
putonEquipments(objects, fnPostProcess, fnParams);
}
});
}
return;
}
}
if (fnPostProcess != null) {
fnPostProcess(fnParams);
}
}
let currentEquipments = null;
function beginPutonEquipments() {
genericPopupTaskSetState(2, '- 检查装备...');
let equipsToPuton = [];
for (let i = 0; i < 4; i++) {
let equipInfo = bindingEquipments[i].split(',');
equipInfo.push(-1);
if (equipmentInfoComparer(equipInfo, currentEquipments[i]) != 0) {
equipsToPuton.push(equipInfo);
}
}
if (equipsToPuton.length == 0) {
beginLoadAmulets();
}
else {
let store = [];
beginReadObjects(null, store, scheduleEquipments, null);
function scheduleEquipments() {
let eqIds = findEquipmentIds(store, equipsToPuton);
if (equipsToPuton.length == 0) {
genericPopupTaskSetState(2, `- 穿戴装备...(${eqIds.length})`);
putonEquipments(eqIds, beginLoadAmulets, null);
}
else {
console.log(equipsToPuton);
alert('有装备不存在,请重新检查绑定!');
httpRequestAbortAll();
roleSetupCompletion();
}
}
}
}
function beginSetupHalo() {
if (bindingHalos?.length > 0) {
let halo = [];
bindingHalos.forEach((h) => {
let hid = g_haloMap.get(h.trim())?.id;
if (hid > 0) {
halo.push(hid);
}
});
if ((halo = halo.join(','))?.length > 0) {
genericPopupTaskSetState(1, '- 设置光环...');
httpRequestBegin(
g_switchSolutionRequests.halo.request,
g_switchSolutionRequests.halo.data.replace('"+savearr+"', halo),
(response) => {
genericPopupTaskSetState(1);
genericPopupTaskComplete(1, response.responseText != 'ok');
checkForRoleSetupCompletion();
});
return;
}
}
genericPopupTaskComplete(1);
checkForRoleSetupCompletion();
}
function beginRoleSetup() {
beginSetupHalo();
beginPutonEquipments();
}
function beginSwitch(roleInfo) {
function cancelSwitching() {
httpRequestAbortAll();
roleSetupCompletion();
}
if (!(roleInfo?.length > 0)) {
alert('获取当前角色信息失败,无法执行切换。');
window.location.reload();
return;
}
currentEquipments = equipmentNodesToInfoArray(roleInfo[2]);
genericPopupInitialize();
genericPopupTaskListPopupSetup('切换中...', 300, [ '卡片', '光环', '装备', '饰品' ], cancelSwitching);
genericPopupShowModal(false);
if (roleId == roleInfo[0]) {
genericPopupTaskComplete(0);
beginRoleSetup();
}
else {
genericPopupTaskSetState(0, '- 装备中...');
httpRequestBegin(
g_switchSolutionRequests.card.request,
g_switchSolutionRequests.card.data.replace('"+id+"', roleId),
(response) => {
genericPopupTaskSetState(0);
if (response.responseText == 'ok' || response.responseText == '你没有这张卡片或已经装备中') {
genericPopupTaskComplete(0);
beginRoleSetup();
}
else {
genericPopupTaskComplete(0, true);
alert('卡片装备失败!');
cancelSwitching();
}
});
}
}
let roleInfo = [];
beginReadRoleInfo(roleInfo, beginSwitch, roleInfo);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//
// constants
//
////////////////////////////////////////////////////////////////////////////////////////////////////
const g_roles = [
{ index : -1 , id : 3000 , name : '舞' , hasG : true , shortMark : 'WU' },
{ index : -1 , id : 3001 , name : '默' , hasG : false , shortMark : 'MO' },
{ index : -1 , id : 3002 , name : '琳' , hasG : false , shortMark : 'LIN' },
{ index : -1 , id : 3003 , name : '艾' , hasG : false , shortMark : 'AI' },
{ index : -1 , id : 3004 , name : '梦' , hasG : false , shortMark : 'MENG' },
{ index : -1 , id : 3005 , name : '薇' , hasG : false , shortMark : 'WEI' },
{ index : -1 , id : 3006 , name : '伊' , hasG : false , shortMark : 'YI' },
{ index : -1 , id : 3007 , name : '冥' , hasG : false , shortMark : 'MING' },
{ index : -1 , id : 3008 , name : '命' , hasG : false , shortMark : 'MIN' },
{ index : -1 , id : 3009 , name : '希' , hasG : true , shortMark : 'XI' },
{ index : -1 , id : 3010 , name : '霞' , hasG : true , shortMark : 'XIA' },
{ index : -1 , id : 3011 , name : '雅' , hasG : true , shortMark : 'YA' }
];
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_properties = [
{ index : -1 , id : 3001 , name : '体能刺激药水' },
{ index : -1 , id : 3002 , name : '锻造材料箱' },
{ index : -1 , id : 3003 , name : '灵魂药水' },
{ index : -1 , id : 3004 , name : '随机装备箱' },
{ index : -1 , id : 3005 , name : '宝石原石' },
{ index : -1 , id : 3301 , name : '蓝锻造石' },
{ index : -1 , id : 3302 , name : '绿锻造石' },
{ index : -1 , id : 3303 , name : '金锻造石' },
{ index : -1 , id : 3309 , name : '苹果核' },
{ index : -1 , id : 3310 , name : '光环天赋石' }
];
const g_propertyMap = new Map();
g_properties.forEach((item, index) => {
item.index = index;
item.alias = item.name;
g_propertyMap.set(item.id, item);
g_propertyMap.set(item.id.toString(), item);
g_propertyMap.set(item.name, item);
});
function propertyLoadTheme(theme) {
if (theme.itemsname?.length > 0) {
theme.itemsname.forEach((item, index) => {
if (!g_propertyMap.has(item) && index < g_properties.length) {
g_properties[index].alias = item;
g_propertyMap.set(item, g_properties[index]);
}
});
}
}
const g_gemWorks = [
{
index : -1,
name : '赶海',
nameRegex : { line : 1 , regex : /^\d+贝壳$/ },
progressRegex : { line : 1 , regex : /(\d+)/ },
completionProgress : 1000000,
unitRegex : { line : 4 , regex : /(\d+)/ },
unitSymbol : '贝壳'
},
{
index : -1,
name : '随机装备箱',
nameRegex : { line : 1 , regex : /^随机装备箱$/ },
progressRegex : { line : 0 , regex : /(\d+\.?\d*)%?/ },
completionProgress : 100,
unitRegex : { line : 4 , regex : /(\d+\.?\d*)%?/ },
unitSymbol : '%'
},
{
index : -1,
name : '灵魂药水',
nameRegex : { line : 1 , regex : /^灵魂药水$/ },
progressRegex : { line : 0 , regex : /(\d+\.?\d*)%?/ },
completionProgress : 100,
unitRegex : { line : 4 , regex : /(\d+\.?\d*)%?/ },
unitSymbol : '%'
},
{
index : -1,
name : '宝石原石',
nameRegex : { line : 1 , regex : /^宝石原石/ },
progressRegex : { line : 0 , regex : /(\d+\.?\d*)%?/ },
completionProgress : 100,
unitRegex : { line : 4 , regex : /(\d+\.?\d*)%?/ },
unitSymbol : '%'
},
{
index : -1,
name : '星沙',
nameRegex : { line : 1 , regex : /^\d+星沙\(\d+\.?\d*\)/ },
progressRegex : { line : 1 , regex : /\((\d+\.?\d*)\)/ },
completionProgress : 10,
precision : 0,
unitRegex : { line : 4 , regex : /(\d+\.?\d*)/ },
unitSymbol : ''
},
{
index : -1,
name : '幻影经验',
nameRegex : { line : 1 , regex : /^\d+幻影经验$/ },
progressRegex : { line : 1 , regex : /(\d+)/ },
completionProgress : 200,
precision : 0,
unitRegex : { line : 4 , regex : /(\d+\.?\d*)/ },
unitSymbol : ''
}
];
const g_gemMinWorktimeMinute = 8 * 60;
const g_gemPollPeriodMinute = { min : 1 , max : 8 * 60 , default : 60 };
const g_gemFailurePollPeriodSecond = 30;
const g_gemWorkMap = new Map();
g_gemWorks.forEach((item, index) => {
item.index = index;
item.alias = item.name;
g_gemWorkMap.set(item.name, item);
});
function readGemWorkCompletionCondition() {
let cond = g_configMap.get('gemWorkCompletionCondition');
let conds = cond.value.split(',');
let len = Math.min(conds.length, g_gemWorks.length);
let error = 0;
for (let i = len - 1; i >= 0; i--) {
if ((conds[i] = conds[i].trim()).length > 0) {
let comp = conds[i].match(new RegExp(`^${g_gemWorks[i].unitRegex.regex.source}$`))?.[1];
if (!isNaN(comp = Number.parseFloat(comp))) {
g_gemWorks[i].completionProgress = comp;
}
else {
error++;
}
}
}
return error;
}
const g_equipAttributes = [
{ index : 0 , type : 0 , name : '物理攻击' },
{ index : 1 , type : 0 , name : '魔法攻击' },
{ index : 2 , type : 0 , name : '攻击速度' },
{ index : 3 , type : 0 , name : '最大生命' },
{ index : 4 , type : 0 , name : '最大护盾' },
{ index : 5 , type : 1 , name : '附加物伤' },
{ index : 6 , type : 1 , name : '附加魔伤' },
{ index : 7 , type : 1 , name : '附加攻速' },
{ index : 8 , type : 1 , name : '附加生命' },
{ index : 9 , type : 1 , name : '附加护盾' },
{ index : 10 , type : 1 , name : '附加回血' },
{ index : 11 , type : 1 , name : '附加回盾' },
{ index : 12 , type : 0 , name : '护盾回复' },
{ index : 13 , type : 0 , name : '物理穿透' },
{ index : 14 , type : 0 , name : '魔法穿透' },
{ index : 15 , type : 0 , name : '暴击穿透' },
{ index : 16 , type : 1 , name : '附加物穿' },
{ index : 17 , type : 1 , name : '附加物防' },
{ index : 18 , type : 1 , name : '附加魔防' },
{ index : 19 , type : 1 , name : '物理减伤' },
{ index : 20 , type : 1 , name : '魔法减伤' },
{ index : 21 , type : 0 , name : '生命偷取' },
{ index : 22 , type : 0 , name : '伤害反弹' },
{ index : 23 , type : 1 , name : '附加魔穿' },
{ index : 24 , type : 1 , name : '技能概率' },
{ index : 25 , type : 1 , name : '暴击概率' },
{ index : 26 , type : 1 , name : '力量生命' },
{ index : 27 , type : 1 , name : '体魄生命' },
{ index : 28 , type : 1 , name : '意志生命' },
{ index : 29 , type : 1 , name : '体魄物减' },
{ index : 30 , type : 1 , name : '意志魔减' },
{ index : 31 , type : 1 , name : '敏捷绝伤' },
{ index : 32 , type : 1 , name : '敏捷生命' },
{ index : 33 , type : 0 , name : '未知属性' }
];
const g_equipments = [
{
index : -1,
name : '待更新的未知新武器',
type : 0,
attributes : [ { attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 } ],
shortMark : 'NEWEQA'
},
{
index : -1,
name : '待更新的未知新手饰',
type : 1,
attributes : [ { attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 } ],
shortMark : 'NEWEQB'
},
{
index : -1,
name : '待更新的未知新防具',
type : 2,
attributes : [ { attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 } ],
shortMark : 'NEWEQC'
},
{
index : -1,
name : '待更新的未知新耳饰',
type : 3,
attributes : [ { attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[33] , factor : 1 , additive : 0 } ],
shortMark : 'NEWEQD'
},
{
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 } ],
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 } ],
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 } ],
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 } ],
shortMark : 'SHIELD'
},
{
index : -1,
name : '饮血魔剑',
type : 0,
attributes : [ { attribute : g_equipAttributes[0] , factor : 1 / 5 , additive : 50 },
{ attribute : g_equipAttributes[13] , factor : 1 / 20 , additive : 10 },
{ attribute : g_equipAttributes[23] , factor : 2 , additive : 0 },
{ attribute : g_equipAttributes[21] , factor : 1 / 15, additive : 10 } ],
shortMark : 'SPEAR'
},
{
index : -1,
name : '彩金长剑',
type : 0,
attributes : [ { attribute : g_equipAttributes[0] , factor : 1 / 5 , additive : 10 },
{ attribute : g_equipAttributes[1] , factor : 1 / 5 , additive : 10 },
{ attribute : g_equipAttributes[2] , factor : 1 / 5 , additive : 20 },
{ attribute : g_equipAttributes[31] , factor : 1 / 20 , additive : 0 } ],
shortMark : 'COLORFUL'
},
{
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 } ],
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 } ],
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 } ],
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 } ],
shortMark : 'BRACELET'
},
{
index : -1,
name : '秃鹫手环',
type : 1,
attributes : [ { attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 1 },
{ attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 1 },
{ attribute : g_equipAttributes[21] , factor : 1 / 15 , additive : 1 },
{ attribute : g_equipAttributes[7] , factor : 2 , additive : 0 } ],
merge : [ [ 0, 1, 2 ], [ 3 ] ],
shortMark : 'VULTURE'
},
{
index : -1,
name : '海星戒指',
type : 1,
attributes : [ { attribute : g_equipAttributes[16] , factor : 1 / 2 , additive : 0 },
{ attribute : g_equipAttributes[23] , factor : 1 / 2 , additive : 0 },
{ attribute : g_equipAttributes[25] , factor : 4 / 5 , additive : 0 },
{ attribute : g_equipAttributes[24] , factor : 4 / 5 , additive : 0 } ],
shortMark : 'RING'
},
{
index : -1,
name : '噬魔戒指',
type : 1,
attributes : [ { attribute : g_equipAttributes[23] , factor : 1 / 2 , additive : 0 },
{ attribute : g_equipAttributes[24] , factor : 4 / 5 , additive : 0 },
{ attribute : g_equipAttributes[26] , factor : 20 / 25 , additive : 0,
calculate : (a, l, p) => Math.trunc(l * p / 100 * a.factor + p / 100 * a.additive) / 10 },
{ attribute : g_equipAttributes[3] , factor : 70 / 100 , additive : 0,
calculate : (a, l, p) => Math.trunc(l * p / 100 * a.factor + p / 100 * a.additive) / 10 } ],
shortMark : 'DEVOUR'
},
{
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 } ],
shortMark : 'GLOVES'
},
{
index : -1,
name : '旅法师的灵光袍',
type : 2,
attributes : [ { attribute : g_equipAttributes[8] , factor : 10 , additive : 0 },
{ attribute : g_equipAttributes[11] , factor : 60 , additive : 0 },
{ attribute : g_equipAttributes[4] , factor : 1 / 5 , additive : 25 },
{ attribute : g_equipAttributes[9] , factor : 50 , additive : 0 } ],
shortMark : 'CLOAK'
},
{
index : -1,
name : '挑战斗篷',
type : 2,
attributes : [ { attribute : g_equipAttributes[4] , factor : 1 / 5 , additive : 50 },
{ attribute : g_equipAttributes[9] , factor : 100 , additive : 0 },
{ attribute : g_equipAttributes[18] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[20] , factor : 5 , additive : 0 } ],
shortMark : 'CAPE'
},
{
index : -1,
name : '战线支撑者的荆棘重甲',
type : 2,
attributes : [ { attribute : g_equipAttributes[3] , factor : 1 / 5 , additive : 20 },
{ attribute : g_equipAttributes[17] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[18] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[22] , factor : 1 / 15 , additive : 10 } ],
shortMark : 'THORN'
},
{
index : -1,
name : '复苏战衣',
type : 2,
attributes : [ { attribute : g_equipAttributes[3] , factor : 1 / 5 , additive : 50 },
{ attribute : g_equipAttributes[19] , factor : 5 , additive : 0 },
{ attribute : g_equipAttributes[20] , factor : 5 , additive : 0 },
{ attribute : g_equipAttributes[10] , factor : 20 , additive : 0 } ],
shortMark : 'WOOD'
},
{
index : -1,
name : '探险者铁甲',
type : 2,
attributes : [ { attribute : g_equipAttributes[8] , factor : 20 , additive : 0 },
{ attribute : g_equipAttributes[17] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[18] , factor : 1 , additive : 0 },
{ attribute : g_equipAttributes[10] , factor : 10 , additive : 0 } ],
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 } ],
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 } ],
shortMark : 'CLOTH'
},
{
index : -1,
name : '萌爪耳钉',
type : 3,
attributes : [ { attribute : g_equipAttributes[29] , factor : 17 / 2000 , additive : 0 },
{ attribute : g_equipAttributes[30] , factor : 17 / 2000 , additive : 0 },
{ attribute : g_equipAttributes[27] , factor : 1 / 30 , additive : 0 },
{ attribute : g_equipAttributes[28] , factor : 1 / 30 , additive : 0 } ],
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 } ],
shortMark : 'TIARA'
},
{
index : -1,
name : '猎魔耳环',
type : 3,
attributes : [ { attribute : g_equipAttributes[24] , factor : 2 / 5 , additive : 0 },
{ attribute : g_equipAttributes[26] , factor : 2 / 25 , additive : 0 },
{ attribute : g_equipAttributes[32] , factor : 2 / 25 , additive : 0 },
{ attribute : g_equipAttributes[3] , factor : 3 / 50 , additive : 0 } ],
shortMark : 'HUNT'
},
{
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 } ],
shortMark : 'SCARF'
}
];
const g_equipMap = new Map();
g_equipments.forEach((item, index) => {
item.index = index;
item.alias = item.name;
g_equipMap.set(item.name, item);
g_equipMap.set(item.shortMark, item);
});
const g_oldEquipNames = [
[ '荆棘盾剑', '荆棘剑盾' ],
[ '饮血魔剑', '饮血长枪' ],
[ '探险者手环', '探险者手套' ],
[ '秃鹫手环', '秃鹫手套' ],
[ '复苏战衣', '复苏木甲' ],
[ '萌爪耳钉', '天使缎带' ],
[ '占星师的耳饰', '占星师的发饰' ],
[ '探险者耳环', '探险者头巾' ]
];
const g_defaultEquipAttributeMerge = [ [0], [1], [2], [3] ];
const defaultEquipAttributeCalculate = ((a, l, p) => Math.trunc((l * a.factor + a.additive) * (p / 10)) / 10);
function defaultEquipmentNodeComparer(setting, eqKey, eq1, eq2) {
let eqMeta = g_equipMap.get(eqKey);
let delta = [];
let quality = eq1[2] + eq1[3] + eq1[4] + eq1[5] - eq2[2] - eq2[3] - eq2[4] - eq2[5];
let majorAdv = 0;
let majorEq = 0;
let majorDis = 0;
let minorAdv = 0;
eqMeta.attributes.forEach((attr, index) => {
let calculator = (attr.calculate ?? defaultEquipAttributeCalculate);
let d = calculator(attr, eq1[0], eq1[index + 1]) - calculator(attr, eq2[0], 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) {
majorAdv++;
}
else if (sum < 0) {
majorDis++;
}
else {
majorEq++;
}
};
return { quality : quality, majorAdv : majorAdv, majorEq : majorEq, majorDis : majorDis, minorAdv : minorAdv };
}
function formatEquipmentAttributes(e, itemSeparator) {
let text = '';
if (e?.length > 7) {
itemSeparator ??= ', ';
let sp = '';
console.log(e[0]);
g_equipMap.get(e[0])?.attributes.forEach((attr, index) => {
text += `${sp}${attr.attribute.name} +${(attr.calculate ?? defaultEquipAttributeCalculate)
(attr, e[1], e[index + 4])}${attr.attribute.type == 0 ? '%' : ''}`;
sp = itemSeparator;
});
}
return text;
}
function equipmentVerify(node, e) {
if ((e ??= equipmentInfoParseNode(node, false)) != null) {
let error = 0;
let attrs = node.getAttribute('data-content')?.match(/'>.+\+\d+\.?\d*%?.*? {
let eBase = (attr.calculate ?? defaultEquipAttributeCalculate)(attr, parseInt(e[1]), parseInt(e[index + 4]));
let disp = attrs[index].match(/\d+\.?\d*/)?.[0];
if (eBase.toString() != disp) {
console.log(`${e[0]} Lv.${e[1]} #${index}: ${attrs[index].slice(2, -5)} ---> ${eBase}`);
error++;
}
});
return error;
}
}
console.log(`BUG equip: ${node}`);
return -1;
}
function equipmentVerifyManual(name, level, attrs, displays) {
let eqMeta = g_equipMap.get(name);
if (eqMeta != null && attrs?.length == 4 && displays?.length == 4) {
console.log(`${name} Lv.${level}`);
eqMeta.attributes.forEach((a, i) => {
let eBase = (a.calculate ?? defaultEquipAttributeCalculate)(a, level, attrs[i]);
console.log(`${i}: ${displays[i]} ---> ${eBase}`);
});
}
}
// equipmentVerifyManual('噬魔戒指', 200, [90, 82, 99, 90], ['90', '131.2', '15.8', '12.6']);
var g_useOldEquipName = false;
var g_useThemeEquipName = false;
function equipLoadTheme(theme) {
if (theme.level?.length > 0) {
g_equipmentLevelName = theme.level;
}
if (g_useOldEquipName) {
g_oldEquipNames.forEach((item) => {
if (!g_equipMap.has(item[1])) {
let eqMeta = g_equipMap.get(item[0]);
if (eqMeta != null) {
eqMeta.alias = item[1];
g_equipMap.set(eqMeta.alias, eqMeta);
}
}
});
}
if (g_useThemeEquipName) {
for(let item in theme) {
if (/^[a-z]+\d+$/.test(item) && theme[item].length >= 5 && theme[item][3]?.length > 0 && !g_equipMap.has(theme[item][3])) {
let eqMeta = g_equipMap.get(theme[item][2]);
if (eqMeta != null) {
eqMeta.alias = theme[item][3];
g_equipMap.set(eqMeta.alias, eqMeta);
}
}
}
}
}
const g_halos = [
{ index : -1 , id : 101 , name : '启程之誓' , points : 0 , shortMark : 'SHI' },
{ index : -1 , id : 102 , name : '启程之心' , points : 0 , shortMark : 'XIN' },
{ index : -1 , id : 103 , name : '启程之风' , points : 0 , shortMark : 'FENG' },
{ index : -1 , id : 104 , name : '等级挑战' , points : 0 , shortMark : 'TIAO' },
{ index : -1 , id : 105 , name : '等级压制' , points : 0 , shortMark : 'YA' },
{ index : -1 , id : 201 , name : '破壁之心' , points : 20 , shortMark : 'BI' },
{ index : -1 , id : 202 , name : '破魔之心' , points : 20 , shortMark : 'MO' },
{ index : -1 , id : 203 , name : '复合护盾' , points : 20 , shortMark : 'DUN' },
{ index : -1 , id : 204 , name : '鲜血渴望' , points : 20 , shortMark : 'XUE' },
{ index : -1 , id : 205 , name : '削骨之痛' , points : 20 , shortMark : 'XIAO' },
{ index : -1 , id : 206 , name : '圣盾祝福' , points : 20 , shortMark : 'SHENG' },
{ index : -1 , id : 207 , name : '恶意抽奖' , points : 20 , shortMark : 'E' },
{ index : -1 , id : 301 , name : '伤口恶化' , points : 30 , shortMark : 'SHANG' },
{ index : -1 , id : 302 , name : '精神创伤' , points : 30 , shortMark : 'SHEN' },
{ index : -1 , id : 303 , name : '铁甲尖刺' , points : 30 , shortMark : 'CI' },
{ index : -1 , id : 304 , name : '忍无可忍' , points : 30 , shortMark : 'REN' },
{ index : -1 , id : 305 , name : '热血战魂' , points : 30 , shortMark : 'RE' },
{ index : -1 , id : 306 , name : '点到为止' , points : 30 , shortMark : 'DIAN' },
{ index : -1 , id : 307 , name : '午时已到' , points : 30 , shortMark : 'WU' },
{ index : -1 , id : 308 , name : '纸薄命硬' , points : 30 , shortMark : 'ZHI' },
{ index : -1 , id : 309 , name : '不动如山' , points : 30 , shortMark : 'SHAN' },
{ 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' },
{ index : -1 , id : 407 , name : '钝化锋芒' , points : 100 , shortMark : 'DUNH' },
{ index : -1 , id : 408 , name : '自信回头' , points : 100 , shortMark : 'ZI' }
];
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 : `最大并发网络请求(${ConcurrentRequestCount.min} - ${ConcurrentRequestCount.max})`,
defaultValue : ConcurrentRequestCount.default,
value : ConcurrentRequestCount.default,
tips : '同时向服务器提交的请求的最大数量。过高的设置容易引起服务阻塞或被认定为DDOS攻击从而导致服务器停止服务(HTTP 503)。',
validate : ((value) => {
return (!isNaN(value = parseInt(value)) && value >= ConcurrentRequestCount.min && value <= ConcurrentRequestCount.max);
}),
onchange : ((value) => {
if (!isNaN(value = parseInt(value)) && value >= ConcurrentRequestCount.min && value <= ConcurrentRequestCount.max) {
return (g_maxConcurrentRequests = value);
}
return (g_maxConcurrentRequests = ConcurrentRequestCount.default);
})
},
{
index : -1,
id : 'solutionSwitchMethod',
name : '绑定方案切换方式(0:完全,1:差分)',
defaultValue : 1,
value : 1,
tips : '执行绑定方案切换时所要使用的方法。“完全切换”和“差分切换”的主要区别在于护符组的加载方式,完全切换方式总是首先清空饰品栏然后按照绑定' +
'定义中指定的加载顺序逐项加载护符组,优点是优先级较高的组会先行加载,当饰品栏空间不足时不完全加载的组一般为优先级较低的组,而缺点则' +
'是加载效率较低;差分切换方式会忽略护符组的优先级,首先卸载已经在饰品栏中但并不在方案中的护符而留下重叠部分,然后加载缺失的护符,但' +
'加载顺序随机,这意味着当饰品栏空间不足时可能出现重要护符未能加载的情况,但这种加载方式效率较高(尤其是在护符重叠较多的方案间切换时)。' +
'正常情况下由于当前版本的饰品栏空间较为固定,所以绑定方案不应使用多于饰品栏空间的护符数量,而在这种情况下差分加载方式具有明显的优势。',
validate : ((value) => {
return /^[01]$/.test(value);
}),
onchange : ((value) => {
if (/^[01]$/.test(value)) {
return parseInt(value);
}
return 0;
})
},
{
index : -1,
id : 'singlePotRecovery',
name : '以单瓶方式使用体能刺激药水(0:禁止,1:允许)',
defaultValue : 1,
value : 1,
tips : '以单瓶方式使用体能刺激药水可再一次获得翻牌的贝壳和经验奖励(仅基础奖励,无道具),无需重新出击和翻牌。此方式对于药水充足且出击胜率高' +
'的玩家并非最佳选择,将此选项设为0可防止点错。',
validate : ((value) => {
return /^[01]$/.test(value);
}),
onchange : ((value) => {
if (/^[01]$/.test(value)) {
return parseInt(value);
}
return 0;
})
},
{
index : -1,
id : 'openCardSequence',
name : `一键翻牌次序(整数 ±(1 - 12) 组成的无重复序列或留空)`,
defaultValue : '',
value : '',
tips : '一键翻牌时的开牌次序,此设置留空时(不可留有空格字符,空格不会被判定为空而会判定为随机元素)将不显示一键翻牌相关面板。序列中禁止出现绝对值' +
'重复的元素,负数表示排除某张牌,可出现在任意位置且不占位,排除个数须不大于3。由于最多只需翻开9张牌必定会出现三同色,所以正数及随机元素个数' +
'须不大于9,不足9时程序将在开牌时按需随机补足。单个元素也可设置为随机,只需将该元素设置为无内容、仅含空格和/或单个“?”即可。' +
'例:“?”=“ ? ”=“,”=“ , ”=“ ”=全随机;“-12”=排除绮,其余全随机;“-1,12,,6,-2, , ? ,+5”=排除舞和默,依次翻开:绮、随机、薇、随机、' +
'随机、梦、随机到底。开牌过程中任意时刻出现三同色或错误即终止。',
validate : ((value) => {
let seq = value.split(',');
let abs;
let inc = 0;
let exc = 0;
let dup = [];
for (let e of seq) {
if ((e = e.trim()).length == 0 || e == '?') {
if (++inc > 9) {
return false;
}
continue;
}
else if ((!/^(\+|-)?\d+$/.test(e) || (abs = Math.abs(e = parseInt(e))) < 1 || abs > 12 || dup[abs] != null) ||
(e < 0 && ++exc > 3) || (e > 0 && ++inc > 9)) {
return false;
}
dup[abs] = true;
}
return true;
}),
onchange : ((value) => {
let seq = value.split(',');
let abs;
let inc = 0;
let exc = 0;
let dup = [];
for (let e of seq) {
if ((e = e.trim()).length == 0 || e == '?') {
if (++inc > 9) {
return '';
}
continue;
}
else if ((!/^(\+|-)?\d+$/.test(e) || (abs = Math.abs(e = parseInt(e))) < 1 || abs > 12 || dup[abs] != null) ||
(e < 0 && ++exc > 3) || (e > 0 && ++inc > 9)) {
return '';
}
dup[abs] = true;
}
return value;
})
},
{
index : -1,
id : 'maxEquipForgeHistoryList',
name : `锻造历史记录数限制(0 - 100)`,
defaultValue : 15,
value : 15,
tips : '锻造历史记录的条目数限制,0为不保存。当记录数超出限制时将按照锻造顺序移除较早的记录。除非您有特殊需求,否则此项设置不宜过大,15 ~ 30' +
'可能是较为平衡的选择',
validate : ((value) => {
return (!isNaN(value = parseInt(value)) && value >= 0 && value <= 100);
}),
onchange : ((value) => {
if (!isNaN(value = parseInt(value)) && value >= 0 && value <= 100) {
return value;
}
return 15;
})
},
{
index : -1,
id : 'minBeachEquipLevelToAmulet',
name : `沙滩装备转护符最小等级(绿,黄,红)`,
defaultValue : '1,1,1',
value : '1,1,1',
tips : '沙滩装备批量转换护符时各色装备所需达到的最小等级,小于对应等级的装备不会被转换,但玩家依然可以选择手动熔炼。',
validate : ((value) => {
return /^\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*$/.test(value);
}),
onchange : ((value) => {
if (/^\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*$/.test(value)) {
return value;
}
return '1,1,1';
})
},
{
index : -1,
id : 'minBeachAmuletPointsToStore',
name : `沙滩转护符默认入仓最小加成(苹果,葡萄,樱桃)`,
defaultValue : '1,1%,1%',
value : '1,1%,1%',
tips : '沙滩装备批量转换护符时默认处于入仓列表的最小加成,“%”可省略。此设置仅为程序产生分类列表时作为参考,玩家可通过双击或以上下文菜单键单击' +
'特定护符移动它的位置。',
validate : ((value) => {
return /^\s*\d+\s*,\s*\d+\s*%?\s*,\s*\d+\s*%?\s*$/.test(value);
}),
onchange : ((value) => {
if (/^\s*\d+\s*,\s*\d+\s*%?\s*,\s*\d+\s*%?\s*$/.test(value)) {
return value;
}
return '1,1%,1%';
})
},
{
index : -1,
id : 'clearBeachAfterBatchToAmulet',
name : `批量转护符完成后自动清理沙滩(0:否,1:是)`,
defaultValue : 0,
value : 0,
tips : '沙滩装备批量转换护符完成后自动清理沙滩上残存的装备。装备一但被清理将转换为各种锻造石且不可恢复,当此选项开启时请务必确保在使用批量转护符' +
'功能之前拾取所有欲保留的装备。',
validate : ((value) => {
return /^[01]$/.test(value);
}),
onchange : ((value) => {
if (/^[01]$/.test(value)) {
return parseInt(value);
}
return 0;
})
},
{
index : -1,
id : 'gemPollPeriod',
name : `宝石工坊挂机轮询周期(1 - ${Math.min(g_gemPollPeriodMinute.max, g_gemMinWorktimeMinute)}分钟,0:禁用挂机)`,
defaultValue : g_gemPollPeriodMinute.default,
value : g_gemPollPeriodMinute.default,
tips : '宝石工坊挂机程序向服务器发出进度轮询请求的间隔时间,以分钟为单位。0表示禁用挂机程序,同时各工作台完工剩余时间和工会面板也将不会显示。此项设置越' +
'小对服务器造成的压力越大,除非您一直盯着宝石工坊的页面(这种情况建议手动按需刷新),否则您不会因较小的设置值获得任何额外优势,因为挂机程序会根据' +
'工作进度动态调整轮询时间间隔以便及时收工重开。所以如果您的需求只是单纯的自动收工重开则建议设置为最大值。唯一例外是当您的设备时钟走时明显偏慢,小' +
'时误差达到分钟级别时可根据情况适当调小设置,推荐公式为“期望时间 - 期望时间内可能产生的最大正误差”。如果您的设备时钟偏快则在设为最大值的情况下无' +
'需进行任何额外调整。',
validate : ((value) => {
return (!isNaN(value = parseInt(value)) && value <= g_gemPollPeriodMinute.max);
}),
onchange : ((value) => {
if (!isNaN(value = parseInt(value)) && value <= g_gemPollPeriodMinute.max) {
return value;
}
return g_gemPollPeriodMinute.default;
})
},
{
index : -1,
id : 'gemWorkCompletionCondition',
name : `宝石工坊完工条件(按工作台顺序以“,”分隔或留空)`,
defaultValue : '',
value : '',
tips : '宝石工坊运作过程中挂机程序对各工作台进行完工判定的条件,按照工作台次序以“,”分隔,“%”可省略。也可以单项或全部留空表示使用默认值(目前贝壳1000000,' +
'星沙10,幻影经验200,其它100%)。在满足基本收工条件后(目前为开工满8小时),此设置将被挂机程序直接用于自动收工判定。请注意,设置程序只进行格式检' +
'查,并不会对有效值范围作出任何假设,所以一个错误的设置可能会令完工判定逻辑失效从而影响自动收工功能的正确运行。',
validate : ((value) => {
let conds = value.split(',');
if (conds.length > g_gemWorks.length) {
return false;
}
for (let i = conds.length - 1; i >= 0; i--) {
if ((conds[i] = conds[i].trim()).length > 0 && !(new RegExp(`^${g_gemWorks[i].unitRegex.regex.source}$`)).test(conds[i])) {
return false;
}
}
return true;
}),
onchange : ((value) => {
let conds = value.split(',');
if (conds.length > g_gemWorks.length) {
return '';
}
for (let i = conds.length - 1; i >= 0; i--) {
if ((conds[i] = conds[i].trim()).length > 0 && !(new RegExp(`^${g_gemWorks[i].unitRegex.regex.source}$`)).test(conds[i])) {
return '';
}
}
return value;
})
}
];
const g_configMap = new Map();
g_configs.forEach((item, index) => {
item.index = index;
g_configMap.set(item.id, item);
});
function readConfig() {
let udata = loadUserConfigData();
g_configs.forEach((item) => {
item.value = (item.onchange?.call(null, udata.config[item.id] ?? item.defaultValue));
});
return udata;
}
function modifyConfig(configIds, title, reload) {
title ??= '插件设置';
let udata = readConfig();
let configs = (configIds?.length > 0 ? [] : g_configs);
if (configIds?.length > 0) {
for (let id of configIds) {
let cfg = g_configMap.get(id);
if (cfg != null) {
configs.push(cfg);
}
}
}
genericPopupInitialize();
let fixedContent =
'请勿随意修改配置项,' +
`除非您知道它的准确用途并且设置为正确的值,否则可能导致插件工作异常
';
let mainContent =
`