(.*)<\/div><\/div><\/div>/;
+ const profs = gE('#stats_pane .stats_page:last-child .st2:last-child', doc).innerHTML.match( new RegExp(regex, regex.flags + 'g'));
+ profs.forEach(p=>{
+ const exec = p.match(regex);
+ proficiency[exec[2]] = exec[1]*1;
+ });
+ } else {
+ gE('#stats_scrollable table:last-child tr', 'all', doc).forEach((tr) => {
+ const exec = tr.innerHTML.match(/
(.*)<\/td>.* | (.*)<\/td>/);
+ proficiency[exec[2]] = exec[1]*1;
+ });
}
- const html = await $ajax.fetch(href);
+ localStorage.setItem(`hvAA-${current}_proficiency`, JSON.stringify(proficiency));
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err) } }
+
+ async function asyncSetAbilityData() { try {
+ await waitPause();
+ $async.logSwitch(arguments);
+ const html = await $ajax.insert('?s=Character&ss=ab');
const doc = $doc(html);
- const slots = Array.from(gE('.ability_slotbox>div>div', 'all', doc)).forEach(slot => {
- const id = slot.id.match(/_(\d*)/)[1];
- const parent = slot.parentNode.parentNode.parentNode;
- ability[id] = {
- name: gE('.fc2', parent).innerText,
- type: type,
- level: Array.from(gE('.aw1,.aw2,.aw3,.aw4,.aw5,.aw6,.aw7,.aw8,.aw9,.aw10', parent).children).map(div => div.style.cssText.indexOf('f.png') === -1 ? 0 : 1).reduce((x, y) => x + y),
- }
+ const abd = {
+ // 'HP Tank': { id: 1101, unlock: [0, 25, 50, 75, 100, 120, 150, 200, 250, 300], level: 0 },
+ // 'MP Tank': { id: 1102, unlock: [0, 30, 60, 90, 120, 160, 210, 260, 310, 350], level: 0 },
+ // 'SP Tank': { id: 1103, unlock: [0, 40, 80, 120, 170, 220, 270, 330, 390, 450], level: 0 },
+ // 'Better Health Pots': { id: 1104, unlock: [0, 100, 200, 300, 400], level: 0 },
+ // 'Better Mana Pots': { id: 1105, unlock: [0, 80, 140, 220, 380], level: 0 },
+ // 'Better Spirit Pots': { id: 1106, unlock: [0, 90, 160, 240, 400], level: 0 },
+ // '1H Damage': { id: 2101, unlock: [0, 100, 200], level: 0 },
+ // '1H Accuracy': { id: 2102, unlock: [50, 150], level: 0 },
+ // '1H Block': { id: 2103, unlock: [250], level: 0 },
+ // '2H Damage': { id: 2201, unlock: [0, 100, 200], level: 0 },
+ // '2H Accuracy': { id: 2202, unlock: [50, 150], level: 0 },
+ // '2H Parry': { id: 2203, unlock: [250], level: 0 },
+ // 'DW Damage': { id: 2301, unlock: [0, 100, 200], level: 0 },
+ // 'DW Accuracy': { id: 2302, unlock: [50, 150], level: 0 },
+ // 'DW Crit': { id: 2303, unlock: [250], level: 0 },
+ // 'Staff Spell Damage': { id: 2501, unlock: [0, 100, 200], level: 0 },
+ // 'Staff Accuracy': { id: 2502, unlock: hvVersion < 91 ? [50, 150, 300] : [50, 150], level: 0 },
+ // 'Staff Damage': { id: 2503, unlock: [0], level: 0 },
+ // 'Cloth Spellacc': { id: 3101, unlock: hvVersion < 91 ? [0, 120, 240] : [120], level: 0 },
+ // 'Cloth Spellcrit': { id: 3102, unlock: [0, 40, 90, 130, 190], level: 0 },
+ // 'Cloth Castspeed': { id: 3103, unlock: [150, 250], level: 0 },
+ // 'Cloth MP': { id: 3104, unlock: [0, 60, 110, 170, 230, 290, 350], level: 0 },
+ // 'Light Acc': { id: 3201, unlock: [0], level: 0 },
+ // 'Light Crit': { id: 3202, unlock: [0, 40, 90, 130, 190], level: 0 },
+ // 'Light Speed': { id: 3203, unlock: [150, 250], level: 0 },
+ // 'Light HP/MP': { id: 3204, unlock: [0, 60, 110, 170, 230, 290, 350], level: 0 },
+ // 'Heavy Crush': { id: 3301, unlock: [0, 75, 150], level: 0 },
+ // 'Heavy Prcg': { id: 3302, unlock: [0, 75, 150], level: 0 },
+ // 'Heavy Slsh': { id: 3303, unlock: [0, 75, 150], level: 0 },
+ // 'Heavy HP': { id: 3304, unlock: [0, 60, 110, 170, 230, 290, 350], level: 0 },
+ 'Better Weaken': { id: 4201, unlock: [70, 100, 130, 190, 250], level: 0 },
+ 'Faster Weaken': { id: 4202, unlock: [80, 165, 250], level: 0 },
+ 'Better Imperil': { id: 4203, unlock: [130, 175, 230, 285, 330], level: 0 },
+ 'Faster Imperil': { id: 4204, unlock: [140, 225, 310], level: 0 },
+ 'Better Blind': { id: 4205, unlock: [110, 130, 160, 190, 220], level: 0 },
+ 'Faster Blind': { id: 4206, unlock: [120, 215, 275], level: 0 },
+ 'Mind Control': { id: 4207, unlock: [80, 130, 170], level: 0 },
+ 'Better Silence': { id: 4211, unlock: [120, 170, 215], level: 0 },
+ 'Better MagNet': hvVersion < 91 ? { id: 4212, unlock: [250, 295, 340, 370, 400], level: 0 } : undefined,
+ 'Better Immobilize': hvVersion < 91 ? undefined : { id: 4212, unlock: [250, 295, 340, 370, 400], level: 0 },
+ 'Better Slow': { id: 4213, unlock: [30, 50, 75, 105, 135], level: 0 },
+ // 'Better Drain': { id: 4216, unlock: [20, 50, 90], level: 0 },
+ // 'Faster Drain': { id: 4217, unlock: [30, 70, 110, 150, 200], level: 0 },
+ // 'Ether Theft': { id: 4218, unlock: [150], level: 0 },
+ // 'Spirit Theft': { id: 4219, unlock: [150], level: 0 },
+ // 'Better Haste': { id: 4102, unlock: [60, 75, 90, 110, 130], level: 0 },
+ // 'Better Shadow Veil': { id: 4103, unlock: [90, 105, 120, 135, 155], level: 0 },
+ // 'Better Absorb': { id: 4104, unlock: [40, 60, 80], level: 0 },
+ // 'Stronger Spirit': { id: 4105, unlock: [200, 220, 240, 265, 285], level: 0 },
+ // 'Better Heartseeker': { id: 4106, unlock: [140, 185, 225, 265, 305, 345, 385], level: 0 },
+ // 'Better Arcane Focus': { id: 4107, unlock: [175, 205, 245, 285, 325, 365, 405], level: 0 },
+ // 'Better Regen': { id: 4108, unlock: [50, 70, 95, 145, 195, 245, 295, 375, 445, 500], level: 0 },
+ // 'Better Cure': { id: 4109, unlock: [0, 35, 65], level: 0 },
+ // 'Better Spark': { id: 4110, unlock: [100, 125, 150], level: 0 },
+ // 'Better Protection': { id: 4101, unlock: [40, 55, 75, 95, 120], level: 0 },
+ // 'Flame Spike Shield': { id: 4111, unlock: [10, 65, 140, 220, 300], level: 0 },
+ // 'Frost Spike Shield': { id: 4112, unlock: [10, 65, 140, 220, 300], level: 0 },
+ // 'Shock Spike Shield': { id: 4113, unlock: [10, 65, 140, 220, 300], level: 0 },
+ // 'Storm Spike Shield': { id: 4114, unlock: [10, 65, 140, 220, 300], level: 0 },
+ 'Conflagration': { id: 4301, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 },
+ 'Cryomancy': { id: 4302, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 },
+ 'Havoc': { id: 4303, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 },
+ 'Tempest': { id: 4304, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 },
+ // 'Sorcery': { id: 4305, unlock: [70, 140, 210, 280, 350], level: 0 },
+ // 'Elementalism': { id: 4306, unlock: [85, 170, 255, 340, 425], level: 0 },
+ // 'Archmage': { id: 4307, unlock: [90, 180, 270, 360, 450], level: 0 },
+ 'Better Corruption': { id: 4401, unlock: [75, 150], level: 0 },
+ 'Better Disintegrate': { id: 4402, unlock: [175, 250], level: 0 },
+ 'Better Ragnarok': { id: 4403, unlock: [250, 325, 400], level: 0 },
+ // 'Ripened Soul': { id: 4404, unlock: [150, 300, 450], level: 0 },
+ // 'Dark Imperil': { id: 4405, unlock: [175, 225, 275, 325, 375], level: 0 },
+ 'Better Smite': { id: 4501, unlock: [75, 150], level: 0 },
+ 'Better Banish': { id: 4502, unlock: [175, 250], level: 0 },
+ 'Better Paradise': { id: 4503, unlock: [250, 325, 400], level: 0 },
+ // 'Soul Fire': { id: 4504, unlock: [150, 300, 450], level: 0 },
+ // 'Holy Imperil': { id: 4505, unlock: [175, 225, 275, 325, 375], level: 0 },
+ }
+
+ const newAbility = {};
+ gE('#ability_top div[onmouseover*="overability"]', 'all', doc).forEach((div) => {
+ const exec = div.getAttribute('onmouseover').match(/overability\(\d+, '([^']+)','.+?','(?:(Not Acquired|At Maximum)|Requires Level (\d+).+?)','(Not Acquired|At Maximum|Requires Level (\d+).+?)'/);
+ const name = exec[1];
+ const ab = abd[name];
+ if (!ab) return;
+ newAbility[ab.id] = exec[2] ? 0 : ab.unlock.indexOf(1*exec[3]) + 1;
});
- } catch (e) {console.error(e)}}));
- setValue('ability', ability);
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
-
- async function asyncSetEnergyDrinkHathperk() { try {
- if (isIsekai || !g('option').restoreStamina) {
- return;
- }
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncSetEnergyDrinkHathperk();
- }
- logSwitchAsyncTask(arguments);
- const html = await $ajax.fetch('https://e-hentai.org/hathperks.php');
- if(!html) {
- return;
- }
- const doc = $doc(html);
- const perks = gE('.stuffbox>table>tbody>tr', 'all', doc);
- if (!perks) {
- return;
- }
- setValue('staminaHathperk', perks[25].innerHTML.includes('Obtained'));
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
-
- async function asyncSetStamina() { try {
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncSetStamina();
- }
- logSwitchAsyncTask(arguments);
- const html = await $ajax.fetch(window.location.href);
- setValue('staminaTime', Math.floor(time(0) / 1000 / 60 / 60));
- setValue('stamina', gE('#stamina_readout .fc4.far>div', $doc(html)).textContent.match(/\d+/)[0] * 1);
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
-
- async function asyncGetItems() { try {
- if (!g('option').checkSupply && (isIsekai || !g('option').restoreStamina)) {
- return;
- }
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncGetItems();
- }
- logSwitchAsyncTask(arguments);
- const html = await $ajax.fetch('?s=Character&ss=it');
- const items = {};
- for (let each of gE('.nosel.itemlist>tbody', $doc(html)).children) {
- const name = each.children[0].children[0].innerText;
- const id = each.children[0].children[0].getAttribute('id').split('_')[1];
- const count = each.children[1].innerText;
- items[id] = [name, count];
- }
- g('items', items);
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
-
- async function asyncCheckSupply() { try {
- if (!g('option').checkSupply) {
- return true;
- }
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncCheckSupply();
- }
- logSwitchAsyncTask(arguments);
- const items = g('items');
- const thresholdList = g('option').checkItem;
- const checkList = g('option').isCheck;
- const needs = [];
- for (let id in checkList) {
- const item = items[id];
- if (!item) {
- continue;
- }
- const [name, count] = item;
- const threshold = thresholdList[id] ?? 0;
- if ((count ?? 0) >= threshold) {
- continue;
- }
- needs.push(`\n${name}(${count}<${threshold})`);
- }
- if (needs.length) {
- console.log(`Needs supply:${needs}`);
- document.title = `[C!]` + document.title;
- }
- logSwitchAsyncTask(arguments);
- return !needs.length;
- } catch (e) {console.error(e)} return false; }
+ setValue('ability', newAbility);
+ ability = Object.keys(newAbility).length ? newAbility : ability;
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err) } }
+
+ async function asyncSetStamina() { try {
+ await waitPause();
+ $async.logSwitch(arguments);
+ const stamina = getValue('stamina', true) ?? { ratio: 1 };
+ let [last, lastTime] = [stamina.current, stamina.time];
+ [stamina.current, stamina.punish, stamina.perk] = await Promise.all([
+ ... (await getCurrentStamina()),
+ (async () => { try {
+ let perk = stamina.perk;
+ if (perk && !Array.isArray(perk)) {
+ perk = Object.keys(perk).map(id=>id*1);
+ }
+ if (!perk?.length) {
+ perk = undefined;
+ }
+ if (isIsekai || !g().option.restoreStamina) {
+ return perk;
+ }
+ let currentID, html;
+ if (perk && perk[currentID = getCurrentUser() * 1]) {
+ return perk;
+ }
+ if (!(html = await $ajax.insert('https://e-hentai.org/hathperks.php'))) {
+ return perk;
+ }
+ const doc = $doc(html);
+ const perks = gE('.stuffbox>table>tbody>tr', 'all', doc);
+ if (perks && perks[25].innerHTML.includes('Obtained') && !(perk ??= []).includes(currentID)) {
+ perk.push(currentID);
+ }
+ return perk;
+ } catch(e) {console.error(e) }})()
+ ]);
+ if (!stamina.current) {
+ if (!getValue('stamina')) {
+ setValue('stamina', stamina);
+ }
+ $async.logSwitch(arguments);
+ return
+ }
+ stamina.time = time(0);
+ if (!stamina.punish) {
+ [stamina.lastRatio, stamina.lastRatioRaw] = [stamina.ratio, stamina.ratioRaw];
+ [stamina.ratio, stamina.ratioRaw] = [undefined, undefined]
+ }
+ if (stamina.ratio === 1 && (stamina.lastRatio === 1 || !stamina.lastRatio)) {
+ [stamina.ratio, stamina.lastRatio, stamina.lastRatioRaw, stamina.ratioRaw] = Array(4).fill(undefined);
+ }
+ const lastCost = stamina.lastCost;
+ stamina.lastCost = undefined;
+ if (!lastCost || lastCost <= 0.06 ) {
+ setValue('stamina', stamina);
+ $async.logSwitch(arguments);
+ return;
+ }
+ last += Math.floor(stamina.time / _1h) - Math.floor(lastTime / _1h);
+ const delta = last - stamina.current;
+ if (!delta) {
+ setValue('stamina', stamina);
+ $async.logSwitch(arguments);
+ return;
+ }
+ const ratio = stamina.punish ? Math.max(1, Math.round(delta / lastCost / 0.25)*0.25) : 1;
+ if (stamina.ratio === ratio) {
+ setValue('stamina', stamina);
+ $async.logSwitch(arguments);
+ return;
+ }
+ [stamina.lastRatio, stamina.lastRatioRaw] = [stamina.ratio ?? 1, stamina.ratioRaw];
+ [stamina.ratio, stamina.ratioRaw] = [ratio, `${delta} / ${Math.round(lastCost * 100) / 100} = ${delta / lastCost}`]
+ setValue('stamina', stamina);
+ console.log('stamina', stamina, '\n', last, '->', stamina.current, '=', lastCost, '*', ratio);
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err) } }
- async function asyncCheckRepair() { try {
- if (!g('option').repair) {
- return true;
- }
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncCheckRepair();
- }
- logSwitchAsyncTask(arguments);
- const doc = $doc(await $ajax.fetch('?s=Forge&ss=re'));
- const json = JSON.parse((await $ajax.fetch(gE('#mainpane>script[src]', doc).src)).match(/{.*}/)[0]);
- const eqps = (await Promise.all(Array.from(gE('.eqp>[id]', 'all', doc)).map(async eqp => { try {
- const id = eqp.id.match(/\d+/)[0];
- const condition = 1 * json[id].d.match(/Condition: \d+ \/ \d+ \((\d+)%\)/)[1];
- if (condition > g('option').repairValue) {
+ async function asyncGetItems() { try {
+ if (!g().option.checkSupply && (isIsekai || !g().option.restoreStamina)) {
return;
}
- return gE('.messagebox_error', $doc(await $ajax.fetch(`?s=Forge&ss=re`, `select_item=${id}`)))?.innerText ? undefined : id;
- } catch (e) {console.error(e)}}))).filter(e => e);
- if (eqps.length) {
- console.log('eqps need repair: ', eqps);
- document.title = `[R!]` + document.title;
- }
- logSwitchAsyncTask(arguments);
- return !eqps.length;
- } catch (e) {console.error(e)}; return false; }
-
- function checkStamina(low, cost) {
- let stamina = getValue('stamina');
- const lastTime = getValue('staminaTime');
- let timeNow = Math.floor(time(0) / _1h);
- stamina += lastTime ? timeNow - lastTime : 0;
- const stmNR = stamina + 24 - (timeNow % 24);
- cost ??= 0;
- const stmNRChecked = !cost || stmNR - cost >= g('option').staminaLowWithReNat;
- console.log('stamina:', stamina,'\nstamina with nature recover:', stmNR, '\nnext arena stamina cost: ', cost.toString());
- if (stamina - cost >= (low ?? g('option').staminaLow) && stmNRChecked) {
- return 1;
- }
- let checked = 0;
- if (!stmNRChecked) {
- checked = -1;
- }
- if (isIsekai || !g('option').restoreStamina) {
- return checked;
- }
- const items = g('items');
- if (!items) {
- return checked;
- }
- const recover = items[11402] ? 5 : items[11401] ? getValue('staminaHathperk') ? 20 : 10 : 0;
- if (recover && stamina <= (100 - recover)) {
- $ajax.open(window.location.href, 'recover=stamina');
- return checked;
- }
- }
+ await waitPause();
+ $async.logSwitch(arguments);
+ const html = await $ajax.insert('?s=Character&ss=it');
+ const items = {};
+ const doc = $doc(html);
+ if (gE('#riddlecounter', doc) || gE('#battle_main', doc)) {
+ $async.logSwitch(arguments);
+ g('items', null);
+ return;
+ }
+ for (let each of gE('.nosel.itemlist>tbody', doc).children) {
+ const name = each.children[0].children[0].innerText;
+ const id = each.children[0].children[0].getAttribute('id').split('_')[1];
+ const count = each.children[1].innerText;
+ items[id] = [name, count];
+ }
+ g('items', items);
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err) } }
- async function updateEncounter(engage, isInBattle) { try {
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await updateEncounter(engage, isInBattle);
- }
- const encounter = getEncounter();
- const encountered = encounter.filter(e => e.encountered && e.href);
- const count = encounter.filter(e => e.href).length;
-
- const now = time(0);
- const last = encounter[0]?.time ?? getValue('lastEH', true) ?? 0; // 上次遭遇 或 上次打开EH 或 0
- let cd;
- if (encountered.length >= 24) {
- cd = Math.floor(encounter[0].time / _1d + 1) * _1d - now;
- } else if (!last) {
- cd = 0;
- } else {
- cd = _1h / 2 + last - now;
- }
- cd = Math.max(0, cd);
- const ui = gE('.encounterUI') ?? (() => {
- const ui = gE('body').appendChild(cE('a'));
- ui.className = 'encounterUI';
- ui.title = `${time(3, last)}\nEncounter Time: ${count}`;
- if (!isInBattle) {
- ui.href = 'https://e-hentai.org/news.php?encounter';
- }
- return ui;
- })();
-
- const missed = count - encountered.length;
- if (count === 24) {
- ui.style.cssText += 'color:orange!important;';
- } else if (!cd) {
- ui.style.cssText += 'color:red!important;';
- } else {
- ui.style.cssText += 'color:unset!important;';
- }
- ui.innerHTML = `${formatTime(cd).slice(0, 2).map(cdi => cdi.toString().padStart(2, '0')).join(`:`)}[${encounter.length ? (count >= 24 ? `☯` : count) : `✪`}${missed ? `-${missed}` : ``}]`;
- if (engage && !cd) {
- onEncounter();
- return true;
- }
- let interval = cd > _1h ? _1m : (!g('option').encounterQuickCheck || cd > _1m) ? _1s : 80;
- interval = (g('option').encounterQuickCheck && cd > _1m) ? (interval - cd % interval) / 4 : interval; // 让倒计时显示更平滑
- setTimeout(() => updateEncounter(engage), interval);
- } catch (e) {console.error(e)}}
-
- function onEncounter() {
- if (getValue('disabled') || getValue('battle') || !checkBattleReady(onEncounter, { staminaLow: g('option').staminaEncounter })) {
- return;
- }
- setEncounter(getEncounter()); // 离开页面前保存
- if(!window.top.location.href.endsWith(`?s=Battle`)){
- setValue('lastHref', window.top.location.href);
+ function checkSupply(isGFStandalone) {
+ const option = g().option;
+ if (!option.checkSupply) {
+ return true;
+ }
+ const items = g().items;
+ if (!items) {
+ return false;
+ }
+ const thresholdList = isGFStandalone ? option.checkItemGF : option.checkItem;
+ const checkList = isGFStandalone ? option.isCheckGF : option.isCheck;
+ const percentage = isGFStandalone ? option.checkSupplyWarnGF : option.checkSupplyWarn;
+ const needs = [];
+ const warns = [];
+ for (let id in checkList) {
+ const item = items[id];
+ if (!item) continue;
+ let [name, count] = item;
+ const threshold = thresholdList[id] ?? 0;
+ const warnThreshold = threshold * percentage / 100;
+ count ??= 0;
+ if (count < warnThreshold) {
+ warns.push(`\n${name}(${count}<${warnThreshold}(${threshold}*${percentage}%))`);
+ }
+ if (count >= threshold) {
+ continue;
+ }
+ needs.push(`\n${name}(${count}<${threshold})`);
+ }
+ if (needs.length) {
+ console.log(`Needs supply:${needs}`);
+ document.title = `[C!${isGFStandalone ? '!' : ''}]` + document.title;
+ switch(option.lang * 1) {
+ case 0:
+ popup(`消耗品${isGFStandalone ? '(压榨届独立配置)' : ''}不足:\n${needs}`);
+ break
+ case 1:
+ popup(`消耗品${isGFStandalone ? '(壓榨屆獨立配置)' : ''}不足:\n${needs}`);
+ break
+ case 2:
+ default:
+ popup(`Failed supply check${isGFStandalone ? ' for Grindfest standalone' : ''}:\n${needs}`);
+ break
+ }
+ } else if (warns.length) {
+ console.log(`Warn supply:${warns}`);
+ document.title = `[C!${isGFStandalone ? '!' : ''}]` + document.title;
+ switch(option.lang * 1) {
+ case 0:
+ popup(`消耗品${isGFStandalone ? '(压榨届独立配置)' : ''} < ${percentage}%:\n${warns}`);
+ break
+ case 1:
+ popup(`消耗品${isGFStandalone ? '(壓榨屆獨立配置)' : ''} < ${percentage}%:\n${warns}`);
+ break
+ case 2:
+ default:
+ popup(`Supplys ${isGFStandalone ? ' for Grindfest standalone' : ''} < ${percentage}%:\n${warns}`);
+ break
+ }
+ }
+ return !needs.length;
}
- $ajax.openNoFetch('https://e-hentai.org/news.php?encounter');
- }
- async function startUpdateArena(idleStart, startIdleArena=true) { try {
- const now = time(0);
- if (!idleStart) {
- await updateArena();
- }
- let timeout = g('option').idleArenaTime * _1s;
- if (idleStart) {
- timeout -= time(0) - idleStart;
- }
- if(startIdleArena){
- setTimeout(idleArena, timeout);
- }
- const last = getValue('arena', true)?.date ?? now;
- setTimeout(startUpdateArena, Math.max(0, Math.floor(last / _1d + 1) * _1d - now));
- } catch (e) {console.error(e)}}
-
- async function updateArena(forceUpdateToken = false) { try {
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await updateArena(forceUpdateToken);
- }
- let arena = getValue('arena', true) ?? {};
- const isToday = arena.date && time(2, arena.date) === time(2);
- if (forceUpdateToken || !isToday || !arena.isOptionUpdated) {
- arena.token = {};
- arena.sites ??= [
- '?s=Battle&ss=gr',
- '?s=Battle&ss=ar',
- '?s=Battle&ss=ar&page=2',
- '?s=Battle&ss=rb'
- ]
- await Promise.all(arena.sites.map(async site => { try {
- const doc = $doc(await $ajax.fetch(site));
- if (site === '?s=Battle&ss=gr') {
- arena.token.gr = gE('img[src*="startgrindfest.png"]', doc).getAttribute('onclick').match(/init_battle\(1, '(.*?)'\)/)[1];
- return;
+ async function asyncCheckEnchant(isGrindFest) {
+ try {
+ if (hvVersion >= 91) return true;
+ $async.logSwitch(arguments);
+ const option = g().option;
+ const [isEnchant, thresholds] = isGrindFest && option.checkEnchantGF ? [option.isEnchantGF, option.enchantGF] : [option.isEnchant, option.enchant];
+ if (!isEnchant) return true;
+ await waitPause();
+
+ const enchant = {};
+ Object.keys(isEnchant).forEach(id => {
+ if (!isEnchant[id]) return;
+ const item = Math.floor(id/100);
+ const slot = id % 100;
+ (enchant[slot] ??= {})[item] = thresholds[id];
+ });
+
+ const url = `?s=Forge&ss=re`;
+ const enchant_data= {
+ "Voidseeker's Blessing" : { Weapon: 'vseek', item:61001, }, // 'Voidseeker Shard'
+ 'Suffused Aether' : { Weapon: 'ether', item:61101, }, // 'Aether Shard'
+ 'Featherweight Charm' : { Weapon: 'feath', Armor: 'feath', item:61501, }, // 'Featherweight Shard'
+ 'Infused Flames' : { Weapon: 'sfire', Armor: 'pfire', item:12101, }, // 'Infusion of Flames'
+ 'Infused Frost' : { Weapon: 'scold', Armor: 'pcold', item:12201, }, // 'Infusion of Frost'
+ 'Infused Lightning' : { Weapon: 'selec', Armor: 'pelec', item:12301, }, // 'Infusion of Lightning'
+ 'Infused Storms' : { Weapon: 'swind', Armor: 'pwind', item:12401, }, // 'Infusion of Storms'
+ 'Infused Divinity' : { Weapon: 'sholy', Armor: 'pholy', item:12501, }, // 'Infusion of Divinity'
+ 'Infused Darkness' : { Weapon: 'sdark', Armor: 'pdark', item:12601, }, // 'Infusion of Darkness'
+ }
+ const item2Enchant = {
+ 61001: "Voidseeker's Blessing",
+ 61101: 'Suffused Aether',
+ 61501: 'Featherweight Charm',
+ 12101: 'Infused Flames',
+ 12201: 'Infused Frost',
+ 12301: 'Infused Lightning',
+ 12401: 'Infused Storms',
+ 12501: 'Infused Divinity',
+ 12601: 'Infused Darkness',
}
- gE('img[src*="startchallenge.png"]', 'all', doc).forEach((_) => {
- const temp = _.getAttribute('onclick').match(/init_battle\((\d+),\d+,'(.*?)'\)/);
- arena.token[temp[1]] = temp[2];
+ const d = $doc(await $ajax.insert(`?s=Character&ss=eq`));
+ const eqps = {};
+ Array.from(gE('.eqb', 'all', d)).forEach(eqb=> {
+ const slot = eqb.getAttribute('onclick').match(`equip_slot=(.*)'`)[1] * 1;
+ const id = gE('div[onmouseover*="equips.set"]', eqb)?.id.replace('e', '') * 1;
+ eqps[id]=slot;
});
- } catch (e) {console.error(e)}}));
- }
- if(!isToday){
- arena.date = time(0);
- arena.gr = g('option').idleArenaGrTime;
- arena.arrayDone = [];
- }
- if (!isToday || !arena.isOptionUpdated) {
- arena.array = g('option').idleArenaValue.split(',') ?? [];
- arena.array.reverse();
- }
- return setValue('arena', arena);
- } catch (e) {console.error(e)}}
+ const doc = $doc(await $ajax.insert(url));
+ const eqpdoc = await $ajax.insert(gE('#mainpane>script[src]', doc).src);
+ const json = JSON.parse(eqpdoc.match(/{.*}/)[0]);
+ await Promise.all(Array.from(gE('.eqp>[id]', 'all', doc)).map(e => (async eqp => { try {
+ const id = eqp.id.match(/\d+/)[0];
+ const slot = eqps[id];
+ const data = json[id];
+ if (!enchant[slot]) return;
+ const enchanted = {};
+ Array.from(gE('#ee>span', 'all', $doc(await $ajax.insert(`equip/${id}/${data.k}`))))?.forEach(s=> {
+ const info = s.innerHTML.match(/(.*) \[(\d+)m\]/);
+ enchanted[enchant_data[info[1]].item] = info[2]*1
+ return;
+ });
+ let type = data.d.match(`.* *(Armor|Weapon|Shield|Staff).* Condition`)[1];
+ type = (type === 'Shield') ? 'Armor' : (type === 'Staff' ? 'Weapon' : type);
+ return await Promise.all(Object.keys(enchant[slot]).map(i => (async item => { try {
+ const threshold = enchant[slot][item];
+ const current = enchanted[item] ?? 0;
+ const enc = enchant_data[item2Enchant[item]][type];
+ if (!enc) return;
+ if (current && current >= threshold) return;
+ const failed = $doc(await $ajax.insert(`?s=Forge&ss=en`, `select_item=${id}&enchantment=${enc}`))?.innerText;
+ if (failed) {
+ console.error(failed, ':', data.t, ':', enc, '=', current, '>?', threshold, '=', current >= threshold);
+ }
+ } catch (err) { console.error(err) }; })(i)));
+ } catch (err) { console.error(err) }; })(e)));
+ } catch (err) { console.error(err) }; return false; }
- function checkBattleReady(method, condition = {}) {
- if (getValue('disabled')) {
- setTimeout(method, _1s);
- return;
- }
- if (condition.checkEncounter && getEncounter()[0]?.href && !getEncounter()[0]?.encountered) {
- Debug.log(getEncounter());
- return;
- }
- const staminaChecked = checkStamina(condition.staminaLow, condition.staminaCost);
- console.log("staminaChecked", condition.staminaLow, condition.staminaCost, staminaChecked);
- if(staminaChecked === 1){ // succeed
+ async function asyncCheckRepair(isGrindFestStandalone) { try {
+ const option = g().option;
+ if (!option.repair) {
return true;
- }
- if(staminaChecked === 0){ // failed until today ends
- setTimeout(method, Math.floor(time(0) / _1h + 1) * _1h - time(0));
- document.title = `[S!!]` + document.title;
- } else { // case -1: // failed with nature recover
- document.title = `[S!]` + document.title;
- }
- }
+ }
+ await waitPause();
+ $async.logSwitch(arguments);
+ let eqps;
+ const threshold = isGrindFestStandalone ? option.repairValueGF : option.repairValue;
+ if (!threshold) { // skip because default repair has been checked before idleArena>GF
+ $async.logSwitch(arguments);
+ return true;
+ }
+ if (hvVersion < 91) {
+ const url = `?s=Forge&ss=re`;
+ const doc = $doc(await $ajax.insert(url));
+ const eqpdoc = await $ajax.insert(gE('#mainpane>script[src]', doc).src);
+ const json = JSON.parse(eqpdoc.match(/{.*}/)[0]);
+ eqps = await Promise.all(Array.from(gE('.eqp>[id]', 'all', doc)).map(async eqp => { try {
+ const id = eqp.id.match(/\d+/)[0];
+ const condition = 1 * json[id].d.match(/Condition: \d+ \/ \d+ \((\d+)%\)/)[1];
+ if (condition > threshold) {
+ return;
+ }
+ const after = $doc(await $ajax.insert(url, `select_item=${id}`));
+ return gE('.messagebox_error', )?.innerText ? undefined : json[id].t;
+ } catch (err) { console.error(err) } }));
+ } else {
+ const url = `?s=Bazaar&ss=am&screen=repair&filter=equipped`;
+ const doc = $doc(await $ajax.insert(url));
+ if (gE('#riddlecounter', doc) || gE('#battle_main', doc)) {
+ $async.logSwitch(arguments);
+ return undefined;
+ }
+ const token = gE('#equipform>input[name="postoken"]', doc).value;
+ eqps = await Promise.all(Array.from(gE('#equiplist>table>tbody>tr:not(.eqselall):not(.eqtplabel)', 'all', doc)).map(async eqp => { try {
+ const id = gE('input', eqp).value;
+ const condition = 1 * gE('td:last-child', eqp).textContent.replace('%', '');
+ if (condition > threshold) {
+ return;
+ }
+ const after = $doc(await $ajax.insert(url, `&eqids[]=${id}&postoken=${token}&replace_charms=on`));
+ return gE(`#e${id}`, after) ? gE('.lc', eqp).childNodes[2].textContent : undefined;
+ } catch (err) { console.error(err) } }));
+ }
+ eqps = eqps.filter(e=>e);
+ if (eqps.length) {
+ console.log('equips need repair:\n', eqps.join('\n '));
+ switch(option.lang * 1) {
+ case 0:
+ popup(`装备需要修理:\n${eqps.join('\n ')}`);
+ break
+ case 1:
+ popup(`裝備需要修理:\n${eqps.join('\n ')}`);
+ break
+ case 2:
+ default:
+ popup(`Equips need repair:\n${eqps.join('\n ')}`);
+ break
+ }
+ document.title = `[R!]` + document.title;
+ }
+ $async.logSwitch(arguments);
+ return !eqps.length;
+ } catch (err) { console.error(err) }; return false; }
- async function idleArena() { try { // 闲置竞技场
- let arena = getValue('arena', true);
- console.log('arena:', getValue('arena', true));
- if (arena.array.length === 0) {
- setTimeout(autoSwitchIsekai, (g('option').isekaiTime * (Math.random() * 20 + 90) / 100) * _1s);
- return;
- }
- logSwitchAsyncTask(arguments);
- const array = [...arena.array];
- let id;
- const RBundone = [];
- while (array.length > 0) {
- id = array.pop() * 1;
- id = isNaN(id) ? 'gr' : id;
- if(arena.arrayDone?.includes(id)){
- id = undefined;
- continue;
+ async function asyncCheckEquStorage() { try {
+ const option = g().option;
+ if (!option.equStorage) {
+ return true;
+ }
+ await waitPause();
+ $async.logSwitch(arguments);
+ let count;
+ if (hvVersion < 91) {
+ const url = `?s=Character&ss=in`;
+ const doc = $doc(await $ajax.insert(url));
+ if (gE('#riddlecounter', doc) || gE('#battle_main', doc)) {
+ $async.logSwitch(arguments);
+ return false;
+ }
+ count = gE('#eqinv_bot>div>div>div', doc).innerText.match(/: (\d+) \/ \d+/)[1];
+ } else {
+ const url = `?s=Bazaar&ss=am`;
+ const doc = $doc(await $ajax.insert(url));
+ if (gE('#riddlecounter', doc) || gE('#battle_main', doc)) {
+ $async.logSwitch(arguments);
+ return false;
+ }
+ count = gE('#equipblurb>table>tbody>tr>td:nth-child(2)', doc).innerText;
+ }
+ $async.logSwitch(arguments);
+ return count * 1 <= option.equStorageValue;
+ } catch (err) { console.error(err) }; return false; }
+
+ async function checkBattleReady(method, condition = {}) {
+ await waitPause();
+ if (condition.checkEncounter) {
+ const encounter = getEncounter();
+ if (encounter[0]?.url && !encounter[0]?.encountered) {
+ console.log(getEncounter());
+ return;
+ }
}
- if (id in arena.token) {
+ const option = g().option;
+ const stamina = getValue('stamina', true);
+ const [low, lowNR, cost, ratio] = [condition.staminaLow??option.staminaLow, option.staminaLowWithReNat??0, Math.round((condition.staminaCost??0) * 100) / 100, stamina.punish ? stamina.ratio??1 : 1]
+ const checked = await checkStamina(low, cost);
+ const [staminaChecked, stmNR] = [checked.checked, checked.stmNR];
+ const [neat, neatNR] = [stamina.current-low, stmNR-lowNR];
+ console.log(
+ 'stamina check succeed:', staminaChecked === 1, ... staminaChecked === -1 ? ['with nature recover', lowNR, 'stmNR:', stmNR, '(', ... neatNR>=0 ? ['+', neatNR] : ['-', -neatNR], ')'] : [],
+ '\nlow:', low, ... cost ? ['cost:', cost, ... stamina.punish ? ['*', ratio, '=', Math.round(cost*ratio*10000)/10000] : [], 'current:', stamina.current, '(', neat>=0?'+':'-', neat, ')'] : [],
+ '\nstamina:', stamina,
+ );
+ if (staminaChecked === 1) { // succeed
+ document.title = document.title.replace(`[S!]`, '');
+ return true;
+ }
+ if (staminaChecked === 0) { // failed currently
+ const now = time(0);
+ setTimeout(method, Math.floor(now / _1h + 1) * _1h - now);
+ // popup('Failed stamina check for now.');
+ if (!document.title.includes(`[S!]`)) {
+ document.title = `[S!]` + document.title;
+ }
+ } else { // case -1: // failed with nature recover
+ switch(option.lang * 1) {
+ case 0:
+ popup('当日精力不足(含自然恢复)');
+ break
+ case 1:
+ popup('當日精力不足(含自然恢復)');
+ break
+ case 2:
+ default:
+ popup('Failed stamina check with nature recover.');
+ break
+ }
+ document.title = `[S!!]` + document.title;
+ }
+ }
+
+ async function getCurrentStamina() { try {
+ // await waitPause();
+ $async.logSwitch(arguments);
+ const doc = $doc(await $ajax.insert(window.location.href));
+ if (gE('#riddlecounter', doc) || gE('#battle_main', doc)) {
+ $async.logSwitch(arguments);
+ return [ undefined, undefined ];
+ }
+ const current = gE('#stamina_readout .fc4.far>div', doc).textContent.match(/\d+/)[0] * 1;
+ const punish = !!gE('#stamina_readout .fc4.far', doc).parentNode.title;
+ $async.logSwitch(arguments);
+ return [current, punish ? punish : undefined];
+ } catch(e) { console.error(e); }}
+
+ async function checkStamina(low, cost) {
+ // await waitPause();
+ $async.logSwitch(arguments);
+ const stamina = getValue('stamina', true);
+ const option = g().option;
+ let now = time(0);
+ let hours = Math.floor(now / _1h);
+ let [current, punish] = await getCurrentStamina();
+ const stmNR = current + 24 - (hours % 24);
+ cost ??= 0;
+ if (punish && option.staminaRatio) {
+ cost *= stamina.ratio
+ }
+ const stmNRChecked = !cost || stmNR - cost >= option.staminaLowWithReNat;
+ const result = { checked: stmNRChecked ? (current - cost >= (low ?? option.staminaLow)) ? 1 : 0 : -1, stmNR: stmNR };
+ $async.logSwitch(arguments);
+ if (result.checked === 1 || isIsekai || !option.restoreStamina) return result;
+ const items = g().items;
+ $async.logSwitch(arguments);
+ if (!items) return result;
+ const recoverItems = { 11401: true, 11402: false }
+ const isPerk = stamina.perk?.includes(getCurrentUser());
+ for (let id in recoverItems) {
+ if (!items[id]) continue;
+ const recover = recoverItems[id] ? isPerk ? 20 : 10 : 5;
+ if (current + recover >= 100) continue; // check if overflow by (20 or 10) -> (5)
+ const recovered = gE('#stamina_readout .fc4.far>div', $doc(await $ajax.insert(window.location.href, 'recover=stamina'))).textContent.match(/\d+/)[0] * 1;
+ goto();
break;
}
- if (id >= 105) {
- arena.token = (await updateArena(true)).token;
+ $async.logSwitch(arguments);
+ return result;
+ }
+
+ async function updateEncounter(engage) { try {
+ if (!g().option.encounter && !g().option.encounterDisplay) {
+ console.log("skip encounter check");
+ return false;
+ }
+ // await waitPause();
+ // $async.logSwitch(arguments);
+ const encounter = getEncounter();
+ const count = encounter.filter(e => e.url).length;
+ const option = g().option;
+ const now = time(0);
+ const last = encounter[0]?.time ?? getValue('lastEH', true) ?? 0; // 上次遭遇 或 上次打开EH 或 0
+ let cd;
+ if (encounter.filter(e => e.url && (e.encountered || (time(0) - e.time >= 30 * _1m))).length >= 24) {
+ cd = Math.floor(encounter[0].time / _1d + 1) * _1d - now;
+ } else if (!last) {
+ cd = 0;
+ } else {
+ cd = _1h / 2 + last - now;
+ }
+ cd = Math.max(0, cd);
+ const ui = gE('.encounterUI') ?? (() => {
+ const ui = (gE('.hvAAPauseUI') ?? gE('body')).appendChild(cE('a'));
+ ui.className = 'encounterUI';
+ ui.title = `${time(3, last)}\nEncounter Time: ${count}`;
+ if (!gE('#battle_main')) {
+ ui.href = 'https://e-hentai.org/news.php?encounter';
+ }
+ return ui;
+ })();
+ const waitCD = option.encounterWaitCD;
+ const missed = count - encounter.filter(e => e.encountered && e.url).length;
+ if (count === 24) {
+ ui.style.cssText += 'color:orange!important;';
+ } else if (cd <= waitCD) {
+ ui.style.cssText += 'color:red!important;';
+ } else {
+ ui.style.cssText += 'color:unset!important;';
+ }
+ ui.innerHTML = `${formatTime(cd).slice(0, 2).map(cdi => cdi.toString().padStart(2, '0')).join(`:`)}[${encounter.length ? (count >= 24 ? `☯` : count) : `✪`}${missed ? `-${missed}` : ``}]`;
+ if (engage) {
+ if (!cd) {
+ await waitPause();
+ onEncounter();
+ // $async.logSwitch(arguments);
+ return true;
+ }
+ if (cd < 30 * _1m && encounter[0]?.url && !encounter[0].encountered) {
+ await waitPause();
+ $ajax.openNoFetch(encounter[0].url);
+ // $async.logSwitch(arguments);
+ return true;
+ }
+ }
+ let interval = cd > _1h ? _1m : (!option.encounterQuickCheck || cd > _1m) ? _1s : 80;
+ interval = (option.encounterQuickCheck && cd > _1m) ? (interval - cd % interval) / 4 : interval; // 让倒计时显示更平滑
+ setTimeout(() => updateEncounter(engage), interval);
+ // $async.logSwitch(arguments);
+ return engage && cd <= waitCD;
+ } catch (err) { console.error(err) } }
+
+ async function onEncounter() { try {
+ const option = g().option;
+ while (
+ !(await $ajax.insert(location))
+ || (option.checkURLBeforeNewRound && !(await $ajax.insert(option.checkURLBeforeNewRound)))
+ ) { // perhaps network connect not available
+ await pauseAsync(option.checkURLBeforeNewRoundRetry);
+ }
+ $async.logSwitchStrict('updateEncounter', true);
+ if (getValue('disabled') || getValue('battle') || !await checkBattleReady(onEncounter, { staminaLow: option.staminaEncounter })) {
+ $async.logSwitchStrict('updateEncounter', false);
+ return;
+ }
+ setEncounter(getEncounter()); // 离开页面前保存
+ if (!window.top.location.href.endsWith(`?s=Battle`)) {
+ setValue('lastUrl', window.top.location.href);
+ }
+ if (option.enchantEncounter) await asyncCheckEnchant();
+ $ajax.openNoFetch('https://e-hentai.org/news.php?encounter');
+ $async.logSwitchStrict('updateEncounter', false);
+ } catch (err) { console.error(err) } }
+
+ async function startUpdateArena(idleStart, startIdleArena = true) { try {
+ $async.logSwitchStrict('startUpdateArena', true);
+ const now = time(0);
+ if (!idleStart) {
+ await updateArena();
+ }
+ let timeout = g().option.idleArenaTime * _1s;
+ if (idleStart) {
+ timeout -= time(0) - idleStart;
+ }
+ if (startIdleArena) {
+ setTimeout(idleArena, timeout);
+ }
+ const last = getValue('arena', true)?.date ?? now;
+ setTimeout(startUpdateArena, Math.max(0, Math.floor(last / _1d + 1) * _1d - now));
+ $async.logSwitchStrict('startUpdateArena', false);
+ } catch (err) { console.error(err) } }
+
+ async function updateArena(forceUpdateToken = false) { try {
+ await waitPause();
+ $async.logSwitch(arguments);
+ let arena = getValue('arena', true) ?? {};
+ const isToday = arena.date && time(2, arena.date) === time(2);
+ if (forceUpdateToken || !isToday || !arena.isOptionUpdated) {
+ arena.token = {};
+ await Promise.all(['gr', 'ar', 'rb'].map(s => (async site => {
+ try {
+ const doc = $doc(await $ajax.insert(`?s=Battle&ss=${site}`));
+ getStartBattleButtons(doc, site).forEach(btn => {
+ if (btn.cleared) {
+ arena.token[btn.id] = btn.token ?? null;
+ return;
+ }
+ delete arena.token[btn.id];
+ });
+ } catch (err) { console.error(err) }
+ })(s)));
+ }
+
+ if (!isToday) {
+ arena.date = time(0);
+ arena.gr = g().option.idleArenaGrTime;
+ arena.arrayDone = [];
+ }
+ if (!isToday || !arena.isOptionUpdated) {
+ arena.array = g().option.idleArenaValue?.split(',') ?? [];
+ arena.array.reverse();
+ }
+ arena.arrayDone = arena.arrayDone.filter(id => id === 'gr' || !Object.keys(arena.token).includes(id.toString()));
+ $async.logSwitch(arguments);
+ return setValue('arena', arena);
+ } catch (err) { console.error(err) } }
+
+ async function idleArena() { try { // 闲置竞技场
+ let id;
+ let arena = getValue('arena', true);
+ const option = g().option;
+ const writeArenaStart = function () {
+ console.log('Arena Start', id);
+ document.title = _alert(-1, '闲置竞技场开始', '閒置競技場開始', 'Idle Arena start');
+ if (id !== 'gr') {
+ arena.arrayDone.push(id);
+ } else {
+ arena.gr--;
+ }
+ setValue('arena', arena);
+ }
+ if (arena.array.length === 0) {
+ setTimeout(autoSwitchIsekai, (option.isekaiTime * (Math.random() * 20 + 90) / 100) * _1s);
+ return;
+ }
+ $async.logSwitch(arguments);
+ const array = [...arena.array];
+ const RBundone = [];
+ while (array.length > 0) {
+ id = array.pop() * 1;
+ id = isNaN(id) ? 'gr' : id;
+ if (arena.arrayDone?.includes(id)) {
+ id = undefined;
+ continue;
+ }
if (id in arena.token) {
break;
}
+ if (id >= 105) {
+ arena.token = (await updateArena(true)).token;
+ if (id in arena.token) {
+ break;
+ }
+ }
+ id = undefined;
}
- id = undefined;
- }
- if (!id) {
- setValue('arena', arena);
- logSwitchAsyncTask(arguments);
- return;
- }
- let staminaCost = {
- 1: 2, 3: 4, 5: 6, 8: 8, 9: 10,
- 11: 12, 12: 15, 13: 20, 15: 25, 16: 30,
- 17: 35, 19: 40, 20: 45, 21: 50, 23: 55,
- 24: 60, 26: 65, 27: 70, 28: 75, 29: 80,
- 32: 85, 33: 90, 34: 95, 35: 100,
- 105: 1, 106: 1, 107: 1, 108: 1, 109: 1, 110: 1, 111: 1, 112: 1,
- gr: arena.gr
- }
- let stamina = getValue('stamina');
- const lastTime = getValue('staminaTime');
- let timeNow = Math.floor(time(0) / 1000 / 60 / 60);
- stamina += lastTime ? timeNow - lastTime : 0;
- for (let key in staminaCost) {
- staminaCost[key] *= (isIsekai ? 2 : 1) * (stamina >= 60 ? 0.03 : 0.02)
- }
- staminaCost.gr += 1
-
- let href, cost;
- let token = arena.token[id];
- const key = id;
- if (key === 'gr') {
- if (arena.gr <= 0) {
+ if (!id) {
+ console.log('No Arena Id Available', arena);
setValue('arena', arena);
- idleArena();
- arena.arrayDone.push('gr');
+ $async.logSwitch(arguments);
return;
}
- arena.gr--;
- href = 'gr';
- key = 1;
- cost = staminaCost.gr;
- } else if (key >= 105) {
- href = 'rb';
- } else if (key >= 19) {
- href = 'ar&page=2';
- } else {
- href = 'ar';
- }
- cost ??= staminaCost[key];
- if (!checkBattleReady(idleArena, { staminaCost: cost, checkEncounter: true })) {
- logSwitchAsyncTask(arguments);
- return;
- }
- document.title = _alert(-1, '闲置竞技场开始', '閒置競技場開始', 'Idle Arena start');
- if(key !== 'gr'){
- arena.arrayDone.push(key);
- }
- setValue('arena', arena);
- $ajax.open(`?s=Battle&ss=${href}`, `initid=${String(key)}&inittoken=${token}`);
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
-
- // 战斗中//
- function onBattle() { // 主程序
- let battle = getValue('battle', true);
- if (!battle || !battle.roundAll) { // 修复因多个页面/世界同时读写造成缓存数据异常的情况
- battle = JSON.parse(JSON.stringify(g('battle')));
- battle.monsterStatus = battle.monsterStatus.map(ms => {
- return {
- order: ms.order,
- hp: ms.hp
- }
- })
- battle.monsterStatus.sort(objArrSort('order'));
- };
- Debug.log('onBattle', `\n`, JSON.stringify(battle, null, 4));
- //人物状态
- if (gE('#vbh')) {
- g('hp', gE('#vbh>div>img').offsetWidth / 500 * 100);
- g('mp', gE('#vbm>div>img').offsetWidth / 210 * 100);
- g('sp', gE('#vbs>div>img').offsetWidth / 210 * 100);
- g('oc', gE('#vcp>div>div') ? (gE('#vcp>div>div', 'all').length - gE('#vcp>div>div#vcr', 'all').length) * 25 : 0);
- } else {
- g('hp', gE('#dvbh>div>img').offsetWidth / 418 * 100);
- g('mp', gE('#dvbm>div>img').offsetWidth / 418 * 100);
- g('sp', gE('#dvbs>div>img').offsetWidth / 418 * 100);
- g('oc', gE('#dvrc').childNodes[0].textContent * 1);
- }
-
- // 战斗战况
- if (!gE('.hvAALog')) {
- const div = gE('#hvAABox2').appendChild(cE('div'));
- div.className = 'hvAALog';
- }
- const status = [
- ' 物理物理Physical',
- ' 火火Fire',
- ' 冰冰Cold',
- ' 雷雷Elec',
- ' 风風Wind',
- ' 圣聖Divine',
- ' 暗暗Forbidden',
- ];
- function getBattleTypeDisplay(isTitle) {
- const battleInfoList = {
- 'gr': {
- name: ['压榨', '壓榨', 'Grindfest'],
- title: 'GF',
- },
- 'iw': {
- name: ['道具', '道具', 'Item World'],
- title: 'IW',
- },
- 'ar': {
- name: ['竞技', '競技', 'Arena'],
- title: 'AR',
- list: [
- ['第一滴血', '第一滴血', 'First Blood', 1, 2],
- ['经验曲线', '經驗曲綫', 'Learning Curves', 10, 4],
- ['毕业典礼', '畢業典禮', 'Graduation', 20, 6],
- ['荒凉之路', '荒涼之路', 'Road Less Traveled', 30, 8],
- ['浪迹天涯', '浪跡天涯', 'A Rolling Stone', 40, 10],
- ['鲜肉一族', '鮮肉一族', 'Fresh Meat', 50, 12],
- ['乌云密布', '烏雲密佈', 'Dark Skies', 60, 15],
- ['风暴成形', '風暴成形', 'Growing Storm', 70, 20],
- ['力量流失', '力量流失', 'Power Flux', 80, 25],
- ['杀戮地带', '殺戮地帶', 'Killzone', 90, 30],
- ['最终阶段', '最終階段', 'Endgame', 100, 35],
- ['无尽旅程', '無盡旅程', 'Longest Journey', 110, 40],
- ['梦陨之时', '夢隕之時', 'Dreamfall', 120, 45],
- ['流亡之途', '流亡之途', 'Exile', 130, 50],
- ['封印之力', '封印之力', 'Sealed Power', 140, 55],
- ['崭新之翼', '嶄新之翼', 'New Wings', 150, 60],
- ['弑神之路', '弑神之路', 'To Kill a God', 165, 65],
- ['死亡前夜', '死亡前夜', 'Eve of Death', 180, 70],
- ['命运三女神与树', '命運三女神與樹', 'The Trio and the Tree', 200, 75],
- ['世界末日', '世界末日', 'End of Days', 225, 80],
- ['永恒黑暗', '永恆黑暗', 'Eternal Darkness', 250, 85],
- ['与龙共舞', '與龍之舞', 'A Dance with Dragons', 300, 90],
- ['额外游戏内容', '額外游戲内容', 'Post Game Content', 400, 95],
- ['神秘小马领域', '神秘小馬領域', 'Secret Pony Level', 500, 100],
- ],
- condition: (bt) => bt[4] === battle.roundAll,
- content: (bt) => bt[3],
- },
- 'rb': {
- name: ['浴血', '浴血', 'Ring of Blood'],
- title: 'RB',
- list: [
- ['九死一树', '九死一樹', 'Triple Trio and the Tree', 250, 'Yggdrasil'],
- ['飞天意面怪', '飛行義大利麵怪物', 'Flying Spaghetti Monster', 200],
- ['隐形粉红独角兽', '隱形粉紅獨角獸', 'Invisible Pink Unicorn', 150],
- ['现实生活', '現實生活', 'Real Life', 100],
- ['长门有希', '長門有希', 'Yuki Nagato', 75],
- ['朝仓凉子', '朝倉涼子', 'Ryouko Asakura', 75],
- ['朝比奈实玖瑠', '朝比奈實玖瑠', 'Mikuru Asahina', 75],
- ['泉此方', '泉此方', 'Konata', 75],
- ],
- condition: (bt) => monsterNames.indexOf(bt[4] ?? bt[2]) !== -1,
- content: (bt) => bt[3],
- },
- 'ba': {
- name: ['遭遇', '遭遇', 'Random Encounter'],
- title: 'BA',
- content: (_) => getEncounter().filter(e => e.encountered).length,
- },
- 'tw': {
- name: ['塔楼', '塔樓', 'The Tower'],
- title: 'TW',
- list: [
- ['PFUDOR×20', 'PFUDOR×20', 'PFUDOR×20', 40],
- ['IWBTH×15', 'IWBTH×15', 'IWBTH×15', 34],
- ['任天堂×10', '任天堂×10', 'Nintendo×10', 27],
- ['地狱×7', '地獄×7', 'Hell×7', 20],
- ['噩梦×4', '噩夢×4', 'Nightmare×4', 14],
- ['困难×2', '困難×2', 'Hard×2', 7],
- ['普通×1', '普通×1', 'Normal×1', 1],
- ],
- condition: (bt) => bt[3] && bt[3] <= battle.tower,
- content: (_) => battle.tower,
- end: battle.tower > 40 ? `+${(battle.tower - 40) * 5}%DMG&HP` : '',
- }
- }
- const type = battle.roundType;
- let subtype, title;
- const monsterNames = Array.from(gE('div.btm3>div>div', 'all')).map(monster => monster.innerHTML);
- const lang = g('lang') * 1;
- const info = battleInfoList[type];
- switch (type) {
- case 'ar':
- case 'rb':
- case 'tw':
- case 'ba':
- for (let sub of (info.list ?? [[]])) {
- if (info.condition && !info.condition(sub)) {
- continue;
- }
- title = `${info.title}${info.content(sub)}`;
- if (!sub[lang]) {
+ let staminaCost = {
+ 1: 2, 3: 4, 5: 6, 8: 8, 9: 10,
+ 11: 12, 12: 15, 13: 20, 15: 25, 16: 30,
+ 17: 35, 19: 40, 20: 45, 21: 50, 23: 55,
+ 24: 60, 26: 65, 27: 70, 28: 75, 29: 80,
+ 32: 85, 33: 90, 34: 95, 35: 100,
+ 105: 1, 106: 1, 107: 1, 108: 1, 109: 1, 110: 1, 111: 1, 112: 1,
+ gr: 0
+ }
+ let stamina = getValue('stamina', true);
+ [stamina.current, stamina.punish] = await getCurrentStamina();
+ stamina.time = time(0);
+ for (let id in staminaCost) {
+ staminaCost[id] *= (isIsekai ? 2 : 1) * (stamina.current >= 60 ? 0.03 : 0.02);
+ }
+
+ let query;
+ if (id !== 'gr') {
+ query = id >= 105 ? 'rb' : 'ar';
+ } else {
+ if (arena.gr <= 0) {
+ setValue('arena', arena);
+ idleArena();
+ arena.arrayDone.push('gr');
+ return;
+ }
+ query = 'gr';
+ }
+ query = `?s=Battle&ss=${query}`;
+ if (id === 'gr' && ((option.checkSupplyGF && !checkSupply(true)) || (option.repairValueGF && !await asyncCheckRepair(true)))) {
+ console.log('Check gr Battle Ready Failed in supply/repair', 'id:', id, arena);
+ $async.logSwitch(arguments);
+ return;
+ }
+ const cost = staminaCost[id];
+ if (!await checkBattleReady(idleArena, { staminaCost: cost, checkEncounter: option.encounter, staminaLow: id === 'gr' ? option.staminaGrindFest : undefined })) {
+ console.log('Check Battle Ready Failed', 'id:', id, arena);
+ $async.logSwitch(arguments);
+ return;
+ }
+ let token = arena.token[id];
+ if (hvVersion < 91) {
+ token = `&inittoken=${token}`;
+ } else {
+ token = `&postoken=${gE('#initform>input[name="postoken"]', $doc(await $ajax.insert(query))).value}`;
+ }
+ await waitPause();
+ writeArenaStart();
+ while(option.checkURLBeforeNewRound && !(await $ajax.insert(option.checkURLBeforeNewRound))) {
+ await pauseAsync(option.checkURLBeforeNewRoundRetry);
+ }
+ await asyncCheckEnchant(id === 'gr');
+ while(!(await $ajax.insert(query, `initid=${id === 'gr' ? 1 : id}${token}`))) {
+ await pauseAsync(option.checkURLBeforeNewRoundRetry);
+ }
+ stamina.lastCost = id === 'gr' ? undefined : cost;
+ setValue('stamina', stamina);
+ if (option.altBattleFirst && await $ajax.insert(location.replace('hentaiverse.org', 'alt.hentaiverse.org').replace('alt.alt', 'alt'))) {
+ console.log('Arena Fetch Done.', 'altBattleFirst:', option.altBattleFirst, 'Arena goto alt', arena);
+ gotoAlt(true);
+ } else {
+ console.log('Arena Fetch Done.', 'altBattleFirst:', option.altBattleFirst, 'Arena goto', arena);
+ goto();
+ }
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err) } }
+
+ // 战斗中//
+ function onBattleRound() { // 主程序
+ if (!gE('#battle_main')) return;
+ let battle = getValue('battle', true);
+ if (!battle || !battle.roundAll) { // 修复因多个页面/世界同时读写造成缓存数据异常的情况
+ battle = JSON.parse(JSON.stringify(g().battle));
+ battle.monsterStatus = battle.monsterStatus.map(ms => {
+ return {
+ order: ms.order,
+ hp: ms.hp
+ }
+ });
+ battle.monsterStatus.sort(objArrSort('order'));
+ };
+ $debug.log('onBattle', `\n`, battle);
+ //人物状态
+ if (gE('#vbh')) {
+ g('hp', gE('#vbh>div>img').offsetWidth / 496 * 100);
+ g('mp', gE('#vbm>div>img').offsetWidth / 207 * 100);
+ g('sp', gE('#vbs>div>img').offsetWidth / 207 * 100);
+ g('oc', gE('#vcp>div>div') ? (gE('#vcp>div>div', 'all').length - gE('#vcp>div>div#vcr', 'all').length) * 25 : 0);
+ } else {
+ g('hp', gE('#dvbh>div>img').offsetWidth / 418 * 100);
+ g('mp', gE('#dvbm>div>img').offsetWidth / 418 * 100);
+ g('sp', gE('#dvbs>div>img').offsetWidth / 418 * 100);
+ g('oc', gE('#dvrc').childNodes[0].textContent * 1);
+ }
+
+ // 战斗战况
+ if (!gE('.hvAALog')) {
+ const div = gE('#hvAABox2').appendChild(cE('div'));
+ div.className = 'hvAALog';
+ }
+
+ function getBattleTypeDisplay(isTitle) {
+ const battleInfoList = {
+ 'gr': {
+ name: ['压榨', '壓榨', 'Grindfest'],
+ title: 'GF',
+ },
+ 'iw': {
+ name: ['道具', '道具', 'Item World'],
+ title: 'IW',
+ },
+ 'ar': {
+ name: ['竞技', '競技', 'Arena'],
+ title: 'AR',
+ list: [
+ ['第一滴血', '第一滴血', 'First Blood', 1, 2],
+ ['经验曲线', '經驗曲綫', 'Learning Curves', 10, 4],
+ ['毕业典礼', '畢業典禮', 'Graduation', 20, 6],
+ ['荒凉之路', '荒涼之路', 'Road Less Traveled', 30, 8],
+ ['浪迹天涯', '浪跡天涯', 'A Rolling Stone', 40, 10],
+ ['鲜肉一族', '鮮肉一族', 'Fresh Meat', 50, 12],
+ ['乌云密布', '烏雲密佈', 'Dark Skies', 60, 15],
+ ['风暴成形', '風暴成形', 'Growing Storm', 70, 20],
+ ['力量流失', '力量流失', 'Power Flux', 80, 25],
+ ['杀戮地带', '殺戮地帶', 'Killzone', 90, 30],
+ ['最终阶段', '最終階段', 'Endgame', 100, 35],
+ ['无尽旅程', '無盡旅程', 'Longest Journey', 110, 40],
+ ['梦陨之时', '夢隕之時', 'Dreamfall', 120, 45],
+ ['流亡之途', '流亡之途', 'Exile', 130, 50],
+ ['封印之力', '封印之力', 'Sealed Power', 140, 55],
+ ['崭新之翼', '嶄新之翼', 'New Wings', 150, 60],
+ ['弑神之路', '弑神之路', 'To Kill a God', 165, 65],
+ ['死亡前夜', '死亡前夜', 'Eve of Death', 180, 70],
+ ['命运三女神与树', '命運三女神與樹', 'The Trio and the Tree', 200, 75],
+ ['世界末日', '世界末日', 'End of Days', 225, 80],
+ ['永恒黑暗', '永恆黑暗', 'Eternal Darkness', 250, 85],
+ ['与龙共舞', '與龍之舞', 'A Dance with Dragons', 300, 90],
+ ['额外游戏内容', '額外游戲内容', 'Post Game Content', 400, 95],
+ ['神秘小马领域', '神秘小馬領域', 'Secret Pony Level', 500, 100],
+ ],
+ condition: (bt) => bt[4] === battle.roundAll,
+ content: (bt) => bt[3],
+ },
+ 'rb': {
+ name: ['浴血', '浴血', 'Ring of Blood'],
+ title: 'RB',
+ list: [
+ ['九死一树', '九死一樹', 'Triple Trio and the Tree', 250, 'Yggdrasil'],
+ ['飞天意面怪', '飛行義大利麵怪物', 'Flying Spaghetti Monster', 200],
+ ['隐形粉红独角兽', '隱形粉紅獨角獸', 'Invisible Pink Unicorn', 150],
+ ['现实生活', '現實生活', 'Real Life', 100],
+ ['长门有希', '長門有希', 'Yuki Nagato', 75],
+ ['朝仓凉子', '朝倉涼子', 'Ryouko Asakura', 75],
+ ['朝比奈实玖瑠', '朝比奈實玖瑠', 'Mikuru Asahina', 75],
+ ['泉此方', '泉此方', 'Konata', 75],
+ ],
+ condition: (bt) => monsterNames.indexOf(bt[4] ?? bt[2]) !== -1,
+ content: (bt) => bt[3],
+ },
+ 'ba': {
+ name: ['遭遇', '遭遇', 'Random Encounter'],
+ title: 'BA',
+ content: (_) => getEncounter().filter(e => e.encountered).length,
+ },
+ 'tw': {
+ name: ['塔楼', '塔樓', 'The Tower'],
+ title: 'TW',
+ list: [
+ ['PFUDOR×20', 'PFUDOR×20', 'PFUDOR×20', 40],
+ ['IWBTH×15', 'IWBTH×15', 'IWBTH×15', 34],
+ ['任天堂×10', '任天堂×10', 'Nintendo×10', 27],
+ ['地狱×7', '地獄×7', 'Hell×7', 20],
+ ['噩梦×4', '噩夢×4', 'Nightmare×4', 14],
+ ['困难×2', '困難×2', 'Hard×2', 7],
+ ['普通×1', '普通×1', 'Normal×1', 1],
+ ],
+ condition: (bt) => bt[3] && bt[3] <= battle.tower,
+ content: (_) => battle.tower,
+ end: battle.tower > 40 ? `+${(battle.tower - 40) * 5}%DMG&HP` : '',
+ }
+ }
+ const type = battle.roundType;
+ let subtype, title;
+ const monsterNames = Array.from(gE(`${monsterStateKeys.name}>div>div`, 'all')).map(monster => monster.innerHTML);
+ const lang = g().lang * 1;
+ const info = battleInfoList[type];
+ switch (type) {
+ case 'ar':
+ case 'rb':
+ case 'tw':
+ case 'ba':
+ for (let sub of (info.list ?? [[]])) {
+ if (info.condition && !info.condition(sub)) {
+ continue;
+ }
+ title = `${info.title}${info.content(sub)}`;
+ if (!sub[lang]) {
+ break;
+ }
+ subtype = `${sub[lang] ? ` ${sub[lang]}` : ``}${info.end ? ` ${info.end}` : ``}`;
break;
}
- subtype = `${sub[lang] ? ` ${sub[lang]}` : ``}${info.end ? ` ${info.end}` : ``}`;
break;
- }
- break;
- case 'iw':
- case 'gr':
- title = `${info.title}`;
- break;
- default:
- break;
- }
- return isTitle ? title : `${(info?.name ?? ['未知', '未知', 'Unknown'])[lang]}:[${title}]${subtype ?? ''}`;
- }
-
- const currentTurn = (battle.turn ?? 0) + 1;
-
- gE('.hvAALog').innerHTML = [
- ` 攻击模式攻擊模式Attack Mode: ${status[g('attackStatus')]}`,
- `${isIsekai ? ' 异世界異世界Isekai' : ' 恒定世界恆定世界Persistent'}`, // 战役模式显示
- `${getBattleTypeDisplay()}`, // 战役模式显示
- `R${battle.roundNow}/${battle.roundAll}:T${currentTurn}`,
- `TPS: ${g('runSpeed')}`,
- ` 敌人敌人Monsters: ${g('monsterAlive')}/${g('monsterAll')}`,
- ].join(` `);
- if (!battle.roundAll) {
- pauseChange();
- Debug.shiftLog();
- }
- document.title = `${getBattleTypeDisplay(true)}:R${battle.roundNow}/${battle.roundAll}:T${currentTurn}@${g('runSpeed')}tps,${g('monsterAlive')}/${g('monsterAll')}`;
- setValue('battle', battle);
- if (!battle.monsterStatus || battle.monsterStatus.length !== g('monsterAll')) {
- fixMonsterStatus();
- }
- countMonsterHP();
- displayMonsterWeight();
- displayPlayStatePercentage();
-
- if (getValue('disabled')) { // 如果禁用
- document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused');
- gE('#hvAABox2>button').innerHTML = ' 继续繼續Continue';
- return;
- }
- battle = getValue('battle', true);
- g('battle').turn = currentTurn;
- battle.turn = currentTurn;
- setValue('battle', battle);
+ case 'iw':
+ case 'gr':
+ title = `${info.title}`;
+ break;
+ default:
+ break;
+ }
+ return isTitle ? title : `${(info?.name ?? ['未知', '未知', 'Unknown'])[lang]}:[${title}]${subtype ?? ''}`;
+ }
- killBug(); // 解决 HentaiVerse 可能出现的 bug
+ const currentTurn = (battle.turn ?? 0) + 1;
- if (g('option').autoFlee && checkCondition(g('option').fleeCondition)) {
- gE('1001').click();
- SetExitBattleTimeout('Flee');
- return;
- }
- var taskList = {
- 'Pause': autoPause,
- 'Rec': autoRecover,
- 'Def': autoDefend,
- 'Scroll': useScroll,
- 'Channel': useChannelSkill,
- 'Buff': useBuffSkill,
- 'Infus': useInfusions,
- 'Debuff': useDeSkill,
- 'Focus': autoFocus,
- 'SS': autoSS,
- 'Skill': autoSkill,
- 'Atk': attack,
- };
- const names = g('option').battleOrderName?.split(',') ?? [];
- for (let i = 0; i < names.length; i++) {
- if(taskList[names[i]]()){
- return;
+ gE('.hvAALog').innerHTML = [
+ ` 攻击模式攻擊模式Attack Mode: ${attackStatusType[g().attackStatus]}`,
+ `${isIsekai ? ' 异世界異世界Isekai' : ' 恒定世界恆定世界Persistent'}`, // 战役模式显示
+ `${getBattleTypeDisplay()}`, // 战役模式显示
+ `R${battle.roundNow}/${battle.roundAll}:T${currentTurn}`,
+ `TPS: ${g().runSpeed}`,
+ ` 敌人敌人Monsters: ${g().monsterAlive}/${g().monsterAll}`,
+ ].join(` `);
+ if (!battle.roundAll) {
+ pauseChange();
+ $debug.shiftLog();
}
- delete taskList[names[i]];
- }
- for (let name in taskList) {
- if (taskList[name]()) {
- return;
+ document.title = `${getBattleTypeDisplay(true)}:R${battle.roundNow}/${battle.roundAll}:T${currentTurn}@${g().runSpeed}tps,${g().monsterAlive}/${g().monsterAll}`;
+ setValue('battle', battle);
+ if (!battle.monsterStatus || battle.monsterStatus.length !== g().monsterAll) {
+ fixMonsterStatus();
}
- }
- }
+ countMonsterHP();
+ displayMonsterWeight();
+ displayPlayStatePercentage();
- function getMonsterID(s) {
- if (s.order !== undefined) {
- return (s.order + 1) % 10;
- } // case is monsterStatus
- return (s + 1) % 10; // case is order
- }
+ if (getValue('disabled')) { // 如果禁用
+ document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused');
+ gE('#hvAABox2>button').innerHTML = ` 继续繼續Continue${(g().option.pauseHotkey && g().option.pauseHotkeyStr) ? `(${g().option.pauseHotkeyStr})` : '' }`;
+ return;
+ }
+ battle = getValue('battle', true);
+ g().battle.turn = currentTurn;
+ battle.turn = currentTurn;
+ setValue('battle', battle);
+ killBug(); // 解决 HentaiVerse 可能出现的 bug
- /**
- * 按照技能范围,获取包含原目标且范围内最终权重(finweight)之和最低的范围的中心目标
- * @param {int} id id from g('battle').monsterStatus.sort(objArrSort('finWeight'));
- * @param {int} range radius, 0 for single-target and all-targets, 1 for treble-targets, ..., n for (2n+1) targets
- * @param {(target) => bool} excludeCondition target with id
- * @returns
- */
- function getRangeCenterID(target, range = undefined, isWeaponAttack = false, excludeCondition = undefined) {
- if (!range) {
- return getMonsterID(target);
- }
- const centralExtraWeight = -1 * Math.log10(1 + (isWeaponAttack ? (g('option').centralExtraRatio / 100) ?? 0 : 0));
- let order = target.order;
- let newOrder = order;
- // sort by order to fix id
- let msTemp = JSON.parse(JSON.stringify(g('battle').monsterStatus));
- msTemp.sort(objArrSort('order'));
- let unreachableWeight = g('option').unreachableWeight;
- let minRank;
- for (let i = order - range; i <= order + range; i++) {
- if (i < 0 || i >= msTemp.length || msTemp[i].isDead) {
- continue; // 无法选中
+ if (g().option.autoFlee && checkCondition(g().option.fleeCondition)) {
+ gE('1001').click();
+ setExitBattleTimeout('Flee');
+ return;
}
- let rank = 0;
- for (let j = i - range; j <= (i + range); j++) {
- let cew = j === i ? centralExtraWeight : 0; // cew <= 0, 增加未命中权重,降低命中权重
- let mon = msTemp[j];
- if (j < 0 || j >= msTemp.length // 超出范围
- || mon.isDead // 死亡目标
- || (excludeCondition && excludeCondition(mon))) { // 特殊排除判定
- rank += unreachableWeight - cew;
- continue;
+ const taskList = {
+ 'Cure': ()=>autoRecover(true),
+ 'Pause': autoPause,
+ 'SSDisable': ()=>autoSS(true),
+ 'Rec': ()=>autoRecover(false),
+ 'Scroll': useScroll,
+ 'Infus': useInfusions,
+ 'Def': autoDefend,
+ 'Channel': useChannelSkill,
+ 'Buff': useBuffSkill,
+ 'Debuff': useDeSkill,
+ 'Focus': autoFocus,
+ 'SS': ()=>autoSS(false),
+ 'Skill': autoSkill,
+ 'Atk': attack,
+ };
+ const option = g().option;
+ const names = option.battleOrderDefaultOnly ? [] : option.battleOrderName?.split(',') ?? [];
+ for (let i = 0; i < names.length; i++) {
+ if (taskList[names[i]]()) {
+ onStepInDone();
+ return;
}
- rank += mon.finWeight + cew; // 中心目标会受到副手及冲击攻击时,相当于有效生命值降低
+ delete taskList[names[i]];
}
- if (rank < minRank) {
- newOrder = i;
+ for (let name in taskList) {
+ if (taskList[name]()) {
+ onStepInDone();
+ return;
+ }
}
}
- return getMonsterID(newOrder);
- }
- function autoPause() {
- if (g('option').autoPause && checkCondition(g('option').pauseCondition)) {
- pauseChange();
- return true;
+ function getBuffTurnFromImg(buff) {
+ let duration = buff?.getAttribute('onmouseover').match(/\(.*,.*,(\s*)(.*?)\)$/)[2];
+ if (!duration) {
+ duration = 0;
+ } else if (['permanent', '-', "'-'"].includes(duration)) {
+ duration = Infinity;
+ } else {
+ duration *= 1;
+ }
+ return duration;
}
- return false;
- }
- function autoDefend() {
- if (g('option').defend && checkCondition(g('option').defendCondition)) {
- gE('#ckey_defend').click();
- return true;
+ function getMonsterID(s) {
+ if (s.order !== undefined) {
+ return (s.order + 1) % 10;
+ } // case is monsterStatus
+ return (s + 1) % 10; // case is order
}
- return false;
- }
- function pauseChange() { // 暂停状态更改
- if (getValue('disabled')) {
- if (gE('.pauseChange')) {
- gE('.pauseChange').innerHTML = '暂停暫停Pause';
- }
- document.title = getValue('disabled');
- delValue(0);
- if (!gE('#navbar')) { // in battle
- onBattle();
- }
- } else {
- if (gE('.pauseChange')) {
- gE('.pauseChange').innerHTML = '继续繼續Continue';
- }
- setValue('disabled', document.title);
- document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused');
+ function getMonster(id) {
+ return gE(`#mkey_${id % 10}`);
}
- }
- function SetExitBattleTimeout(alarm){
- setAlarm(alarm);
- if(alarm === 'SkipDefeated') return;
- delValue(1);
- if(g('option').ExitBattleWaitTime) {
- setTimeout(() => {
- $ajax.open(getValue('lastHref'));
- }, g('option').ExitBattleWaitTime * _1s);
- return;
+ function getBuff(buff, id) {
+ if (buff?.match(`^{.*}$`)) {
+ for (const b of buff.replace(/[\{\}\s]/g, '').split(',')) {
+ const found = getBuff(b, id);
+ if (found) return found;
+ }
+ return undefined;
+ }
+ if (id === undefined) {
+ return gE(`#pane_effects>img[src*="/${buff}"]`) ?? gE(`#pane_effects>img[src*="_${buff}"]`);
+ }
+ return gE(`${monsterStateKeys.buffs}>img[src*="/${buff}"]`, getMonster(id)) ?? gE(`${monsterStateKeys.buffs}>img[src*="_${buff}"]`, getMonster(id));
+ }
+
+ function isOn(id) { // 是否可以施放技能/使用物品
+ if (id * 1 > 10000) { // 使用物品
+ return gE(`.bti3>div[onmouseover*="(${id})"]`);
+ } // 施放技能
+ return gE(id) && (gE(id).style.opacity * 1 !== 0.5);
+ }
+
+ /**
+ * 按照技能范围,获取包含原目标且范围内最终权重(finweight)之和最低的范围的中心目标
+ * @param {int} id id from g().battle.monsterStatus.sort(objArrSort('finWeight'));
+ * @param {int} range radius, 0 for single-target and all-targets, 1 for treble-targets, ..., n for (2n+1) targets
+ * @param {(target) => number} excludeWeightRatio target with id
+ * @returns
+ */
+ function getRangeCenter(target, range, isWeaponAttack, excludeWeightRatio, forceUseIndex) {
+ let msTemp = JSON.parse(JSON.stringify(g().battle.monsterStatus));
+ msTemp.sort(objArrSort('order'));
+ let minWeight = Number.MAX_SAFE_INTEGER;
+ // 0. 范围大于等于全体时,直接释放全体
+ if (!range || range >= msTemp.length) {
+ return { id: getMonsterID(target), weight: minWeight };
+ }
+ const option = g().option;
+ const centralExtraWeight = -1 * Math.log10(1 + (isWeaponAttack ? option.centralExtraRatio / 100 : 0));
+ let order = target.order;
+ let newOrder = order;
+ // sort by order to fix id
+ let unreachableWeight = option.unreachableWeight;
+ // 1. 以选中目标为中心,优先向上
+ // 2. 超过顶部则向下找
+ // 3. 死亡、超过底下的将被溢出抛弃
+ const up = Math.floor(range / 2);
+ const down = range - up - 1;
+ const top = order < range ? 0 : Math.max(order - down, 0);
+ const bottom = Math.min(order + up, msTemp.length-1);
+ for (let i = top; i <= bottom; i++) {
+ let center = i;
+ if (msTemp[center].isDead) continue;
+ let weight = 0;
+ let overflow = Math.max(up-center,0);
+ const [min, max] = [center - up + overflow, center + down + overflow];
+ for (let inRange = min; inRange <= max; inRange++) {
+ let cew = inRange === center ? centralExtraWeight : 0; // cew <= 0, 增加未命中权重,降低命中权重
+ let mon = msTemp[inRange];
+ if (inRange < 0 || inRange >= msTemp.length || mon.isDead) { // 超出范围 或 死亡目标
+ weight += unreachableWeight;
+ continue;
+ }
+ if (excludeWeightRatio) {
+ // 特殊排除(ratio === 1则直接排除,00) {
+ continue;
+ }
+ }
+ weight += cew; // 中心目标会受到副手及冲击攻击时,相当于有效生命值降低
+ weight += forceUseIndex ? -1 : mon.finWeight; // 强制使用顺序而非权重时,全部使用统一的权重而非怪物状态
+ }
+ if (weight < minWeight) {
+ newOrder = center;
+ minWeight = weight;
+ break;
+ }
+ }
+ return { id: getMonsterID(newOrder), weight: minWeight };
}
- $ajax.open(getValue('lastHref'));
- }
- function reloader() {
- let obj; let a; let cost;
- const battleUnresponsive = {
- 'Alert': { Method: setAlarm },
- 'Reload': { Method: goto },
- 'Alt': { Method: gotoAlt }
- }
- function clearBattleUnresponsive(){
- Object.keys(battleUnresponsive).forEach(t=>clearTimeout(battleUnresponsive[t].Timeout));
- }
- const eventStart = cE('a');
- eventStart.id = 'eventStart';
- eventStart.onclick = function () {
- a = unsafeWindow.info;
- for(let t in g('option').battleUnresponsive) {
- if (g('option').battleUnresponsive[t]) {
- battleUnresponsive[t].Timeout = setTimeout(battleUnresponsive[t].Method, Math.max(1, g('option').battleUnresponsiveTime[t]) * _1s);
- }
- }
- if (g('option').recordUsage) {
- obj = {
- mode: a.mode,
- };
- if (a.mode === 'items') {
- obj.item = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).textContent;
- } else if (a.mode === 'magic') {
- obj.magic = gE(a.skill).textContent;
- cost = gE(a.skill).getAttribute('onmouseover').match(/\('.*', '.*', '.*', (\d+), (\d+), \d+\)/);
- obj.mp = cost[1] * 1;
- obj.oc = cost[2] * 1;
- }
+ function autoPause() {
+ const option = g().option;
+ if (option.autoPause && checkCondition(option.pauseCondition)) {
+ pauseChange();
+ return true;
}
- };
- gE('body').appendChild(eventStart);
- const eventEnd = cE('a');
- eventEnd.id = 'eventEnd';
- eventEnd.onclick = function () {
- const timeNow = time(0);
- g('runSpeed', (1000 / (timeNow - g('timeNow'))).toFixed(2));
- g('timeNow', timeNow);
- const monsterDead = gE('img[src*="nbardead"]', 'all').length;
- g('monsterAlive', g('monsterAll') - monsterDead);
- const bossDead = gE('div.btm1[style*="opacity"] div.btm2[style*="background"]', 'all').length;
- g('bossAlive', g('bossAll') - bossDead);
- const battleLog = gE('#textlog>tbody>tr>td', 'all');
- if (g('option').recordUsage) {
- obj.log = battleLog;
- recordUsage(obj);
+ return false;
+ }
+
+ function autoDefend() {
+ const option = g().option;
+ if (option.defend && checkCondition(option.defendCondition)) {
+ gE('#ckey_defend').click();
+ return true;
}
- if (g('monsterAlive') && !gE('#btcp')) {
- clearBattleUnresponsive();
- onBattle();
+ return false;
+ }
+
+ function setExitBattleTimeout(alarm) {
+ setAlarm(alarm);
+ const option = g().option;
+ if (alarm === 'Defeat' && !option.autoSkipDefeated) {
return;
}
- if (g('option').dropMonitor) {
- dropMonitor(battleLog);
+ delValue(1);
+ setTimeoutOrExecute(() => $ajax.openNoFetch(getValue('lastUrl')), option.ExitBattleWaitTime * _1s);
+ }
+
+ function reloader() {
+ const battleUnresponsive = {
+ 'Alert': { method: setAlarm },
+ 'Reload': { method: goto },
+ 'Alt': { method: gotoAlt }
}
- if (g('option').recordUsage) {
- recordUsage2();
+ function clearBattleUnresponsive() {
+ Object.keys(battleUnresponsive).forEach(t => {
+ if (!battleUnresponsive[t].timeout) return;
+ clearTimeout(battleUnresponsive[t].timeout);
+ });
}
- if (g('battle').roundNow !== g('battle').roundAll) { // Next Round
- if(g('option').NewRoundWaitTime){
- setTimeout(onNewRound, g('option').NewRoundWaitTime * _1s);
- } else {
- onNewRound();
+ async function onBattleUnresponsive(method) { try {
+ await waitPause();
+ method();
+ } catch (err) { console.error(err) } }
+
+ let obj, a, cost;
+ const eventStart = cE('a');
+ eventStart.id = 'eventStart';
+ eventStart.onclick = function () {
+ const option = g().option;
+ a = unsafeWindow.info;
+ for (let t in option.battleUnresponsive) {
+ if (option.battleUnresponsive[t]) {
+ battleUnresponsive[t].timeout = setTimeout(() => onBattleUnresponsive(battleUnresponsive[t].method), Math.max(1, option.battleUnresponsiveTime[t]) * _1s);
+ }
}
- return;
-
- async function onNewRound(){
- try {
- const html = await $ajax.fetch(window.location.href);
+ if (option.recordUsage) {
+ obj = {
+ mode: a.mode,
+ };
+ if (a.mode === 'items') {
+ obj.item = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).textContent;
+ } else if (a.mode === 'magic') {
+ obj.magic = gE(a.skill).textContent;
+ cost = gE(a.skill).getAttribute('onmouseover').match(/\('.*', '.*', '.*', (\d+), (\d+), \d+\)/);
+ obj.mp = cost[1] * 1;
+ obj.oc = cost[2] * 1;
+ }
+ }
+ };
+ gE('body').appendChild(eventStart);
+
+ const eventEnd = cE('a');
+ eventEnd.id = 'eventEnd';
+ eventEnd.onclick = function () {
+ const option = g().option;
+ const timeNow = time(0);
+ g('runSpeed', (1000 / (timeNow - g().timeNow)).toFixed(2));
+ g('timeNow', timeNow);
+ const monsterDead = gE('img[src*="nbardead"]', 'all').length;
+ g('monsterAlive', g().monsterAll - monsterDead);
+ const bossDead = gE(`${monsterStateKeys.obj}[style*="opacity"] ${monsterStateKeys.lv}[style*="background"]`, 'all').length;
+ g('bossAlive', g().bossAll - bossDead);
+ const battleLog = gE('#textlog>tbody>tr>td', 'all');
+ if (option.recordUsage) {
+ obj.log = battleLog;
+ recordUsage(obj);
+ }
+ if (g().monsterAlive && !gE('#btcp')) {
+ clearBattleUnresponsive();
+ onBattleRound();
+ return;
+ }
+ if (option.dropMonitor) {
+ dropMonitor(battleLog);
+ }
+ if (option.recordUsage) {
+ recordUsage2();
+ }
+ onRoundEnd();
+ async function onRoundEnd() { try {
+ await waitPause();
+ // $async.logSwitch(arguments);
+ if (g().monsterAlive > 0) { // Defeat
+ setExitBattleTimeout('Defeat');
+ clearBattleUnresponsive();
+ } else if (g().battle.roundNow === g().battle.roundAll) { // Victory
+ setExitBattleTimeout('Victory');
+ clearBattleUnresponsive();
+ } else { // Next Round
+ setTimeoutOrExecute(onNewRound, option.NewRoundWaitTime * _1s);
+ }
- gE('#pane_completion').removeChild(gE('#btcp'));
+ async function onNewRound() { try {
+ await waitPause();
+ // $async.logSwitch(arguments);
+ if (gE('#btcp')?.innerHTML.includes("finishbattle.png")) {
+ goto();
+ // $async.logSwitch(arguments);
+ return;
+ }
clearBattleUnresponsive();
- const doc = $doc(html)
+ let urlChecked;
+ if (option.checkURLBeforeNewRound) {
+ while (!urlChecked) {
+ try {
+ urlChecked = await $ajax.insert(option.checkURLBeforeNewRound);
+ } catch(e) {
+ await waitPause();
+ await pauseAsync(option.checkURLBeforeNewRoundRetry);
+ } finally {
+ if (!!urlChecked) {
+ // console.log('Done url check:', !!urlChecked, option.checkURLBeforeNewRound);
+ } else {
+ console.error('Failed connect ', option.checkURLBeforeNewRound);
+ }
+ }
+ }
+ }
+ const html = await $ajax.insert(window.location.href);
+ const doc = $doc(html);
if (gE('#riddlecounter', doc)) {
- if (g('option').riddlePopup && !window.opener) {
+ console.log('url check:', option.checkURLBeforeNewRound, '\n', urlChecked);
+ if (option.riddlePopup && !window.opener) {
window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707');
+ // $async.logSwitch(arguments);
return;
}
+ console.log(window.location.href);
goto();
+ // $async.logSwitch(arguments);
+ return;
+ }
+ if (option.nativeNewRound) {
+ onStepInDone();
+ gE('#btcp').click();
+ // $async.logSwitch(arguments);
return;
}
- ['#battle_right', '#battle_left'].forEach(selector=>{ gE('#battle_main').replaceChild(gE(selector, doc), gE(selector)); })
- unsafeWindow.battle = new unsafeWindow.Battle();
- unsafeWindow.battle.clear_infopane();
- Debug.log('______________newRound', true);
+ gE('#pane_completion').removeChild(gE('#btcp'));
+ ['#battle_right', '#battle_left'].forEach(selector => { gE('#battle_main').replaceChild(gE(selector, doc), gE(selector)); });
+ unsafeWindow.battle = undefined;
+ await loadUnsafeWindowBattle();
newRound(true);
- onBattle();
- } catch(e) { e=>console.error(e) }
- }
- }
-
- if (g('monsterAlive') > 0) { // Defeat
- SetExitBattleTimeout(g('option').autoSkipDefeated ? 'SkipDefeated' : 'Defeat');
- }
- if (g('battle').roundNow === g('battle').roundAll) { // Victory
- SetExitBattleTimeout('Victory');
- }
- clearBattleUnresponsive();
- };
- gE('body').appendChild(eventEnd);
- window.sessionStorage.delay = g('option').delay;
- window.sessionStorage.delay2 = g('option').delay2;
- const fakeApiCall = cE('script');
- fakeApiCall.textContent = `api_call = ${function (b, a, d) {
- const delay = window.sessionStorage.delay * 1;
- const delay2 = window.sessionStorage.delay2 * 1;
- window.info = a;
- b.open('POST', `${MAIN_URL}json`);
- b.setRequestHeader('Content-Type', 'application/json');
- b.withCredentials = true;
- b.onreadystatechange = d;
- b.onload = function () {
- document.getElementById('eventEnd').click();
+ onStepInDone();
+ onBattleRound();
+ // $async.logSwitch(arguments);
+ } catch (err) { console.error(err) } }
+ // $async.logSwitch(arguments);
+ } catch (err) { console.error(err) } }
};
- document.getElementById('eventStart').click();
- if (a.mode === 'magic' && a.skill >= 200) {
+ gE('body').appendChild(eventEnd);
+
+ window.sessionStorage.delay = g().option.delay;
+ window.sessionStorage.delay2 = g().option.delay2;
+ const fakeApiCall = cE('script');
+ fakeApiCall.textContent = `api_call = ${function (b, a, d) {
+ let delay = window.sessionStorage.delay * 1;
+ const delay2 = window.sessionStorage.delay2 * 1;
+ window.info = a;
+ unsafeWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
+ b.open('POST', `${unsafeWindow.MAIN_URL}json`);
+ b.setRequestHeader('Content-Type', 'application/json');
+ b.withCredentials = true;
+ b.onreadystatechange = d;
+ b.onload = function () {
+ updateMonsterEffects();
+ document.getElementById('eventEnd').click();
+ };
+ document.getElementById('eventStart').click();
+ if (a.mode !== 'magic' || a.skill < 200) {
+ delay = delay2;
+ }
if (delay <= 0) {
b.send(JSON.stringify(a));
} else {
@@ -3087,1267 +5220,1697 @@ try {
b.send(JSON.stringify(a));
}, delay * (Math.random() * 50 + 50) / 100);
}
- } else if (delay2 <= 0) {
- b.send(JSON.stringify(a));
- } else {
- setTimeout(() => {
- b.send(JSON.stringify(a));
- }, delay2 * (Math.random() * 50 + 50) / 100);
- }
- }.toString()}`;
- gE('head').appendChild(fakeApiCall);
- const fakeApiResponse = cE('script');
- fakeApiResponse.textContent = `api_response = ${function (b) {
- if (b.readyState === 4) {
- if (b.status === 200) {
- const a = JSON.parse(b.responseText);
- if (a.login !== undefined) {
- top.window.location.href = login_url;
- } else {
- if (a.error || a.reload) {
- window.location.href = window.location.search;
- }
- return a;
- }
- } else {
+ }.toString()};
+ // bool
+ const isIsekai = ${isIsekai};
+ const isDisplay = ${g().option.isDisplayAllDebuff};
+ const debuffAutoFill = ${g().option.debuffAutoFill?.toString() ?? 'undefined'};
+ const debuffAutoFillRec = ${g().option.debuffAutoFillRec?.toString() ?? 'undefined'};
+ // string
+ const current = "${current}";
+ const other = "${other}";
+ // object
+ const local = ${JSON.stringify(local)};
+ const standalone = ${JSON.stringify(standalone)};
+ const excludeStandalone = ${JSON.stringify(excludeStandalone)};
+ const portable = ${JSON.stringify(portable)};
+ const sharable = ${JSON.stringify(sharable)};
+ const monsterStateKeys = ${JSON.stringify(monsterStateKeys)};
+ const ability = ${JSON.stringify(ability)};
+ const hvVersion = ${JSON.stringify(hvVersion)};
+ const monsterBuffSkillLib = ${JSON.stringify(monsterBuffSkillLib)};
+ // funciton
+ ${[updateMonsterEffects,
+ getMonsterID, getMonster, getMonster, getBuff,
+ getValue, setValue, delValue,
+ getLocal, setLocal, delLocal,
+ gE, cE].map(f=>f.toString()).join(';')};
+ `;
+ gE('head').appendChild(fakeApiCall);
+ const fakeApiResponse = cE('script');
+ fakeApiResponse.textContent = `api_response = ${function (b) {
+ if (b.readyState !== 4) {
+ return false;
+ }
+ if (b.status !== 200) {
+ window.location.href = window.location.search;
+ return false;
+ }
+ const a = JSON.parse(b.responseText);
+ if (a.login !== undefined) {
+ top.window.location.href = login_url;
+ return false;
+ }
+ if (a.error || a.reload) {
window.location.href = window.location.search;
}
+ return a;
+ }.toString()}`;
+ gE('head').appendChild(fakeApiResponse);
+ }
+
+ function updateMonsterEffects(isNewTurn=true) {
+ if (!(typeof GM_getValue === 'undefined' ? debuffAutoFill : g().option.debuffAutoFill)) return;
+ const battle = getValue('battle', true);
+ if (!battle?.monsterStatus) return;
+ let regExp = {
+ locationQueries: /\w+=\w+/g,
+ playerInfo: /(\w+) Lv\.(\d+)/,
+ staminaInfo: /Stamina:\s(\d+)/,
+ spellInfo: /\('([\w\s-]+)'.*, '(\w+)', (\d+), (\d+), (\d+)\)/, //Name, iconID, MP, OC, CD
+ itemInfo: /set_infopane_item\((\d+)/,
+ battleTypeLog: /Initializing (.*) \.\.\./,
+ floor: /Floor (\d+)/,
+ round: /Round (\d+) \/ (\d+)/,
+ monster: /MID=(\d+) \(([^<>]+)\) \LV=(\d+) HP=(\d+)/g,
+ effectGain: /([\w\s-]+) gains the effect ([\w\s-]+)\./g,
+ effectExpired: /The effect ([\w\s-]+) on ([\w\s-]+) has expired\./g,
+ effectWear: /The effect ([\w\s-]+) on ([\w\s-]+) has worn off\./g,
+ effectWearAsleep: /([^<>]+) has been roused from its sleep\./g,
+ effectWearConfused: /([^<>]+) got knocked out of confuse\./g,
+ oc: /div/g,
+ ocHalf: /vcr/g,
+ /*isekai911*/
+ spellMatch: /\('(?[\w\s-]+)(?:\s\(x(?\d+)\))?',\s?(?.*),\s?'?(?[\w\s-]+)'?\)/,
+ /*isekai912*/
+ //battleRecorder
+ turnLog: /([^]+?)| /,
+ //timeRecorder
+ action: />([^<>]+)<\/td><\/tr>( | | Spirit Stance Exhausted<\/td><\/tr>)* | | ([^<>]+)<\/td><\/tr> | [^<>]+<\/td><\/tr>( | | Spirit Stance Exhausted<\/td><\/tr>)* | | ]+damage( \([^<>]+\))*(<\/td><\/tr> | Your spirit shield absorbs \d+ |<|\.)/g,
+ damageType: /for (\d+) (\w+) damage/,
+ spiritShield: /absorbs (\d+)/,
+ crit: /(You crit| crits | blasts )/,
+ strike: /(Fire|Cold|Wind|Elec|Holy|Dark|Void) Strike hits/,
+ damagePlus: /for (\d+) damage/,
+ damagePhysicalPlus: /(Bleeding Wound|Spreading Poison)/,
+ damagePoints: /for (\d+) points of (\w+) damage/,
+ counter: />You counter/g,
+ // dealt magical
+ magicalDealtMiss: /to connect\./g,
+ magicalDealtEvade: /evades your spell\./g,
+ magicalDealtResist50: / (?:hits|blasts) [^y][^<>]+50%/g,
+ magicalDealtResist75: / (?:hits|blasts) [^y][^<>]+75%/g,
+ magicalDealtResist90: / (?:hits|blasts) [^y][^<>]+90%/g,
+ magicalDealtResist: /resists your spell\./g,
+ // dealt physical
+ physicalDealtMiss: /its mark\./g,
+ physicalDealtEvade: /(?: dodges your attack\.|evades your offhand attack\.)/g,
+ physicalDealtParry: /parries your attack\./g,
+ // taken magical
+ magicalTakenEvade: / casts [^<>]+evade the attack\./g,
+ magicalTakenBlock: / casts [^<>]+block the attack\./g,
+ magicalTakenResist50: / (?:hits|blasts) y[^<>]+50%/g,
+ magicalTakenResist75: / (?:hits|blasts) y[^<>]+75%/g,
+ magicalTakenResist90: / (?:hits|blasts) y[^<>]+90%/g,
+ // taken physical
+ physicalTakenMiss: /misses the attack against you\./g,
+ physicalTakenEvade: /(>You evade| uses [^<>]+evade the attack\.)/g,
+ physicalTakenParry: /(>You parry| uses [^<>]+parry the attack\.)/g,
+ physicalTakenBlock: /(>You block| uses [^<>]+block the attack\.)/g,
+ /*isekai911*/
+ //combatRecorder_isekai
+ damage_isekai: /[^<>]+damage/g,
+ damageTaken1_isekai: /(?glances|hits|crits) you.*?(?\d+).*?(?\w+) damage/,
+ damageTaken2_isekai: /which (?glances|hits|crits).*?(?\d+).*?(?\w+) damage/,
+ spiritShield_isekai: /absorbs (\d+)/,
+ damageDealt1_isekai: /(?:You|Your offhand attack|Arcane Blow) (?:(?\d)x-)*(?glance|hit|crit).*?(?\d+).*?(?\w+) damage/,
+ damageDealt2_isekai: /(?:(?\d)x-)*(?glanced|hit|crit|eviscerated) for (?\d+) (?\w+) damage/,
+ strike_isekai: /(Fire|Cold|Wind|Elec|Holy|Dark|Void) Strike hits.*?(\d+).*?(\w+) damage/,
+ explode_isekai: /explodes for (\d+) (\w+) damage/,
+ damagePlus_isekai: /for (\d+) damage/,
+ damagePhysicalPlus_isekai: /(Bleeding Wound|Spreading Poison)/,
+ damagePoints_isekai: /for (\d+) points of (\w+) damage/,
+ debuffLog_isekai: /(?:| [^<>]+(?: gains the effect | partially resists the effects of your spell\.| shrugs off the effects of your spell\.)+[^<>]*<\/td><\/tr>)+ | You cast [a-zA-Z]+\.<\/td><\/tr>/,
+ debuffResist0_isekai: / gains the effect /g,
+ debuffResist1_isekai: / partially resists the effects of your spell\./g,
+ debuffResist3_isekai: / shrugs off the effects of your spell\./g,
+ counter_isekai: />You counter/g,
+ // dealt magical
+ magicalDealtMiss_isekai: / to connect\./g,
+ magicalDealtEvade_isekai: / evades your spell\./g,
+ magicalDealtResistPartially_isekai: / resists, and was/g,
+ magicalDealtResist_isekai: / resists your spell\./g,
+ // dealt physical
+ physicalDealtMiss_isekai: / its mark\./g,
+ physicalDealtEvade_isekai: / dodges your attack\./g,
+ physicalDealtParryPartially_isekai: / parries[^<>]+?(\d+)[^<>]+?(\w+) damage/g,
+ physicalDealtParry_isekai: / parries your attack\./g,
+ // taken magical
+ magicalTakenMiss_isekai: /(?:casts[^<>]+, but misses the attack\.|casts[^<>]+, missing you completely\.)/g,
+ magicalTakenEvade_isekai: />You evade the attack\./g,
+ magicalTakenResistPartially_isekai: / resist the attack/g,
+ magicalTakenBlockPartially_isekai: /casts[^<>]+partially block (?:and|resist| )*the attack/g,
+ magicalTakenBlock_isekai: /(?]+, but misses the attack\.|(?:vigorously whiffs at a shadow|uses[^<>]+), missing you completely\.)/g,
+ physicalTakenEvade_isekai: />You evade the attack from/g,
+ physicalTakenParryPartially_isekai: /partially parry the attack/g,
+ physicalTakenParry_isekai: /(?]+|>)You|you) partially block (?:and|partially|parry| )*the attack/g,
+ physicalTakenBlock_isekai: /(?]+ proficiency/g,
+ proficiency: /(\d+\.\d+) points of ([^<>]+) proficiency/,
+ dropsLogs: /\S+ \[[^<>]+\](<\/span><\/td><\/tr>| A traveling)*/g,
+ drop: /(\S+) \<.*#(.{6}).*\[(.*)\](.)*/,
+ quality: /(Crude|Fair|Average|Superior|Exquisite|Magnificent|Legendary|Peerless)/,
+ credit: /(\d+) Credit/,
+ crystal: /(?:(\d+)x )?(Crystal of \w+)/,
+ }
+
+ function getDuration(skill, channeling) {
+ let [base, profRatio, prof] = [skill.duration, 1, 0];
+ if (typeof base === 'number') {
+ base = base * 1;
+ } else if (base !== 'permanent') { for (const ab in base) {
+ base = base[ab][ability[ab]??0];
+ break;
+ } }
+ if (skill.proficiency) {
+ const [ptype, plow, phigh] = skill.proficiency;
+ prof = proficiency[ptype];
+ profRatio = Math.max(1,Math.min(4, (prof-plow)/(phigh-plow) * 4).toFixed(6)*1);
+ }
+ const channelingRatio = skill.channling ? channeling : 1;
+ const duration = typeof base === 'number' ? Math.round(base * channelingRatio * profRatio) : base;
+ return [duration, base, profRatio, prof, channelingRatio];
}
- return false;
- }.toString()}`;
- gE('head').appendChild(fakeApiResponse);
- }
- function newRound(isNew) { // New Round
- let battle = isNew ? {} : getValue('battle', true);
- if (!battle) {
- battle = JSON.parse(JSON.stringify(g('battle') ?? {}));
- battle.monsterStatus?.sort(objArrSort('order'));
- };
- setValue('battle', battle);
- if (window.location.hash !== '') {
- goto();
- }
- g('monsterAll', gE('div.btm1', 'all').length);
- const monsterDead = gE('img[src*="nbardead"]', 'all').length;
- g('monsterAlive', g('monsterAll') - monsterDead);
- g('bossAll', gE('div.btm2[style^="background"]', 'all').length);
- const bossDead = gE('div.btm1[style*="opacity"] div.btm2[style*="background"]', 'all').length;
- g('bossAlive', g('bossAll') - bossDead);
- const battleLog = gE('#textlog>tbody>tr>td', 'all');
- if (!battle.roundType) {
- const temp = battleLog[battleLog.length - 1].textContent;
+ function getEffectChanges(turnLog) {
+ let effectsAdded = turnLog.matchAll(regExp.effectGain);
+ let effectsRemoved = [...turnLog.matchAll(regExp.effectExpired), ...turnLog.matchAll(regExp.effectWear)];
+ let asleepRemoved = turnLog.matchAll(regExp.effectWearAsleep);
+ let confusedRemoved = turnLog.matchAll(regExp.effectWearConfused);
+ let effectChanges = {};
+
+ for (const match of effectsAdded) (effectChanges[match[1]] ??= { add: [], remove: [] }).add.push(match[2]);
+ for (const match of effectsRemoved) (effectChanges[match[2]] ??= { add: [], remove: [] }).remove.push(match[1]);
+ for (const match of asleepRemoved) (effectChanges[match[1]] ??= { add: [], remove: [] }).remove.push('Asleep');
+ for (const match of confusedRemoved) (effectChanges[match[1]] ??= { add: [], remove: [] }).remove.push('Confused');
+
+ return effectChanges;
+ }
+
+ function applyHiddenDelta(savedEffects, effectObj, delta) {
+ if (!savedEffects) return;
+
+ let elementEffects = ['Searing Skin', 'Freezing Limbs', 'Turbulent Air', 'Deep Burns', 'Breached Defense', 'Blunted Attack'];
+ let effects = Object.keys(effectObj);
+ let elementCount = effects.filter(effect => elementEffects.includes(effect)).length;
+
+ for (const savedEffect in savedEffects) {
+ if (effects.includes(savedEffect)) continue;
+
+ if (
+ (elementCount < 3 && elementEffects.includes(savedEffect)) ||
+ savedEffect === 'Coalesced Mana'
+ ) {
+ delete savedEffects[savedEffect];
+ continue;
+ }
+
+ if (!delta || delta <= 0) continue;
+ let savedTurns = +savedEffects[savedEffect]?.turns;
+ if (isNaN(savedTurns)) continue;
+
+ if (savedTurns - delta < 0 && elementEffects.includes(savedEffect)) {
+ delete savedEffects[savedEffect];
+ continue;
+ }
+ savedEffects[savedEffect].turns = Math.max(0, savedTurns - delta);
+ }
+ }
+
+ const turnLog = gE('#textlog').innerHTML.match(/([^]+?)(( | | )|(<\/tbody>))/)[0];
+ isNewTurn &&= turnLog !== battle.turnLog;
+ if (turnLog.match(regExp.battleTypeLog)) return; // skip if is new round
+
+ // update proficiency
+ const proficiency = battle.proficiency ?? {};
+ const ptypes = hvVersion < 91 ? {
+ 'cloth armor': 'cloth armor',
+ 'deprecating magic' : 'deprecating',
+ 'divine': 'divine',
+ 'dual wielding' : 'dual wielding',
+ 'elemental': 'elemental',
+ 'forbidden': 'forbidden',
+ 'heavy armor': 'heavy armor',
+ 'light armor': 'light armor',
+ 'one-handed weapon': 'one-handed',
+ 'staff': 'staff',
+ 'supportive magic' : 'supportive',
+ 'two-handed weapon': 'two-handed',
+ } :{
+ 'cloth armor': 'Cloth Armor',
+ 'deprecating magic' : 'Deprecating',
+ 'divine': 'Divine',
+ 'dual wielding' : 'Dual-wielding',
+ 'elemental': 'Elemental',
+ 'forbidden': 'Forbidden',
+ 'heavy armor': 'Heavy Armor',
+ 'light armor': 'Light Armor',
+ 'one-handed weapon': 'One-handed',
+ 'staff': 'Staff',
+ 'supportive magic' : 'Supportive',
+ 'two-handed weapon': 'Two-handed',
+ }
+ for (const prof of turnLog.match(regExp.proficiencies)??[]) {
+ const [_, points, type] = prof.match(regExp.proficiency);
+ proficiency[ptypes[type]] += points*1;
+ proficiency[ptypes[type]] = proficiency[ptypes[type]].toFixed(3)*1;
+ }
+
+ // cache last turn channeling
+ const channeling = battle.channeling || 1;
+ battle.channeling = getBuff('channeling') ? 1.5 : 1;
+
+ let effectChanges = getEffectChanges(turnLog);
+
+ // 消除对每次操作影响turns程度最大的加速buff(技能或卷轴)
+ let turnDelta = (isNewTurn && !turnLog.match(regExp.zeroturn)) ? 1 : 0;
+ turnDelta *= 100 / ((getBuff('haste')?.getAttribute('onmouseover').match(/increasing its action speed by (.*)%\./)[1] ?? 0)*1 + 100);
+
+ const getBuffSkill = (buff) => Object.values(monsterBuffSkillLib).find(skill => [skill.name, skill.buff].includes(buff)) ?? console.log('Unknown debuff skill', buff);
+ for (const activeMonster of battle.monsterStatus) {
+ const monster = getMonster(getMonsterID(activeMonster));
+ if (gE('img[src*="nbardead.png"]', monster)) continue; // continue if dead
+
+ let name = gE(monsterStateKeys.name, monster).innerText;
+ let effectObj = {};
+ let jpxObj = {};
+ let monster_btm6 = gE('.btm6', monster);
+
+ monster_btm6.querySelectorAll('img').forEach((effect) => {
+ let tooltip = effect.getAttribute('onmouseover');
+ if (!tooltip) return;
+
+ let matches = tooltip.match(regExp.spellMatch);
+ if (!matches?.groups) return;
+
+ let { name, stack, description, turns } = matches.groups;
+ if (!name) return console.log('Undefined debuff name:', name);
+ const jpx = turns === '-' || description.includes('jpx Hidden Effects');
+ if (jpx) { // 可能为jpx补全的buff,在hvAA中重新计算确认数据
+ jpxObj[name] = effect;
+ return;
+ }
+ let dc = getBuffSkill(name)?.description;
+ if (dc !== description) {
+ // TODO 测试确保 ability[4213] Better Slow 效果描述正常,目前是描述均是30%
+ if (dc !== `'The target has been slowed by ${[30,40,40,45,50,50][ability[4213]??0]}%, making it attack less frequently.'` && description !== `'The target has been slowed by 30%, making it attack less frequently.'`) {
+ console.log('Unmatched debuff description:', description, '\n from', name, dc);
+ }
+ }
+ effectObj[name] = { turns, stack: stack ?? 1 };
+ });
+ let effects = Object.keys(effectObj);
+
+ // DEBUG ---------------------
+ if (typeof GM_getValue === 'undefined' ? debuffAutoFillRec : g().option.debuffAutoFillRec) {
+ // 统计持续时间及熟练度相关数据,以便进行核验和测试
+ const rec = JSON.parse(localStorage.getItem(`hvAA-${current}_rec`) ?? `{}`);
+ for (const effect of effects) {
+ const turns = effectObj[effect].turns*1;
+ if (isNaN(turns)) continue;
+ const skill = getBuffSkill(effect);
+ if (!skill) continue;
+
+ rec[effect] ??= { t:0 };
+ // 获取新增时间(忽略非新增的情况)
+ let [delta, added] = [turns - rec[effect].t, rec[effect].d];
+ if (delta > 0) {
+ added = hvVersion < 91 ? turns : rec[effect].t ? delta : added;
+ }
+ // 获取基础、熟练度计算倍率、熟练度,设置及初始化主要数据
+ let [duration, base, profRatio, prof, channelingRatio] = getDuration(skill, channeling);
+ if (profRatio === 4) rec[effect].f = prof; // 比例刚好是4时的熟练度(推测是公式中的熟练度上限)
+ rec[effect].b = base; // 基础持续时间
+ rec[effect].c = profRatio; // 公式理论计算值
+ rec[effect].ch = rec[effect].t && added > 0 ? channelingRatio : rec[effect].ch; // 引导倍率
+ rec[effect].t = turns; // 当前剩余持续时间
+ rec[effect].d = added; // 新增时间
+ rec[effect].m = Math.max(rec[effect].m??0, added); // 历史最大新增时间
+ rec[effect].a ??= [0,0]; // 推测熟练度倍率 [ 历史最大值, 按照‘缺失引导信息导致变成1.5倍’的修正值(除以1.5) ]
+ rec[effect].r ??= [0,0,0]; // 实际倍率 [ 0-4 应该正常, 4-6推测缺失引导信息, 6+ 异常]
+ rec.error ??= []; // 实际倍率异常时的相关信息
+ // 计算推测倍率
+ if (base <= added) {
+ const a = Math.max(base, added)/base/channelingRatio
+ rec[effect].a[0] = Math.max(a, rec[effect].a[0]).toFixed(4)*1;
+ rec[effect].a[1] = Math.max(a/1.5, rec[effect].a[1]).toFixed(4)*1;
+ }
+ // 检查实际ratio
+ const ratio = Math.max(base, added)/duration;
+ if (ratio > 1.5) {
+ const e =`${effect}: ${Math.max(base, added).toFixed(4)}/(${base.toFixed(4)}*${channelingRatio.toFixed(4)}*${profRatio.toFixed(4)})=${ratio.toFixed(4)}`
+ if (!rec.error.includes(e)) rec.error.push(e);
+ }
+ if (ratio > 1.5 && ratio > rec[effect].r[2]) {
+ rec[effect].r[2] = ratio.toFixed(4)*1;
+ } else if (ratio <= 1.5 && ratio > 1 && ratio > rec[effect].r[1]) {
+ rec[effect].r[1] = ratio.toFixed(4)*1;
+ } else if (ratio <= 1 && ratio > rec[effect].r[0]) {
+ rec[effect].r[0] = ratio.toFixed(4)*1;
+ }
+ localStorage.setItem(`hvAA-${current}_rec`, JSON.stringify(rec));
+ }
+ }
+ // DEBUG END ---------------------
+
+ let savedEffects = activeMonster.effectObj ??= {};
+ if (effects.length <= 5) for (const effect in savedEffects) delete savedEffects[effect];
+ if (effects.length >= 5) for (const effect of effects) savedEffects[effect] = { ...effectObj[effect] }; // updated directly
+ if (effects.length !== 6) continue; // <= 5 && undetermined situation
+
+ if (effectChanges[name]) {
+ for (const effect of effectChanges[name].add) {
+ const skill = getBuffSkill(effect);
+ if (!skill) continue;
+ /* TODO
+ 1. TBD stack from monsterBuffSkillLib etc.
+ 2. 测试检查非 减益技能(deprecating) 的debuff持续时间是否正确 (monsterBuffSkillLib)
+ 3. 确认v091不同buff的叠加规则(部分抵抗无法估算?)
+ 4. 确认熟练度倍率公式,已知最大为4。推测:
+ 计算方式为 (p-pmin)/(pmax-pmin) * 4
+ pmin/pmax 见 https://ehwiki.org/wiki/Spells#Deprecating_Magic
+ 和 https://ehwiki.org/wiki/Spells#Offensive_Magic
+ 减益技能(deprecating) 统一按照减益的熟练度
+ 元素攻击(应该包括Burning Soul/Ripened Soul?)带来的按各自的熟练度(推测是按T3的pmin/pmax)
+ 至于取整方式则暂时无法确定
+ */
+ let [duration, base, profRatio, prof, channelingRatio] = getDuration(skill, channeling);
+ if (savedEffects[name]) savedEffects[name][effect].channeling ??= channelingRatio;
+ if (effects.includes(effect)) continue; // updated directly above
+ if (!duration) { console.log('duration undefined saved effect:', effect, savedEffects[effect]) }
+ if (hvVersion >= 91 && savedEffects[effect]) {
+ const turn = (savedEffects[effect].turns??0) * 1;
+ if (isNewTurn && !isNaN(turn)) duration = turn + (duration ?? 0);
+ }
+ savedEffects[effect] = { turns: duration ?? '-', stack: '-' , channeling: channelingRatio};
+ }
+ for (const effect of effectChanges[name].remove) (!effects.includes(effect) && (effect in savedEffects)) && delete savedEffects[effect];
+ }
+
+ applyHiddenDelta(savedEffects, effectObj, turnDelta);
+
+ monster_btm6.style.width = 'max-content';
+ activeMonster.effectObj = savedEffects;
+ for (const effect in savedEffects) {
+ if (effect in effectObj) continue;
+ let { turns, stack } = savedEffects[effect];
+ effectObj[effect] = { turns, stack };
+ if (isNaN(+turns)) turns = `'${String(turns).replace(/'/g, "\\'")}'`;
+ let img = jpxObj[effect] ?? document.createElement('img');
+ img.src = (`${isIsekai ? '/isekai' : ''}/y/e/${ getBuffSkill(effect)?.img || 'channeling'}.png`);
+ let description = getBuffSkill(effect)?.description;
+ img.setAttribute('onmouseover', `battle.set_infopane_effect('${effect}', ${description}, ${Math.floor(turns)})`);
+ img.setAttribute('onmouseout', 'battle.clear_infopane()');
+
+ monster_btm6.appendChild(img);
+ }
+ }
+ if (!isNewTurn) return;
+ battle.turnLog = turnLog;
+ setValue('battle', battle);
+ }
+
+ async function loadUnsafeWindowBattle() { try {
+ while (!unsafeWindow.battle) {
+ await pauseAsync(300);
+ unsafeWindow.battle = new unsafeWindow.Battle();
+ }
+ unsafeWindow.battle.clear_infopane();
+ } catch(e) { console.error(e) }}
+
+ function newRound(isNew) { // New Round
+ $debug.log('______________newRound', isNew);
+ const token = document.documentElement.outerHTML.match(/var battle_token = "(.*)";/)[1];
+ let battle = getValue('battle', true);
+ const isSameBattle = battle?.token === token;
+ const prof = getValue('proficiency', true);
+ if (isNew) {
+ battle = {
+ proficiency: isSameBattle ? battle?.proficiency ?? prof : prof,
+ skillOTOS: {}
+ };
+ }
+ if (!battle) {
+ battle = JSON.parse(JSON.stringify(g().battle ?? {}));
+ battle.monsterStatus?.sort(objArrSort('order'));
+ };
+ battle.token = token;
+ battle.proficiency = isSameBattle ? battle?.proficiency ?? prof : prof;
+ setValue('battle', battle);
+ if (window.location.hash !== '') {
+ goto();
+ }
+ g('monsterAll', gE(monsterStateKeys.obj, 'all').length);
+ const monsterDead = gE('img[src*="nbardead"]', 'all').length;
+ g('monsterAlive', g().monsterAll - monsterDead);
+ g('bossAll', gE(`${monsterStateKeys.lv}[style^="background"]`, 'all').length);
+ const bossDead = gE(`${monsterStateKeys.obj}[style*="opacity"] ${monsterStateKeys.lv}[style*="background"]`, 'all').length;
+ g('bossAlive', g().bossAll - bossDead);
const types = {
- 'ar': {
+ ar: {
reg: /^Initializing arena challenge/,
extra: (i) => i <= 35,
},
- 'rb': {
+ rb: {
reg: /^Initializing arena challenge/,
extra: (i) => i >= 105,
},
- 'iw': { reg: /^Initializing Item World/ },
- 'gr': { reg: /^Initializing Grindfest/ },
- 'tw': { reg: /^Initializing The Tower/ },
- 'ba': {
- reg: /^Initializing random encounter/,
- extra: (_) => {
- const encounter = getEncounter();
- if (encounter[0] && encounter[0].time >= time(0) - 0.5 * _1h) {
- encounter[0].encountered = time(0);
- setEncounter(encounter);
- }
- return true;
- }
- },
+ iw: { reg: /^Initializing Item World/ },
+ gr: { reg: /^Initializing Grindfest/ },
+ tw: { reg: /^Initializing The Tower/ },
+ ba: { reg: /^Initializing random encounter/ },
}
- battle.tower = (temp.match(/\(Floor (\d+)\)/) ?? [null])[1] * 1;
- const id = (temp.match(/\d+/) ?? [null])[0] * 1;
- battle.roundType = undefined;
- for (let name in types) {
- const type = types[name];
- if (!temp.match(type.reg)) {
- continue;
- }
- if (type.extra && !type.extra(id)) {
- continue;
+ const battleLog = gE('#textlog>tbody>tr>td', 'all');
+ if (!battle.roundType) {
+ const temp = battleLog[battleLog.length - 1].textContent;
+ battle.tower = (temp.match(/\(Floor (\d+)\)/) ?? [null])[1] * 1;
+ const id = (temp.match(/\d+/) ?? [null])[0] * 1;
+ battle.roundType = undefined;
+ for (let name in types) {
+ const type = types[name];
+ if (!temp.match(type.reg)) {
+ continue;
+ }
+ if (type.extra && !type.extra(id)) {
+ continue;
+ }
+ battle.roundType = name;
+ break;
}
- battle.roundType = name;
- break;
}
- }
- if (/You lose \d+ Stamina/.test(battleLog[0].textContent)) {
- const staminaLostLog = getValue('staminaLostLog', true) || {};
- staminaLostLog[time(3)] = battleLog[0].textContent.match(/You lose (\d+) Stamina/)[1] * 1;
- setValue('staminaLostLog', staminaLostLog);
- const losedStamina = battleLog[0].textContent.match(/\d+/)[0] * 1;
- if (losedStamina >= g('option').staminaLose) {
- setAlarm('Error');
- if (!_alert(1, '当前Stamina过低\n或Stamina损失过多\n是否继续?', '當前Stamina過低\n或Stamina損失過多\n是否繼續?', 'Continue?\nYou either have too little Stamina or have lost too much')) {
- pauseChange();
- return;
+ if (battle.roundType === 'ba' || document.body.innerHTML.match(/Initializing random encounter/)) {
+ const encounter = getEncounter();
+ if (encounter[0]) {
+ encounter[0].encountered = time(0);
+ setEncounter(encounter);
}
}
- }
- const roundPrev = battle.roundNow;
-
- if (battleLog[battleLog.length - 1].textContent.match('Initializing')) {
- const monsterStatus = [];
- let order = 0;
- const monsterNames = Array.from(gE('div.btm3>div>div', 'all')).map(monster => monster.innerText);
- const monsterLvs = Array.from(gE('div.btm2>div>div', 'all')).map(monster => monster.innerText);
- const monsterDB = getValue('monsterDB', true) ?? {};
- const monsterMID = getValue('monsterMID', true) ?? {};
- const oldDB = JSON.stringify(monsterDB);
- const oldMID = JSON.stringify(monsterMID);
- for (let i = battleLog.length - 2; i > battleLog.length - 2 - g('monsterAll'); i--) {
- let mid = battleLog[i].textContent.match(/MID=(\d+)/)[1] * 1;
- let name = battleLog[i].textContent.match(/MID=(\d+) \((.*)\) LV/)[2];
- let lv = battleLog[i].textContent.match(/LV=(\d+)/)[1] * 1;
- let hp = battleLog[i].textContent.match(/HP=(\d+)$/)[1] * 1;
- if (isNaN(hp)) {
- hp = getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? monsterStatus[monsterStatus.length - 1].hp;
- }
- if (name && lv && mid) {
- monsterDB[name] ??= {};
- if (monsterDB[name].mid && monsterDB[name].mid !== mid) { // 名称被其他mid被占用
- monsterMID[monsterDB[name].mid] = JSON.parse(JSON.stringify(monsterDB[name])); // 将之前mid的数据进行另外备份
- monsterDB[name] = {}; // 重置该名称的数据
+ const roundPrev = battle.roundNow;
+
+ if (battleLog[battleLog.length - 1].textContent.match('Initializing')) {
+ const monsterStatus = [];
+ let order = 0;
+ const monsterNames = Array.from(gE(`${monsterStateKeys.name}>div>div`, 'all')).map(monster => monster.innerText);
+ const monsterLvs = Array.from(gE(`${monsterStateKeys.lv}>div>div`, 'all')).map(monster => monster.innerText);
+ const monsterDB = getValue('monsterDB', true) ?? {};
+ const monsterMID = getValue('monsterMID', true) ?? {};
+ for (let i = battleLog.length - 2; i > battleLog.length - 2 - g().monsterAll; i--) {
+ let mid = battleLog[i].textContent.match(/MID=(\d+)/)[1] * 1;
+ let name = battleLog[i].textContent.match(/MID=(\d+) \((.*)\) LV/)[2];
+ let lv = battleLog[i].textContent.match(/LV=(\d+)/)[1] * 1;
+ let hp = battleLog[i].textContent.match(/HP=(\d+)$/)[1] * 1;
+ if (isNaN(hp)) {
+ hp = getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? monsterStatus[monsterStatus.length - 1].hp;
}
- if (monsterMID[mid]) {
- monsterDB[name] = JSON.parse(JSON.stringify(monsterMID[mid])); // 将之前备份的mid的数据进行恢复
- delete monsterMID[mid];
+ if (name && lv && mid) {
+ monsterDB[name] ??= {};
+ if (monsterDB[name].mid && monsterDB[name].mid !== mid) { // 名称被其他mid被占用
+ monsterMID[monsterDB[name].mid] = JSON.parse(JSON.stringify(monsterDB[name])); // 将之前mid的数据进行另外备份
+ monsterDB[name] = {}; // 重置该名称的数据
+ }
+ if (monsterMID[mid]) {
+ monsterDB[name] = JSON.parse(JSON.stringify(monsterMID[mid])); // 将之前备份的mid的数据进行恢复
+ delete monsterMID[mid];
+ }
+ monsterDB[name].mid = mid;
+ monsterDB[name][lv] = hp;
}
- monsterDB[name].mid = mid;
- monsterDB[name][lv] = hp;
+ monsterStatus[order] = { order, hp };
+ order++;
}
- monsterStatus[order] = {
- order: order,
- hp,
- };
- order++;
- }
- if(g('option').cacheMonsterHP){
- if (oldDB !== JSON.stringify(monsterDB)) {
+ if (g().option.cacheMonsterHP) {
setValue('monsterDB', monsterDB);
- }
- if (oldMID !== JSON.stringify(monsterMID)) {
setValue('monsterMID', monsterMID);
}
- }
- battle.monsterStatus = monsterStatus;
+ battle.monsterStatus = monsterStatus;
- const round = battleLog[battleLog.length - 1].textContent.match(/\(Round (\d+) \/ (\d+)\)/);
- if (round && battle.roundType !== 'ba') {
- battle.roundNow = round[1] * 1;
- battle.roundAll = round[2] * 1;
- } else {
+ const round = battleLog[battleLog.length - 1].textContent.match(/\(Round (\d+) \/ (\d+)\)/);
+ if (round && battle.roundType !== 'ba') {
+ battle.roundNow = round[1] * 1;
+ battle.roundAll = round[2] * 1;
+ } else {
+ battle.roundNow = 1;
+ battle.roundAll = 1;
+ }
+ } else if (!battle.monsterStatus || battle.monsterStatus.length !== gE(monsterStateKeys.lv, 'all').length) {
battle.roundNow = 1;
- battle.roundAll = 1;
- }
- } else if (!battle.monsterStatus || battle.monsterStatus.length !== gE('div.btm2', 'all').length) {
- battle.roundNow = 1;
- battle.roundAll = 1;
- }
-
- if(roundPrev !== battle.roundNow) {
- battle.turn = 0;
- }
- battle.roundLeft = battle.roundAll - battle.roundNow;
- setValue('battle', battle);
-
- g('skillOTOS', {
- OFC: 0,
- FRD: 0,
- T3: 0,
- T2: 0,
- T1: 0,
- });
- }
-
- function killBug() { // 在 HentaiVerse 发生导致 turn 损失的 bug 时发出警告并移除问题元素: https://ehwiki.org/wiki/HentaiVerse_Bugs_%26_Errors#Combat
- const bugLog = gE('#textlog > tbody > tr > td[class="tlb"]', 'all');
- const isBug = /(Slot is currently not usable)|(Item does not exist)|(Inventory slot is empty)|(You do not have a powerup gem)/;
- for (let i = 0; i < bugLog.length; i++) {
- if (bugLog[i].textContent.match(isBug)) {
- bugLog[i].className = 'tlbWARN';
- setTimeout(() => { // 间隔时间以避免持续刷新
- window.location.href = window.location;// 刷新移除问题元素
- }, 700);
- } else {
- bugLog[i].className = 'tlbQRA';
- }
- }
- }
-
- function countMonsterHP() { // 统计敌人血量
- let i, j;
- const monsterHp = gE('div.btm4>div.btm5:nth-child(1)', 'all');
- let battle = getValue('battle', true);
- const monsterStatus = battle.monsterStatus;
- const hpArray = [];
- for (i = 0; i < monsterHp.length; i++) {
- if (gE('img[src*="nbardead.png"]', monsterHp[i])) {
- monsterStatus[i].isDead = true;
- monsterStatus[i].hpNow = Infinity;
- } else {
- monsterStatus[i].isDead = false;
- monsterStatus[i].hpNow = Math.floor(monsterStatus[i].hp * parseFloat(gE('img', monsterHp[i]).style.width) / 120 + 1);
- hpArray.push(monsterStatus[i].hpNow);
- }
- }
- battle.monsterStatus = monsterStatus;
-
- const skillLib = {
- Sle: {
- name: 'Sleep',
- img: 'sleep',
- },
- Bl: {
- name: 'Blind',
- img: 'blind',
- },
- Slo: {
- name: 'Slow',
- img: 'slow',
- },
- Im: {
- name: 'Imperil',
- img: 'imperil',
- },
- MN: {
- name: 'MagNet',
- img: 'magnet',
- },
- Si: {
- name: 'Silence',
- img: 'silence',
- },
- Dr: {
- name: 'Drain',
- img: 'drainhp',
- },
- We: {
- name: 'Weaken',
- img: 'weaken',
- },
- Co: {
- name: 'Confuse',
- img: 'confuse',
- },
- CM: {
- name: 'Coalesced Mana',
- img: 'coalescemana',
- },
- Stun: {
- name: 'Stunned',
- img: 'wpn_stun',
- },
- PA: {
- name: 'Penetrated Armor',
- img: 'wpn_ap',
- },
- BW: {
- name: 'Bleeding Wound',
- img: 'wpn_bleed',
- },
- };
- const monsterBuff = gE('div.btm6', 'all');
- const hpMin = Math.min.apply(null, hpArray);
- const yggdrasilExtraWeight = g('option').YggdrasilExtraWeight;
- const unreachableWeight = g('option').unreachableWeight;
- const baseHpRatio = g('option').baseHpRatio ?? 1;
- // 权重越小,优先级越高
- for (i = 0; i < monsterStatus.length; i++) { // 死亡的排在最后(优先级最低)
- if (monsterStatus[i].isDead) {
- monsterStatus[i].finWeight = unreachableWeight;
- continue;
- }
- let weight = baseHpRatio * Math.log10(monsterStatus[i].hpNow / hpMin); // > 0 生命越低权重越低优先级越高
- monsterStatus[i].hpWeight = weight;
- if (yggdrasilExtraWeight && ('Yggdrasil' === gE('div.btm3>div>div', monsterBuff[i].parentNode).innerText || '世界树 Yggdrasil' === gE('div.btm3>div>div', monsterBuff[i].parentNode).innerText)) { // 默认设置下,任何情况都优先击杀群体大量回血的boss"Yggdrasil"
- weight += yggdrasilExtraWeight; // yggdrasilExtraWeight.defalut -1000
- }
- for (j in skillLib) {
- if (gE(`img[src*="${skillLib[j].img}"]`, monsterBuff[i])) {
- weight += g('option').weight[j];
- }
- }
- monsterStatus[i].finWeight = weight;
- }
- monsterStatus.sort(objArrSort('finWeight'));
- battle.monsterStatus = monsterStatus;
- g('battle', battle);
- }
+ battle.roundAll = 1;
+ }
- function autoRecover() { // 自动回血回魔
- if (!g('option').item) {
- return false;
- }
- if (!g('option').itemOrderValue) {
- return false;
- }
- const name = g('option').itemOrderName.split(',');
- const order = g('option').itemOrderValue.split(',');
- for (let i = 0; i < name.length; i++) {
- if (g('option').item[name[i]] && checkCondition(g('option')[`item${name[i]}Condition`]) && isOn(order[i])) {
- isOn(order[i]).click();
- return true;
+ if (roundPrev !== battle.roundNow) {
+ battle.turn = 0;
+ setValue('skillOTOS', {});
}
+ battle.roundLeft = battle.roundAll - battle.roundNow;
+ setValue('battle', battle);
}
- return false;
- }
- function useScroll() { // 自动使用卷轴
- if (!g('option').scrollSwitch) {
- return false;
- }
- if (!g('option').scroll) {
- return false;
- }
- if (!checkCondition(g('option').scrollCondition)) {
- return false;
- }
- if (!g('option').scrollRoundType) {
- return false;
- }
- if (!g('option').scrollRoundType[g('battle').roundType]) {
- return false;
+ function killBug() { // 在 HentaiVerse 发生导致 turn 损失的 bug 时发出警告并移除问题元素: https://ehwiki.org/wiki/HentaiVerse_Bugs_%26_Errors#Combat
+ const bugLog = gE('#textlog > tbody > tr > td[class="tlb"]', 'all');
+ const isBug = /(Slot is currently not usable)|(Item does not exist)|(Inventory slot is empty)|(You do not have a powerup gem)/;
+ for (let i = 0; i < bugLog.length; i++) {
+ if (bugLog[i].textContent.match(isBug)) {
+ bugLog[i].className = 'tlbWARN';
+ setTimeout(() => { // 刷新移除问题元素,间隔时间以避免持续刷新
+ window.location.href = window.location.search ? window.location.pathname + window.location.search : window.location.href;
+ }, 700);
+ } else {
+ bugLog[i].className = 'tlbQRA';
+ }
+ }
}
- const scrollLib = {
- Go: {
- name: 'Scroll of the Gods',
- id: 13299,
- mult: '3',
- img1: 'absorb',
- img2: 'shadowveil',
- img3: 'sparklife',
- },
- Av: {
- name: 'Scroll of the Avatar',
- id: 13199,
- mult: '2',
- img1: 'haste',
- img2: 'protection',
- },
- Pr: {
- name: 'Scroll of Protection',
- id: 13111,
- mult: '1',
- img1: 'protection',
- },
- Sw: {
- name: 'Scroll of Swiftness',
- id: 13101,
- mult: '1',
- img1: 'haste',
- },
- Li: {
- name: 'Scroll of Life',
- id: 13221,
- mult: '1',
- img1: 'sparklife',
- },
- Sh: {
- name: 'Scroll of Shadows',
- id: 13211,
- mult: '1',
- img1: 'shadowveil',
- },
- Ab: {
- name: 'Scroll of Absorption',
- id: 13201,
- mult: '1',
- img1: 'absorb',
- },
- };
- const scrollFirst = (g('option').scrollFirst) ? '_scroll' : '';
- let isUsed;
- for (const i in scrollLib) {
- if (g('option').scroll[i] && gE(`.bti3>div[onmouseover*="${scrollLib[i].id}"]`) && checkCondition(g('option')[`scroll${i}Condition`])) {
- for (let j = 1; j <= scrollLib[i].mult; j++) {
- if (gE(`#pane_effects>img[src*="${scrollLib[i][`img${j}`]}${scrollFirst}"]`)) {
- isUsed = true;
- break;
+
+ function countMonsterHP() { // 统计敌人血量
+ let i, j;
+ const monsterHp = gE(`${monsterStateKeys.bars}:nth-child(1)`, 'all');
+ const monsterMp = gE(`${monsterStateKeys.bars}:nth-child(2)`, 'all');
+ const monsterSp = gE(`${monsterStateKeys.bars}:nth-child(3)`, 'all');
+ let battle = getValue('battle', true);
+ const monsterStatus = battle.monsterStatus;
+ const hpArray = [];
+ for (i = 0; i < monsterHp.length; i++) {
+ if (gE('img[src*="nbardead.png"]', monsterHp[i])) {
+ monsterStatus[i].isDead = true;
+ monsterStatus[i].hpNow = Infinity;
+ } else {
+ monsterStatus[i].isDead = false;
+ monsterStatus[i].hpNow = Math.floor(monsterStatus[i].hp * parseFloat(gE('img:first-child', monsterHp[i]).style.width) / 120 + 1);
+ monsterStatus[i].mpNow = parseFloat(gE('img:first-child', monsterMp[i]).style.width) / 120;
+ monsterStatus[i].spNow = parseFloat(gE('img:first-child', monsterSp[i]).style.width) / 120;
+ hpArray.push(monsterStatus[i].hpNow);
+ }
+ }
+ battle.monsterStatus = monsterStatus;
+
+ const monsterBuff = gE(monsterStateKeys.buffs, 'all');
+ const hpMin = Math.min.apply(null, hpArray);
+ const option = g().option;
+ const yggdrasilExtraWeight = option.YggdrasilExtraWeight;
+ const unreachableWeight = option.unreachableWeight;
+ const baseHpRatio = option.baseHpRatio;
+ // 权重越小,优先级越高
+ for (i = 0; i < monsterStatus.length; i++) { // 死亡的排在最后(优先级最低)
+ const target = monsterStatus[i];
+ if (target.isDead) {
+ target.finWeight = unreachableWeight;
+ continue;
+ }
+ let weight = baseHpRatio * Math.log10(target.hpNow / hpMin); // > 0 生命越低权重越低优先级越高
+ const name = gE(`${monsterStateKeys.name}>div>div`, monsterBuff[i].parentNode).innerText;
+ if (yggdrasilExtraWeight && ('Yggdrasil' === name || '世界树 Yggdrasil' === name)) { // 默认设置下,任何情况都优先击杀群体大量回血的boss"Yggdrasil"
+ weight += yggdrasilExtraWeight; // yggdrasilExtraWeight.defalut -1000
+ }
+ const known = {};
+ for (j in monsterBuffSkillLib) {
+ const skill = monsterBuffSkillLib[j];
+ if (!getBuff(skill.img, getMonsterID(target))) {
+ continue;
+ }
+ known[skill.img] = skill;
+ if (skill.elem && skill.elem !== g().attackStatus) {
+ weight += option.weight[`${j}1`] ?? 0;
+ continue;
}
- isUsed = false;
+ weight += option.weight[j] ?? 0;
}
- if (!isUsed) {
- gE(`.bti3>div[onmouseover*="${scrollLib[i].id}"]`).click();
- return true;
+
+ let unknown = gE(`img`, 'all', monsterBuff[i]);
+ if (unknown?.length) {
+ unknown = Array.from(unknown).filter(buff => {
+ const img = buff.src.match(/\/y\/e\/(.*)\.png/)[1];
+ return !(Object.keys(known).includes(img));
+ }).map(buff=>`${buff.getAttribute('onmouseover').match(/^battle.set_infopane_effect\('(.+)', *'.*',.+\)/)[1]}: ${buff.src.match(/\/y\/e\/(.*)\.png/)[1]}`);
+ if (unknown.length) {
+ console.log('unsupported buff weight:', unknown);
+ }
}
+ monsterStatus[i].finWeight = weight;
}
+ monsterStatus.sort(objArrSort('finWeight'));
+ battle.monsterStatus = monsterStatus;
+ g('battle', battle);
}
- return false;
- }
- function useChannelSkill() { // 自动施法Channel技能
- if (!g('option').channelSkillSwitch) {
- return false;
- }
- if (!g('option').channelSkill) {
- return false;
- }
- if (!gE('#pane_effects>img[src*="channeling"]')) {
- return false;
- }
- const skillLib = {
- Pr: {
- name: 'Protection',
- id: '411',
- img: 'protection',
- },
- SL: {
- name: 'Spark of Life',
- id: '422',
- img: 'sparklife',
- },
- SS: {
- name: 'Spirit Shield',
- id: '423',
- img: 'spiritshield',
- },
- Ha: {
- name: 'Haste',
- id: '412',
- img: 'haste',
- },
- AF: {
- name: 'Arcane Focus',
- id: '432',
- img: 'arcanemeditation',
- },
- He: {
- name: 'Heartseeker',
- id: '431',
- img: 'heartseeker',
- },
- Re: {
- name: 'Regen',
- id: '312',
- img: 'regen',
- },
- SV: {
- name: 'Shadow Veil',
- id: '413',
- img: 'shadowveil',
- },
- Ab: {
- name: 'Absorb',
- id: '421',
- img: 'absorb',
- },
- };
- let i; let
- j;
- const skillPack = g('option').buffSkillOrderValue.split(',');
- if (g('option').channelSkill) {
- for (i = 0; i < skillPack.length; i++) {
- j = skillPack[i];
- if (g('option').channelSkill[j] && !gE(`#pane_effects>img[src*="${skillLib[j].img}"]`) && isOn(skillLib[j].id)) {
- gE(skillLib[j].id).click();
- return true;
- }
+ function autoRecover(isCureOnly) { // 自动回血回魔
+ const option = g().option;
+ if (!option.item) {
+ return false;
}
- }
- if (g('option').channelSkill2 && g('option').channelSkill2OrderValue) {
- const order = g('option').channelSkill2OrderValue.split(',');
- for (i = 0; i < order.length; i++) {
- if (isOn(order[i])) {
- gE(order[i]).click();
+ const name = splitOrders(option.itemOrderName, ['FC', 'HE', 'LE', 'HG', 'HP', 'Cure', 'MG', 'MP', 'ME', 'SG', 'SP', 'SE', 'Mystic', 'CC', 'ED']);
+ const order = splitOrders(option.itemOrderValue, [313, 11199, 11501, 10005, 11195, 311, 10006, 11295, 11299, 10007, 11395, 11399, 10008, 11402, 11401]);
+ const cures = [313, 11199, 11501, 10005, 11195, 311];
+ for (let i = 0; i < name.length; i++) {
+ let id = order[i];
+ if (isCureOnly && !cures.includes(id)) {
+ continue;
+ }
+ if (option.item[name[i]] && checkCondition(option[`item${name[i]}Condition`]) && isOn(id)) {
+ (gE(`.bti3>div[onmouseover*="(${id})"]`) ?? gE(id)).click();
return true;
}
}
+ return false;
}
- const buff = gE('#pane_effects>img', 'all');
- if (buff.length > 0) {
- const name2Skill = {
- 'Protection': 'Pr',
- 'Spark of Life': 'SL',
- 'Spirit Shield': 'SS',
- 'Hastened': 'Ha',
- 'Arcane Focus': 'AF',
- 'Heartseeker': 'He',
- 'Regen': 'Re',
- 'Shadow Veil': 'SV',
+
+ function useScroll() { // 自动使用卷轴
+ const option = g().option;
+ if (!option.scrollSwitch) {
+ return false;
+ }
+ if (!option.scroll) {
+ return false;
+ }
+ if (!option.scrollRoundType) {
+ return false;
+ }
+ if (!option.scrollRoundType[g().battle.roundType]) {
+ return false;
+ }
+ if (!checkCondition(option.scrollCondition)) {
+ return false;
+ }
+ const scrollLib = {
+ Go: {
+ name: 'Scroll of the Gods',
+ id: 13299,
+ mult: '3',
+ img1: 'absorb',
+ img2: 'shadowveil',
+ img3: 'sparklife',
+ },
+ Av: {
+ name: 'Scroll of the Avatar',
+ id: 13199,
+ mult: '2',
+ img1: 'haste',
+ img2: 'protection',
+ },
+ Pr: {
+ name: 'Scroll of Protection',
+ id: 13111,
+ mult: '1',
+ img1: 'protection',
+ },
+ Sw: {
+ name: 'Scroll of Swiftness',
+ id: 13101,
+ mult: '1',
+ img1: 'haste',
+ },
+ Li: {
+ name: 'Scroll of Life',
+ id: 13221,
+ mult: '1',
+ img1: 'sparklife',
+ },
+ Sh: {
+ name: 'Scroll of Shadows',
+ id: 13211,
+ mult: '1',
+ img1: 'shadowveil',
+ },
+ Ab: {
+ name: 'Scroll of Absorption',
+ id: 13201,
+ mult: '1',
+ img1: 'absorb',
+ },
};
- for (i = 0; i < buff.length; i++) {
- const spellName = buff[i].getAttribute('onmouseover').match(/'(.*?)'/)[1];
- const buffLastTime = buff[i].getAttribute('onmouseover').match(/\(.*,.*, (.*?)\)$/)[1] * 1;
- if (isNaN(buffLastTime) || buff[i].src.match(/_scroll.png$/)) {
+ const scrollFirst = (option.scrollFirst) ? '_scroll' : '';
+ for (const i in scrollLib) {
+ if (!option.scroll[i]) {
continue;
- } else {
- if (spellName === 'Cloak of the Fallen' && !gE('#pane_effects>img[src*="sparklife"]') && isOn('422')) {
- gE('422').click();
- return true;
- } if (spellName in name2Skill && isOn(skillLib[name2Skill[spellName]].id)) {
- gE(skillLib[name2Skill[spellName]].id).click();
- return true;
+ }
+ if (!gE(`.bti3>div[onmouseover*="(${scrollLib[i].id})"]`)) {
+ continue;
+ }
+ if (!checkCondition(option[`scroll${i}Condition`])) {
+ continue;
+ }
+ for (let j = 1; j <= scrollLib[i].mult; j++) {
+ if (getBuff(scrollLib[i][`img${j}`] + scrollFirst)) {
+ continue;
}
+ gE(`.bti3>div[onmouseover*="(${scrollLib[i].id})"]`).click();
+ return true;
}
}
- }
- return false;
- }
-
- function useBuffSkill() { // 自动施法BUFF技能
- const skillLib = {
- Pr: {
- name: 'Protection',
- id: '411',
- img: 'protection',
- },
- SL: {
- name: 'Spark of Life',
- id: '422',
- img: 'sparklife',
- },
- SS: {
- name: 'Spirit Shield',
- id: '423',
- img: 'spiritshield',
- },
- Ha: {
- name: 'Haste',
- id: '412',
- img: 'haste',
- },
- AF: {
- name: 'Arcane Focus',
- id: '432',
- img: 'arcanemeditation',
- },
- He: {
- name: 'Heartseeker',
- id: '431',
- img: 'heartseeker',
- },
- Re: {
- name: 'Regen',
- id: '312',
- img: 'regen',
- },
- SV: {
- name: 'Shadow Veil',
- id: '413',
- img: 'shadowveil',
- },
- Ab: {
- name: 'Absorb',
- id: '421',
- img: 'absorb',
- },
- };
- if (!g('option').buffSkillSwitch) {
- return false;
- }
- if (!g('option').buffSkill) {
- return false;
- }
- if (!checkCondition(g('option').buffSkillCondition)) {
return false;
}
- let i;
- const skillPack = g('option').buffSkillOrderValue.split(',');
- for (i = 0; i < skillPack.length; i++) {
- let buff = skillPack[i];
- if (g('option').buffSkill[buff] && checkCondition(g('option')[`buffSkill${buff}Condition`]) && !gE(`#pane_effects>img[src*="${skillLib[buff].img}"]`) && isOn(skillLib[buff].id)) {
- gE(skillLib[buff].id).click();
- return true;
+
+ function useChannelSkill() { // 自动施法Channel技能
+ const option = g().option;
+ if (!option.channelSkillSwitch) {
+ return false;
}
- }
- const draughtPack = {
- HD: {
- id: 11191,
- img: 'healthpot',
- },
- MD: {
- id: 11291,
- img: 'manapot',
- },
- SD: {
- id: 11391,
- img: 'spiritpot',
- },
- FV: {
- id: 19111,
- img: 'flowers',
- },
- BG: {
- id: 19131,
- img: 'gum',
- },
- };
- for (i in draughtPack) {
- if (!gE(`#pane_effects>img[src*="${draughtPack[i].img}"]`) && g('option').buffSkill && g('option').buffSkill[i] && checkCondition(g('option')[`buffSkill${i}Condition`]) && gE(`.bti3>div[onmouseover*="${draughtPack[i].id}"]`)) {
- gE(`.bti3>div[onmouseover*="${draughtPack[i].id}"]`).click();
- return true;
+ if (!getBuff('channeling')) {
+ return false;
}
- }
- return false;
- }
- function useInfusions() { // 自动使用魔药
- if (g('attackStatus') === 0) {
- return false;
- }
- if (!g('option').infusionSwitch) {
- return false;
- }
- if (!checkCondition(g('option').infusionCondition)) {
- return false;
- }
+ playerBuffSkillLib.CF.id = getBuff('sparklife') ? undefined : 422;
+ if (option.channelSkill) {
+ const skillPack = splitOrders(option.buffSkillOrderValue, ['SS', 'SL', 'Pr', 'Ab', 'SV', 'Re', 'Ha', 'He', 'AF']);
+ for (const buff of skillPack) {
+ const current = getBuffTurnFromImg(getBuff(playerBuffSkillLib[buff].img));
+ const threshold = option.channelThreshold ? option.channelThreshold[buff] : 0;
+ if (threshold > 0 && current >= threshold) continue;
+ if (!option.channelSkill[buff] || getBuff(playerBuffSkillLib[buff].img)) continue;
+ if (!isOn(playerBuffSkillLib[buff].id)) continue;
+ gE(playerBuffSkillLib[buff].id).click();
+ return true;
+ }
+ }
+ if (option.channelSkill2) {
+ const order = splitOrders(option.channelSkill2OrderValue);
+ for (const id of order) {
+ const buffs = Object.keys(playerBuffSkillLib).filter(s => playerBuffSkillLib[s].id * 1 === 1 * id);
+ for (const buff of buffs) {
+ const current = getBuffTurnFromImg(getBuff(playerBuffSkillLib[buff].img));
+ const threshold = option.channelThreshold ? option.channelThreshold[buff] : 0;
+ if (threshold > 0 && current > threshold) continue;
+ }
+ if (!isOn(id)) continue;
+ gE(id).click();
+ return true;
+ }
+ }
+ if (option.channelRebuff) {
+ let minBuff, minTime;
+ for (const buff in playerBuffSkillLib) {
+ let current = getBuffTurnFromImg(getBuff(playerBuffSkillLib[buff].img));
+ const threshold = option.channelThreshold ? option.channelThreshold[buff] : 0;
+ if (threshold > 0 && current > threshold) continue;
- const infusionLib = [null, {
- id: 12101,
- img: 'fireinfusion',
- }, {
- id: 12201,
- img: 'coldinfusion',
- }, {
- id: 12301,
- img: 'elecinfusion',
- }, {
- id: 12401,
- img: 'windinfusion',
- }, {
- id: 12501,
- img: 'holyinfusion',
- }, {
- id: 12601,
- img: 'darkinfusion',
- }];
- if (gE(`.bti3>div[onmouseover*="${infusionLib[g('attackStatus')].id}"]`) && !gE(`#pane_effects>img[src*="${infusionLib[[g('attackStatus')]].img}"]`)) {
- gE(`.bti3>div[onmouseover*="${infusionLib[g('attackStatus')].id}"]`).click();
- return true;
- }
- return false;
- }
+ current = isNaN(current) ? 0 : current;
+ if (getBuff(playerBuffSkillLib[buff].img)?.src.match(/_scroll.png$/) || (minTime && current >= minTime)) {
+ continue;
+ }
+ if (!current && (!option.buffSkillSwitch || !option.buffSkill[buff])) continue;
- function autoFocus() {
- if (g('option').focus && checkCondition(g('option').focusCondition)) {
- gE('#ckey_focus').click();
- return true;
+ const id = playerBuffSkillLib[buff].id;
+ if (!isOn(id)) continue;
+ minBuff = id;
+ minTime = current;
+ }
+ if (minBuff) {
+ gE(minBuff).click();
+ return true;
+ }
+ }
+ return false;
}
- return false;
- }
- function autoSS() {
- if ((g('option').turnOnSS && checkCondition(g('option').turnOnSSCondition) && !gE('#ckey_spirit[src*="spirit_a"]')) || (g('option').turnOffSS && checkCondition(g('option').turnOffSSCondition) && gE('#ckey_spirit[src*="spirit_a"]'))) {
- gE('#ckey_spirit').click();
- return true;
- }
- return false;
- }
+ function useBuffSkill() { // 自动施法BUFF技能
+ const option = g().option;
+ if (!option.buffSkillSwitch) {
+ return false;
+ }
+ if (!option.buffSkill) {
+ return false;
+ }
+ if (!checkCondition(option.buffSkillCondition)) {
+ return false;
+ }
+ let i;
+ const skillPack = splitOrders(option.buffSkillOrderValue, ['SS', 'SL', 'Pr', 'Ab', 'SV', 'Re', 'Ha', 'He', 'AF']);
+ for (i = 0; i < skillPack.length; i++) {
+ let buff = skillPack[i];
+ if (!option.buffSkill[buff]) continue;
+ if (!isOn(playerBuffSkillLib[buff].id)) continue;
+ if (!checkCondition(option[`buffSkill${buff}Condition`])) continue;
+ const current = getBuffTurnFromImg(getBuff(playerBuffSkillLib[buff].img));
+ const threshold = option.buffSkillThreshold ? option.buffSkillThreshold[buff] : 0;
+ if (threshold >= 0 && current > threshold) continue;
+ gE(playerBuffSkillLib[buff].id).click();
+ return true;
+ }
- /**
- * INNAT / WEAPON SKILLS
- *
- * 优先释放先天和武器技能
- */
- function autoSkill() {
- if (!g('option').skillSwitch) {
- return false;
- }
- if (!gE('#ckey_spirit[src*="spirit_a"]')) {
+ const draughtPack = {
+ HD: {
+ id: 11191,
+ img: 'healthpot',
+ },
+ MD: {
+ id: 11291,
+ img: 'manapot',
+ },
+ SD: {
+ id: 11391,
+ img: 'spiritpot',
+ },
+ FV: {
+ id: 19111,
+ img: 'flowers',
+ },
+ BG: {
+ id: 19131,
+ img: 'gum',
+ },
+ };
+ for (i in draughtPack) {
+ if (!getBuff(draughtPack[i].img) && option.buffSkill && option.buffSkill[i] && checkCondition(option[`buffSkill${i}Condition`]) && gE(`.bti3>div[onmouseover*="(${draughtPack[i].id})"]`)) {
+ gE(`.bti3>div[onmouseover*="(${draughtPack[i].id})"]`).click();
+ return true;
+ }
+ }
return false;
}
- const skillOrder = (g('option').skillOrderValue || 'OFC,FRD,T3,T2,T1').split(',');
- const skillLib = {
- OFC: {
- id: '1111',
- oc: 8,
- },
- FRD: {
- id: '1101',
- oc: 4,
- },
- T3: {
- id: `2${g('option').fightingStyle}03`,
- oc: 2,
- },
- T2: {
- id: `2${g('option').fightingStyle}02`,
- oc: 2,
- },
- T1: {
- id: `2${g('option').fightingStyle}01`,
- oc: 2,
- },
- };
- const rangeSkills = {
- 2101: 2,
- 2403: 2,
- 1111: 4,
- }
- const monsterStatus = g('battle').monsterStatus;
- for (let i in skillOrder) {
- let skill = skillOrder[i];
- let range = 0;
- if (!checkCondition(g('option')[`skill${skill}Condition`])) {
- continue;
- }
- if (!isOn(skillLib[skill].id)) {
- continue;
- }
- if (g('oc') < skillLib[skill].oc) {
- continue;
- }
- if (g('option').skillOTOS && g('option').skillOTOS[skill] && g('skillOTOS')[skill] >= 1) {
- continue;
- }
- g('skillOTOS')[skill]++;
- gE(skillLib[skill].id).click();
- if (skillLib[skill].id in rangeSkills) {
- range = rangeSkills[skillLib[skill].id];
- }
- if (!g('option').mercifulBlow || g('option').fightingStyle !== '2' || skill !== 'T3') {
- continue;
- }
- // Merciful Blow
- for (let j = 0; j < monsterStatus.length; j++) {
- if (monsterStatus[j].hpNow / monsterStatus[j].hp < 0.25 && gE(`#mkey_${getMonsterID(monsterStatus[j])} img[src*="wpn_bleed"]`)) {
- gE(`#mkey_${getRangeCenterID(monsterStatus[j])}`).click();
+ function useInfusions() { // 自动使用魔药
+ const option = g().option;
+ if (!option.infusionSwitch) return false;
+ if (!checkCondition(option.infusionCondition)) {
+ return false;
+ }
+
+ const onUse = function(status) {
+ if (getBuff(infusionLib[status].img)) return false;
+ const itemBtn = gE(`.bti3>div[onmouseover*="(${infusionLib[status].id})"]`);
+ if (!itemBtn) return false;
+ itemBtn.click();
+ return true;
+ }
+ const infusionLib = [ null, {
+ id: 12101,
+ img: 'fireinfusion',
+ name: 'Flames',
+ }, {
+ id: 12201,
+ img: 'coldinfusion',
+ name: 'Frost',
+ }, {
+ id: 12301,
+ img: 'elecinfusion',
+ name: 'Lightning',
+ }, {
+ id: 12401,
+ img: 'windinfusion',
+ name: 'Storms',
+ }, {
+ id: 12501,
+ img: 'holyinfusion',
+ name: 'Divinity',
+ }, {
+ id: 12601,
+ img: 'darkinfusion',
+ name: 'Darkness',
+ }];
+
+ if (option.infusionDefaultOnly) {
+ const attackStatus = g().attackStatus;
+ if (attackStatus === 0) return false;
+ return onUse(attackStatus);
+ }
+ if (!option.infusion) return false;
+ const order = splitOrders(option.infusionOrderName, ['Divinity', 'Darkness', 'Flames', 'Frost', 'Lightning', 'Storms']);
+ for (const name of order) {
+ const condition = option[`infusion${name}Condition`];
+ if (!checkCondition(condition)) continue;
+ if (onUse(infusionLib.findIndex(i => i?.name === name))) {
return true;
}
}
- }
- gE(`#mkey_${getRangeCenterID(monsterStatus[0])}`).click();
- return true;
- }
-
- function useDeSkill() { // 自动施法DEBUFF技能
- if (!g('option').debuffSkillSwitch) { // 总开关是否开启
return false;
}
- // 先处理特殊的 “先给全体上buff”
- let skillPack = ['We', 'Im'];
- for (let i = 0; i < skillPack.length; i++) {
- if (g('option')[`debuffSkill${skillPack[i]}All`]) { // 是否启用
- continue;
- }
- if (!checkCondition(g('option')[`debuffSkill${skillPack[i]}AllCondition`])) { // 检查条件
- continue;
+
+ function autoFocus() {
+ const option = g().option;
+ if (option.focus && checkCondition(option.focusCondition)) {
+ gE('#ckey_focus').click();
+ return true;
}
- skillPack.splice(i, 1);
- i--;
- }
- skillPack.sort((x, y) => g('option').debuffSkillOrderValue.indexOf(x) - g('option').debuffSkillOrderValue.indexOf(y))
- let toAllCount = skillPack.length;
- if (g('option').debuffSkill) { // 是否有启用的buff(不算两个特殊的)
- skillPack = skillPack.concat(g('option').debuffSkillOrderValue.split(','));
+ return false;
}
- for (let i in skillPack) {
- let buff = skillPack[i];
- if (i >= toAllCount && !skillPack[i]) { // 检查buff是否启用
- continue;
- }
- if (!checkCondition(g('option')[`debuffSkill${buff}Condition`])) { // 检查条件
- continue;
+
+ function autoSS(isDisableOnly) {
+ const textSP = gE('#vrs') ?? gE('#dvrs');
+ const spValue = textSP.childNodes[0].textContent * 1;
+ if (spValue <= 1) {
+ return false;
}
- let succeed = useDebuffSkill(skillPack[i], i < toAllCount);
- // 前 toAllCount 个都是先给全体上的
- if (succeed) {
+ const option = g().option;
+ const enabled = gE('#ckey_spirit[src*="spirit_a"]');
+ if (
+ (!isDisableOnly && option.turnOnSS && checkCondition(option.turnOnSSCondition) && !enabled)
+ || (option.turnOffSS && checkCondition(option.turnOffSSCondition) && enabled)
+ ) {
+ gE('#ckey_spirit').click();
return true;
}
+ return false;
}
- return false;
- }
- function useDebuffSkill(buff, isAll = false) {
- const skillLib = {
- Sle: {
- name: 'Sleep',
- id: '222',
- img: 'sleep',
- },
- Bl: {
- name: 'Blind',
- id: '231',
- img: 'blind',
- },
- Slo: {
- name: 'Slow',
- id: '221',
- img: 'slow',
- },
- Im: {
- name: 'Imperil',
- id: '213',
- img: 'imperil',
- range: { 4204: [0, 0, 0, 1] },
- },
- MN: {
- name: 'MagNet',
- id: '233',
- img: 'magnet',
- },
- Si: {
- name: 'Silence',
- id: '232',
- img: 'silence',
- },
- Dr: {
- name: 'Drain',
- id: '211',
- img: 'drainhp',
- },
- We: {
- name: 'Weaken',
- id: '212',
- img: 'weaken',
- range: { 4202: [0, 0, 0, 1] },
- },
- Co: {
- name: 'Confuse',
- id: '223',
- img: 'confuse',
- },
- };
+ async function clickMonster(id) {
+ if (!unsafeWindow.battle) {
+ console.log('loadUnsafeWindowBattle before click monster');
+ await loadUnsafeWindowBattle();
+ }
+ getMonster(id).click();
+ }
+
+ /**
+ * INNAT / WEAPON SKILLS
+ *
+ * 优先释放先天和武器技能
+ */
+ function autoSkill() {
+ const option = g().option;
+ if (!option.skillSwitch) {
+ return false;
+ }
+ // if (!gE('#ckey_spirit[src*="spirit_a"]')) {
+ // return false;
+ // }
+ if (!option.skill) return false;
+
+ const skillOrder = splitOrders(option.skillOrderValue, ['OFC', 'FRD', 'T3', 'T2', 'T1']);
+ const fightStyle = g().fightingStyle; // 1二天 2单手 3双手 4双持 5法杖
+ const skillLib = {
+ OFC: 1111,
+ FRD: 1101,
+ T3: fightStyle ? `2${fightStyle}03`*1 : undefined,
+ T2: fightStyle ? `2${fightStyle}02`*1 : undefined,
+ T1: fightStyle ? `2${fightStyle}01`*1 : undefined,
+ };
+ const skillInfos = {
+ 1101: { oc: 4, range: 10 },
+ 1111: { oc: 8, range: 10 },
+ 2101: { oc: 4, range: 5 },
+ 2201: { oc: 1, },
+ 2203: { oc: 4, },
+ 2302: { range: 5 },
+ 2303: { range: 5 },
+ 2403: { oc: 3, }
+ }
+ const monsterStatus = g().battle.monsterStatus;
+ for (let i in skillOrder) {
+ let skill = skillOrder[i];
+ if (!skill || !option.skill[skill]) {
+ return;
+ }
+ let id = skillLib[skill];
+ if (!isOn(id)) {
+ continue;
+ }
+ if (g().oc < (skillInfos[id]?.oc ?? 2)) {
+ continue;
+ }
+ const skillOTOS = getValue('skillOTOS', true) ?? {};
+ skillOTOS[skill] ??= 0;
+ if (option.skillOTOS && option.skillOTOS[skill] && skillOTOS[skill] >= 1) {
+ continue;
+ }
+ skillOTOS[skill]++;
+ setValue('skillOTOS', skillOTOS);
+ let target = checkCondition(option[`skill${skill}Condition`], monsterStatus);
+ if (!target) {
+ continue;
+ }
+ gE(id).click();
- if (!isOn(skillLib[buff].id)) { // 技能不可用
+ clickMonster(getRangeCenter(target, skillInfos[id]?.range ?? 1).id);
+ return true;
+ }
return false;
}
- const monsterStatus = g('battle').monsterStatus;
- let isDebuffed = (target) => gE(`img[src*="${skillLib[buff].img}"]`, gE(`#mkey_${getMonsterID(target)}>.btm6`));
- let primaryTarget;
- let max = isAll ? monsterStatus.length : 1;
- for (let i = 0; i < max; i++) {
- let target = buff === 'Dr' ? monsterStatus[max - i - 1] : monsterStatus[i];
- if (monsterStatus[i].isDead) {
- continue;
- }
- if (isDebuffed(target)) { // 检查是否已有该buff
- continue;
- }
- primaryTarget = target;
- break;
- }
- if (primaryTarget === undefined) {
+
+ function useDeSkill() { // 自动施法DEBUFF技能
+ const option = g().option;
+ const monsterStatus = g().battle.monsterStatus;
+ if (!option.debuffSkillSwitch || !checkCondition(option.debuffSkillCondition, monsterStatus)) { // 总开关是否开启
+ return false;
+ }
+
+ // 先处理特殊的 “先给全体上buff”
+ let skillPack = splitOrders(option.debuffSkillOrderAllValue, ['Sle', 'Bl', 'We', 'Si', 'Slo', 'Dr', 'Im', 'MN', 'Co']);
+ for (let i = 0; i < skillPack.length; i++) {
+ if (option[`debuffSkill${skillPack[i]}All`]) { // 是否启用
+ if (checkCondition(option[`debuffSkill${skillPack[i]}AllCondition`], monsterStatus)) { // 检查条件
+ continue;
+ }
+ }
+ skillPack.splice(i, 1);
+ i--;
+ }
+ const toAllCount = skillPack.length;
+
+ if (option.debuffSkill) { // 是否有启用的buff(不算两个特殊的)
+ skillPack = skillPack.concat(splitOrders(option.debuffSkillOrderValue, ['Sle', 'Bl', 'We', 'Si', 'Slo', 'Dr', 'Im', 'MN', 'Co']));
+ }
+ for (let i in skillPack) {
+ let buff = skillPack[i];
+ const isToAll = i < toAllCount;
+ if (!isToAll) { // 非先全体
+ if (!buff || !option.debuffSkill[buff] || !checkCondition(option[`debuffSkill${buff}Condition`], monsterStatus)) { // 检查条件
+ continue;
+ }
+ }
+ let succeed = useDebuffSkill(buff, isToAll);
+ // 前 toAllCount 个都是先给全体上的
+ if (succeed) {
+ return true;
+ }
+ }
return false;
}
- let range = 0;
- let ab;
- for (ab in skillLib[buff].range) {
- const ranges = skillLib[buff].range[ab][skillLib[buff].skill * 1];
- if (!ranges) {
- continue;
+ function useDebuffSkill(buff, isAll = false) {
+ const skill = monsterBuffSkillLib[buff];
+ if (!isOn(skill.id)) { // 技能不可用
+ return false;
}
- range = ranges[getValue('ability', true)[ab].level];
- break;
- }
- let id = getRangeCenterID(primaryTarget, range, isDebuffed);
- const imgs = gE('img', 'all', gE(`#mkey_${id}>.btm6`));
- // 已有buff小于6个
- // 未开启debuff失败警告
- // buff剩余持续时间大于等于警报时间
- if (imgs.length < 6 || !g('option').debuffSkillTurnAlert || (g('option').debuffSkillTurn && imgs[imgs.length - 1].getAttribute('onmouseover').match(/\(.*,.*, (.*?)\)$/)[1] * 1 >= g('option').debuffSkillTurn[buff])) {
- gE(skillLib[buff].id).click();
- gE(`#mkey_${id}`).click();
+ // 获取范围
+ let range = 1;
+ let ab;
+ const ability = getValue('ability', true);
+ for (ab in skill.range) {
+ const ranges = skill.range[ab];
+ if (!ranges) {
+ continue;
+ }
+ range = ranges[ability ? ability[ab] ?? 0 : 0];
+ break;
+ }
+ // 获取目标
+ const option = g().option;
+ // TODO TBD 权重倍率 额外结合剩余turns数
+ const excludedRatio = 0.9
+ let exclusiveBuffs;
+ if (isAll && option.debuffAllExclusive) {
+ exclusiveBuffs = Object.keys(option.debuffAllExclusive);
+ exclusiveBuffs = exclusiveBuffs?.includes(buff) ? exclusiveBuffs : undefined
+ }
+ let isDebuffed = (target, b) => {
+ if (b || !exclusiveBuffs) {
+ const current = getBuffTurnFromImg(getBuff(monsterBuffSkillLib[b ?? buff].img, getMonsterID(target)));
+ const threshold = option.debuffSkillThreshold ? option.debuffSkillThreshold[b ?? buff] : 0;
+ return threshold >= 0 && current > threshold;
+ }
+ for (const exclusive of exclusiveBuffs) {
+ if (isDebuffed(target, exclusive)) return excludedRatio;
+ }
+ return 0;
+ };
+ let debuffByIndex = isAll && option[`debuffSkill${buff}AllByIndex`];
+ let monsterStatus = g().battle.monsterStatus;
+ if (debuffByIndex) {
+ monsterStatus = JSON.parse(JSON.stringify(monsterStatus));
+ monsterStatus.sort(objArrSort('order'));
+ }
+ let max = isAll ? monsterStatus.length : 1;
+ let id;
+ let minWeight = Number.MAX_SAFE_INTEGER;
+ const condition = option[`debuffSkill${buff}${isAll ? 'All' : ''}Condition`];
+ const excludeCondition = target => checkCondition(condition, [target]) ? isDebuffed(target) : excludedRatio;
+ for (let i = 0; i < max; i++) {
+ let target = buff === 'Dr' ? monsterStatus[max - i - 1] : monsterStatus[i];
+ target = checkCondition(condition, [target]);
+ if (!target || target.isDead || isDebuffed(target)) {
+ continue;
+ }
+ const center = getRangeCenter(target, range, false, excludeCondition, debuffByIndex);
+ if (!id || center.weight < minWeight) {
+ minWeight = center.weight;
+ id = center.id;
+ if (!isAll) break; // 只有覆盖全体才需要遍历全部
+ }
+ }
+ if (id === undefined) {
+ return false;
+ }
+ const imgs = gE('img', 'all', gE(monsterStateKeys.buffs, getMonster(id)));
+ const buffs = Object.fromEntries(Array.from(imgs).map(img=> [img.src.match(/\/y\/e\/(.*)\.png/)[1], img]));
+ // 已有buff小于6个
+ // 未开启debuff失败警告
+ // buff剩余持续时间大于等于警报时间
+ if (imgs.length >= 6) {
+ switch (option.debuffSkillTurnAlert * 1) {
+ case 1:
+ if ((option.debuffSkillTurn && (getBuffTurnFromImg(buffs[skill.img]) ?? 0) >= option.debuffSkillTurn[buff])) {
+ return false;
+ }
+ _alert(0, '无法正常施放DEBUFF技能,请尝试手动打怪', '無法正常施放DEBUFF技能,請嘗試手動打怪', 'Can not cast de-skills normally, continue the script?\nPlease try attack manually.');
+ pauseChange();
+ return true;
+ case 2:
+ break;
+ default: // case 0, "" or undefined
+ return false;
+ }
+ }
+ gE(skill.id).click();
+ clickMonster(id);
return true;
}
- _alert(0, '无法正常施放DEBUFF技能,请尝试手动打怪', '無法正常施放DEBUFF技能,請嘗試手動打怪', 'Can not cast de-skills normally, continue the script?\nPlease try attack manually.');
- pauseChange();
- return true;
- }
-
- function attack() { // 自动打怪
- // 如果
- // 1. 开启了自动以太水龙头
- // 2. 目标怪在魔力合流状态中
- // 3. 未获得以太水龙头*2 或 *1
- // 4. 满足条件
- // 使用物理普通攻击,跳过Offensive Magic
- // 否则按照属性攻击模式释放Spell > Offensive Magic
-
- const updateAbility = {
- 4301: { //火
- 111: [0, 1, 1, 2, 2, 2, 2, 2],
- 112: [0, 0, 2, 2, 2, 2, 3, 3],
- 113: [0, 0, 0, 0, 3, 4, 4, 4]
- },
- 4302: { //冰
- 121: [0, 1, 1, 2, 2, 2, 2, 2],
- 122: [0, 0, 2, 2, 2, 2, 3, 3],
- 123: [0, 0, 0, 0, 3, 4, 4, 4]
- },
- 4303: { //雷
- 131: [0, 1, 1, 2, 2, 2, 2, 2],
- 132: [0, 0, 2, 2, 2, 2, 3, 3],
- 133: [0, 0, 0, 0, 3, 4, 4, 4]
- },
- 4304: { //雷
- 141: [0, 1, 1, 2, 2, 2, 2, 2],
- 142: [0, 0, 2, 2, 2, 2, 3, 3],
- 143: [0, 0, 0, 0, 3, 4, 4, 4]
- },
- //暗
- 4401: { 161: [0, 1, 2] },
- 4402: { 162: [0, 2, 3] },
- 4403: { 163: [0, 3, 4, 4] },
- //圣
- 4501: { 151: [0, 1, 2] },
- 4502: { 152: [0, 2, 3] },
- 4503: { 153: [0, 3, 4, 4] },
+ function getCurrentAttackStatus() {
+ if (g().attackStatusCurrent === undefined) {
+ attack(true);
+ }
+ const current = g().attackStatusCurrent;
+ g('attackStatusCurrent', undefined);
+ return current;
}
- let range = 0;
- // Spell > Offensive Magic
- const attackStatus = g('attackStatus');
- const monsterStatus = g('battle').monsterStatus;
- if (attackStatus === 0) {
- if (g('option').fightingStyle === '1') { // 二天一流
- range = 1;
- }
- } else {
- if (g('option').etherTap && gE(`#mkey_${getMonsterID(monsterStatus[0])}>div.btm6>img[src*="coalescemana"]`) && (!gE('#pane_effects>img[onmouseover*="Ether Tap (x2)"]') || gE('#pane_effects>img[src*="wpn_et"][id*="effect_expire"]')) && checkCondition(g('option').etherTapCondition)) {
- `pass`
- }
- else {
- const skill = 1 * (() => {
- let lv = 3;
- for (let condition of [g('option').highSkillCondition, g('option').middleSkillCondition, undefined]) {
- let id = `1${attackStatus}${lv--}`;
- if (checkCondition(condition) && isOn(id)) return id;
+ function attack(selectStatusOnly=false) { // 自动打怪
+ let range = g().fightingStyle === '1' ? 3 : 1;
+ const option = g().option;
+ const monsters = g().battle.monsterStatus;
+ let attackStatusOrder = option.attackStatusOrderValue?.split(',').map(x=>x*1) ?? [];
+ attackStatusOrder = attackStatusOrder.concat([0,6,5,1,2,4,3].filter(x=> !(attackStatusOrder.includes(x))));
+ if (option.attackStatusSwitch) {
+ for (const status of attackStatusOrder) {
+ if (!option.attackStatusSwitch[status]) continue;
+ const current = g().attackStatusCurrent;
+ g('attackStatusCurrent', status);
+ if (checkCondition(option[`attackStatusSwitchCondition${status}`], monsters) && onAttack(range, status, selectStatusOnly)) {
+ return true;
}
- })();
- gE(skill)?.click();
- for (let ab in updateAbility) {
- const ranges = updateAbility[ab][skill];
- if (!ranges) {
- continue;
+ g('attackStatusCurrent', current);
+ }
+ }
+ g('attackStatusCurrent', 0);
+ return onAttack(range, g().attackStatus, selectStatusOnly);
+ }
+
+ function onAttack(range, attackStatus, selectStatusOnly=false) {
+ const updateAbility = {
+ 4301: { //火
+ 111: [3, 4, 4, 5, 5, 5, 5, 5],
+ 112: [4, 4, 6, 6, 6, 6, 7, 7],
+ 113: [7, 7, 7, 7, 8, 9, 9, 10]
+ },
+ 4302: { //冰
+ 121: [3, 4, 4, 5, 5, 5, 5, 5],
+ 122: [4, 4, 6, 6, 6, 6, 7, 7],
+ 123: [7, 7, 7, 7, 8, 9, 9, 10]
+ },
+ 4303: { //雷
+ 131: [3, 4, 4, 5, 5, 5, 5, 5],
+ 132: [4, 4, 6, 6, 6, 6, 7, 7],
+ 133: [7, 7, 7, 7, 8, 9, 9, 10]
+ },
+ 4304: { //雷
+ 141: [3, 4, 4, 5, 5, 5, 5, 5],
+ 142: [4, 4, 6, 6, 6, 6, 7, 7],
+ 143: [7, 7, 7, 7, 8, 9, 9, 10]
+ },
+ //暗
+ 4401: { 161: [3, 4, 5] },
+ 4402: { 162: [5, 6, 7] },
+ 4403: { 163: [7, 8, 9, 10] },
+ //圣
+ 4501: { 151: [3, 4, 5] },
+ 4502: { 152: [5, 6, 7] },
+ 4503: { 153: [7, 8, 9, 10] },
+ }
+
+ // 如果
+ // 1. 开启了自动以太水龙头
+ // 2. 目标怪在魔力合流状态中
+ // 3. 未获得以太水龙头*2 或 *1
+ // 4. 满足条件
+ // 使用物理普通攻击,跳过Offensive Magic
+ // 否则按照属性攻击模式释放Spell > Offensive Magic
+
+ const option = g().option;
+ const monsters = g().battle.monsterStatus;
+ let target = monsters[0];
+ const tryAttack = (skill) => {
+ if (!target || target.isDead) {
+ return false;
+ }
+ if (selectStatusOnly) {
+ return true;
+ }
+ if (skill) {
+ gE(skill)?.click();
+ }
+ clickMonster(getRangeCenter(target, range, !attackStatus).id);
+ return true;
+ };
+ // 1. physical
+ if (attackStatus === 0) {
+ return tryAttack();
+ }
+
+ // 2. etherTap
+ if (option.etherTap && getBuff('coalescemana', getMonsterID(target))
+ && (!gE('#pane_effects>img[onmouseover*="Ether Tap (x2)"]') || getBuff(`wpn_et"][id*="effect_expire`))
+ && checkCondition(option.etherTapCondition)) {
+ return tryAttack();
+ }
+ // 2.5 try check skill condition
+ const skill = 1 * (() => {
+ let lv = 3;
+ for (let condition of [option.highSkillCondition, option.middleSkillCondition, option.lowSkillCondition]) {
+ let id = `1${attackStatus}${lv--}`;
+ target = checkCondition(condition, monsters);
+ if (target && isOn(id)) {
+ return id;
}
- range = ranges[getValue('ability', true)[ab]?.level ?? 0];
- break;
}
+ })();
+ // 3. no skill available
+ if (!skill) {
+ return tryAttack();
+ }
+ // 4. cast skill
+ for (let ab in updateAbility) {
+ const ranges = updateAbility[ab][skill];
+ if (!ranges) {
+ continue;
+ }
+ const ability = getValue('ability', true);
+ range = ranges[ability ? ability[ab] ?? 0 : 0];
+ break;
}
+ if (!tryAttack(skill)) {
+ return false;
+ }
+ const skillOTOS = getValue('skillOTOS', true) ?? {};
+ skillOTOS[skill] ??= 0;
+ skillOTOS[skill]++;
+ setValue('skillOTOS', skillOTOS);
+ return true;
}
- gE(`#mkey_${getRangeCenterID(monsterStatus[0], range, !attackStatus)}`).click();
- return true;
- }
- function getHPFromMonsterDB(mdb, name, lv) {
- let hp = (mdb && mdb[name]) ? mdb[name][lv] : undefined;
- // TODO: 根据lv模糊推测
- return hp;
- }
+ function getHPFromMonsterDB(mdb, name, lv) {
+ let hp = (mdb && mdb[name]) ? mdb[name][lv] : undefined;
+ // TODO TBD 根据lv模糊推测(一般数据都是等级逐渐提升的,可能可以直接用缓存而不需要推测,异世界新赛季时可以自动刷新缓存?)
+ return hp;
+ }
- function fixMonsterStatus() { // 修复monsterStatus
- // document.title = _alert(-1, 'monsterStatus错误,正在尝试修复', 'monsterStatus錯誤,正在嘗試修復', 'monsterStatus Error, trying to fix');
- const monsterStatus = [];
- const monsterNames = Array.from(gE('div.btm3>div>div', 'all')).map(monster => monster.innerText);
- const monsterLvs = Array.from(gE('div.btm2>div>div', 'all')).map(monster => monster.innerText);
- const monsterDB = getValue('monsterDB', true);
- gE('div.btm2', 'all').forEach((monster, order) => {
- monsterStatus.push({
- order: order,
- hp: getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? ((monster.style.background === '') ? 1000 : 100000),
+ function fixMonsterStatus() { // 修复monsterStatus
+ // document.title = _alert(-1, 'monsterStatus错误,正在尝试修复', 'monsterStatus錯誤,正在嘗試修復', 'monsterStatus Error, trying to fix');
+ const monsterStatus = [];
+ const monsterNames = Array.from(gE(`${monsterStateKeys.name}>div>div`, 'all')).map(monster => monster.innerText);
+ const monsterLvs = Array.from(gE(`${monsterStateKeys.lv}>div>div`, 'all')).map(monster => monster.innerText);
+ const monsterDB = getValue('monsterDB', true);
+ gE(monsterStateKeys.lv, 'all').forEach((monster, order) => {
+ monsterStatus.push({
+ order: order,
+ hp: getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? ((monster.style.background === '') ? 1000 : 100000),
+ });
});
- });
- const battle = getValue('battle', true);
- battle.monsterStatus = monsterStatus;
- setValue('battle', battle);
- }
+ const battle = getValue('battle', true);
+ battle.monsterStatus = monsterStatus;
+ setValue('battle', battle);
+ }
- function displayMonsterWeight() {
+ function displayMonsterWeight() {
- const status = g('battle').monsterStatus.filter(m => !m.isDead);
- let rank = 0;
+ const status = g().battle.monsterStatus.filter(m => !m.isDead);
+ let rank = 0;
- const weights = [];
- status.forEach(s => {
- if (weights.indexOf(s.finWeight) !== -1) {
- return;
- }
- weights.push(s.finWeight);
- })
- const sec = Math.max(1, weights.length - 1);
- const max = 360 * 2 / 3;
- const colorTextList = [];
- if (g('option').weightBackground) {
+ const weights = [];
status.forEach(s => {
- const rank = weights.indexOf(s.finWeight);
- let colorText = (g('option').weightBackground[rank + 1] ?? [])[0];
- colorTextList[rank] = colorText;
+ if (weights.indexOf(s.finWeight) !== -1) {
+ return;
+ }
+ weights.push(s.finWeight);
});
- }
- status.forEach(s => {
- const rank = weights.indexOf(s.finWeight);
- const id = getMonsterID(s);
- if (!gE(`#mkey_${id}`) || !gE(`#mkey_${id}>.btm3`)) {
- return;
+ const sec = Math.max(1, weights.length - 1);
+ const max = 360 * 2 / 3;
+ const colorTextList = [];
+ const weightBG = g().option.weightBackground
+ if (weightBG) {
+ status.forEach(s => {
+ const rank = weights.indexOf(s.finWeight);
+ let colorText = (weightBG[rank + 1] ?? [])[0];
+ colorTextList[rank] = colorText;
+ });
}
- if (g('option').displayWeightBackground) {
- if (g('option').weightBackground) {
+ status.forEach(s => {
+ const rank = weights.indexOf(s.finWeight);
+ const id = getMonsterID(s);
+ if (!getMonster(id) || !gE(monsterStateKeys.name, getMonster(id))) {
+ return;
+ }
+ if (g().option.displayWeightBackground && weightBG) {
let colorText = colorTextList[rank];
let remainAttemp = 10; // 避免无穷递归
- while(remainAttemp > 0 && colorText && colorText.indexOf(``, colorTextList[i]);
+ while (remainAttemp > 0 && colorText && colorText.indexOf(``, colorTextList[i]);
}
remainAttemp--;
}
try {
colorText = eval(colorText.replace('', rank).replace('', weights.length));
}
- catch {
- }
- gE(`#mkey_${id}`).style.cssText += `background: ${colorText};`;
+ catch { }
+ getMonster(id).style.cssText += `background: ${colorText};`;
}
- }
- gE(`#mkey_${id}>.btm3`).style.cssText += 'display: flex; flex-direction: row;'
- if (g('option').displayWeight) {
- gE(`#mkey_${id}>.btm3`).innerHTML += ` [${rank}|-${-rank + weights.length - 1}|${s.finWeight.toPrecision(s.finWeight >= 1 ? 5 : 4)}] `;
- }
- });
- }
+ gE(monsterStateKeys.name, getMonster(id)).style.cssText += 'display: flex; flex-direction: row;'
+ if (g().option.displayWeight) {
+ gE(monsterStateKeys.name, getMonster(id)).innerHTML += `[${rank}|-${-rank + weights.length - 1}|${s.finWeight.toPrecision(s.finWeight >= 1 ? 5 : 4)}] `;
+ }
+ });
+ }
- function displayPlayStatePercentage() {
- const barHP = gE('#vbh') ?? gE('#dvbh');
- const barMP = gE('#vbm') ?? gE('#dvbm');
- const barSP = gE('#vbs') ?? gE('#dvbs');
- const barOC = gE('#dvbc');
- const textHP = gE('#vrhd') ?? gE('#dvrhd');
- const textMP = gE('#vrm') ?? gE('#dvrm');
- const textSP = gE('#vrs') ?? gE('#dvrs');
- const textOC = gE('#dvrc');
-
- const percentages = [barHP, barMP, barSP, barOC].filter(bar => bar).map(bar => Math.floor((gE('div>img', bar).offsetWidth / bar.offsetWidth) * 100));
- [textHP, textMP, textSP, textOC].filter(bar => bar).forEach((text, i) => {
- const value = text.innerHTML * 1;
- const percentage = value ? percentages[i] : 0;
- const inner = `[${percentage.toString()}%]`;
- const percentageDiv = gE('div', text);
- if (percentageDiv) {
- percentageDiv.innerHTML = inner;
- return;
- }
- text.innerHTML += `${inner} `
- });
- }
+ `
+ const inner = `[${percentages[i].toString()}%]`;
+ if (percentageDiv) {
+ percentageDiv.innerHTML = inner;
+ percentageDiv.style.cssText = style;
+ return;
+ }
+ text.innerHTML += `${inner} `
+ });
+ }
- function dropMonitor(battleLog) { // 掉落监测
- const drop = getValue('drop', true) || {
- '#startTime': time(3),
- '#EXP': 0,
- '#Credit': 0,
- };
- let item; let name; let amount; let
- regexp;
- for (let i = 0; i < battleLog.length; i++) {
- if (/^You gain \d+ (EXP|Credit)/.test(battleLog[i].textContent)) {
- regexp = battleLog[i].textContent.match(/^You gain (\d+) (EXP|Credit)/);
- if (regexp) {
- drop[`#${regexp[2]}`] += regexp[1] * 1;
- }
- } else if (gE('span', battleLog[i])) {
- item = gE('span', battleLog[i]);
- name = item.textContent.match(/^\[(.*?)\]$/)[1];
- if (item.style.color === 'rgb(255, 0, 0)') {
- const quality = ['Crude', 'Fair', 'Average', 'Superior', 'Exquisite', 'Magnificent', 'Legendary', 'Peerless'];
- for (let j = g('option').dropQuality; j < quality.length; j++) {
- if (name.match(quality[j])) {
- name = `Equipment of ${name.match(/^\w+/)[0]}`;
- drop[name] = (name in drop) ? drop[name] + 1 : 1;
- break;
- }
- }
- } else if (item.style.color === 'rgb(186, 5, 180)') {
- regexp = name.match(/^(\d+)x (Crystal of \w+)$/);
+ function dropMonitor(battleLog) { // 掉落监测
+ const drop = getValue('drop', true) || {
+ '#startTime': time(3),
+ '#EXP': 0,
+ '#Credit': 0,
+ };
+ let item, name, amount, regexp;
+ for (let i = 0; i < battleLog.length; i++) {
+ if (/^You gain \d+ (EXP|Credit)/.test(battleLog[i].textContent)) {
+ regexp = battleLog[i].textContent.match(/^You gain (\d+) (EXP|Credit)/);
if (regexp) {
- name = regexp[2];
- amount = regexp[1] * 1;
+ drop[`#${regexp[2]}`] += regexp[1] * 1;
+ }
+ } else if (gE('span', battleLog[i])) {
+ item = gE('span', battleLog[i]);
+ name = item.textContent.match(/^\[(.*?)\]$/)[1];
+ if (item.style.color === 'rgb(255, 0, 0)') {
+ const quality = ['Crude', 'Fair', 'Average', 'Superior', 'Exquisite', 'Magnificent', 'Legendary', 'Peerless'];
+ for (let j = g().option.dropQuality; j < quality.length; j++) {
+ if (name.match(quality[j])) {
+ name = `Equipment of ${name.match(/^\w+/)[0]}`;
+ drop[name] = (name in drop) ? drop[name] + 1 : 1;
+ break;
+ }
+ }
+ } else if (item.style.color === 'rgb(186, 5, 180)') {
+ regexp = name.match(/^(\d+)x (Crystal of \w+)$/);
+ if (regexp) {
+ name = regexp[2];
+ amount = regexp[1] * 1;
+ } else {
+ name = name.match(/^(Crystal of \w+)$/)[1];
+ amount = 1;
+ }
+ drop[name] = (name in drop) ? drop[name] + amount : amount;
+ } else if (item.style.color === 'rgb(168, 144, 0)') {
+ drop['#Credit'] = drop['#Credit'] + name.match(/\d+/)[0] * 1;
} else {
- name = name.match(/^(Crystal of \w+)$/)[1];
- amount = 1;
+ drop[name] = (name in drop) ? drop[name] + 1 : 1;
}
- drop[name] = (name in drop) ? drop[name] + amount : amount;
- } else if (item.style.color === 'rgb(168, 144, 0)') {
- drop['#Credit'] = drop['#Credit'] + name.match(/\d+/)[0] * 1;
- } else {
- drop[name] = (name in drop) ? drop[name] + 1 : 1;
+ } else if (battleLog[i].textContent === 'You are Victorious!') {
+ break;
}
- } else if (battleLog[i].textContent === 'You are Victorious!') {
- break;
+ }
+ const battle = g().battle;
+ if (g().option.recordEach && battle.roundNow === battle.roundAll) {
+ const old = getValue('dropOld', true) || [];
+ drop.__name = getValue('battleCode', true).name;
+ drop['#endTime'] = time(3);
+ old.push(drop);
+ setValue('dropOld', old);
+ delValue('drop');
+ } else {
+ setValue('drop', drop);
}
}
- const battle = g('battle');
- if (g('option').recordEach && battle.roundNow === battle.roundAll) {
- const old = getValue('dropOld', true) || [];
- drop.__name = getValue('battleCode');
- drop['#endTime'] = time(3);
- old.push(drop);
- setValue('dropOld', old);
- delValue('drop');
- } else {
- setValue('drop', drop);
- }
- }
- function recordUsage(parm) {
- const stats = getValue('stats', true) || {
- self: {
- _startTime: time(3),
- _turn: 0,
- _round: 0,
- _battle: 0,
- _monster: 0,
- _boss: 0,
- evade: 0,
- miss: 0,
- focus: 0,
- },
- restore: { // 回复量
- },
- items: { // 物品使用次数
- },
- magic: { // 技能使用次数
- },
- damage: { // 技能攻击造成的伤害
- },
- hurt: { // 受到攻击造成的伤害
- mp: 0,
- oc: 0,
- _avg: 0,
- _count: 0,
- _total: 0,
- _mavg: 0,
- _mcount: 0,
- _mtotal: 0,
- _pavg: 0,
- _pcount: 0,
- _ptotal: 0,
- },
- proficiency: { // 熟练度
- },
- };
- let text; let magic; let point; let
- reg;
- const battle = g('battle');
- if (g('monsterAlive') === 0) {
- stats.self._turn += battle.turn;
- stats.self._round += 1;
- if (battle.roundNow === battle.roundAll) {
- stats.self._battle += 1;
+ function matchDamageInfoFromLogText(text, isSkipUnmatched = true) {
+ const regList = [
+ /you for (\d+) (\w+) damage/,
+ /and take (\d+) (\w+) damage/,
+ /You take (\d+) (\w+) damage/,
+ /hits you, causing (\d+) points of (\w+) damage/
+ ];
+ for (let reg of regList) {
+ let match = text.match(reg);
+ if (!match) {
+ continue;
+ }
+ return match;
+ }
+ if (!isSkipUnmatched) {
+ console.log(`Can't match damage info from: `, text);
}
}
- if (parm.mode === 'magic') {
- magic = parm.magic;
- stats.magic[magic] = (magic in stats.magic) ? stats.magic[magic] + 1 : 1;
- stats.hurt.mp += parm.mp;
- stats.hurt.oc += parm.oc;
- } else if (parm.mode === 'items') {
- stats.items[parm.item] = (parm.item in stats.items) ? stats.items[parm.item] + 1 : 1;
- } else {
- stats.self[parm.mode] = (parm.mode in stats.self) ? stats.self[parm.mode] + 1 : 1;
- }
- const debug = false;
- let log = false;
- for (let i = 0; i < parm.log.length; i++) {
- if (parm.log[i].className === 'tls') {
- break;
+
+ function recordUsage(parm) {
+ const filter = g().option.record;
+ if (!filter) {
+ return;
}
- text = parm.log[i].textContent;
- if (debug) {
- console.log(text);
- }
- if (text.match(/you for \d+ \w+ damage/)) {
- reg = text.match(/you for (\d+) (\w+) damage/);
- magic = reg[2].replace('ing', '');
- point = reg[1] * 1;
- stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
- stats.hurt._count++;
- stats.hurt._total += point;
- stats.hurt._avg = Math.round(stats.hurt._total / stats.hurt._count);
- if (magic.match(/pierc|crush|slash/)) {
- stats.hurt._pcount++;
- stats.hurt._ptotal += point;
- stats.hurt._pavg = Math.round(stats.hurt._ptotal / stats.hurt._pcount);
- } else {
- stats.hurt._mcount++;
- stats.hurt._mtotal += point;
- stats.hurt._mavg = Math.round(stats.hurt._mtotal / stats.hurt._mcount);
- }
- } else if (text.match(/^[\w ]+ [a-z]+s [\w+ -]+ for \d+( .*)? damage/) || text.match(/^You .* for \d+ .* damage/)) {
- reg = text.match(/for (\d+)( .*)? damage/);
- magic = text.match(/^[\w ]+ [a-z]+s [\w+ -]+ for/) ? text.match(/^([\w ]+) [a-z]+s [\w+ -]+ for/)[1].replace(/^Your /, '') : text.match(/^You (\w+)/)[1];
- point = reg[1] * 1;
- stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point;
- } else if (text.match(/Vital Theft hits .*? for \d+ damage/)) {
- magic = 'Vital Theft';
- point = text.match(/Vital Theft hits .*? for (\d+) damage/)[1] * 1;
- stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point;
- } else if (text.match(/You (evade|parry|block) the attack|misses the attack against you|(casts|uses) .* misses the attack/)) {
- stats.self.evade++;
- } else if (text.match(/(resists your spell|Your spell is absorbed|(evades|parries) your (attack|spell))|Your attack misses its mark|Your spell fails to connect/)) {
- stats.self.miss++;
- } else if (text.match(/You gain the effect Focusing/)) {
- stats.self.focus++;
- } else if (text.match(/^Recovered \d+ points of/) || text.match(/You are healed for \d+ Health Points/) || text.match(/You drain \d+ HP from/)) {
- magic = (parm.mode === 'defend') ? 'defend' : text.match(/You drain \d+ HP from/) ? 'drain' : parm.magic || parm.item;
- point = text.match(/\d+/)[0] * 1;
- stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point;
- } else if (text.match(/(restores|drain) \d+ points of/)) {
- reg = text.match(/^(.*) restores (\d+) points of (\w+)/) || text.match(/^You (drain) (\d+) points of (\w+)/);
- magic = reg[1];
- point = reg[2] * 1;
- stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point;
- } else if (text.match(/absorbs \d+ points of damage from the attack into \d+ points of \w+ damage/)) {
- reg = text.match(/(.*) absorbs (\d+) points of damage from the attack into (\d+) points of (\w+) damage/);
- point = reg[2] * 1;
- magic = parm.log[i - 1].textContent.match(/you for (\d+) (\w+) damage/)[2].replace('ing', '');
- stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
- point = reg[3] * 1;
- magic = `${reg[1].replace('Your ', '')}_${reg[4]}`;
- stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
- } else if (text.match(/You gain .* proficiency/)) {
- reg = text.match(/You gain ([\d.]+) points of (.*?) proficiency/);
- magic = reg[2];
- point = reg[1] * 1;
- stats.proficiency[magic] = (magic in stats.proficiency) ? stats.proficiency[magic] + point : point;
- stats.proficiency[magic] = stats.proficiency[magic].toFixed(3) * 1;
- } else if (text.trim() === '' || text.match(/You (gain |cast |use |are Victorious|have reached Level|have obtained the title|do not have enough MP)/) || text.match(/Cooldown|has expired|Spirit Stance|gains the effect|insufficient Spirit|Stop beating dead ponies| defeat |Clear Bonus|brink of defeat|Stop \w+ing|Spawned Monster| drop(ped|s) |defeated/)) {
- // nothing;
- } else if (debug) {
- log = true;
- setAudioAlarm('Error');
- console.log(text);
+ const stats = getValue('stats', true) || {};
+ stats.self ??= { _startTime: time(3) };
+ stats.self._turn = filter.turn ? stats.self._turn ?? 0 : undefined;
+ stats.self._round = filter.round ? stats.self._round ?? 0 : undefined;
+ stats.self._battle = filter.battle ? stats.self._battle ?? 0 : undefined;
+ stats.self._monster = filter.monster ? stats.self._monster ?? 0 : undefined;
+ stats.self._boss = filter.boss ? stats.self._boss ?? 0 : undefined;
+ stats.self.evade = filter.evade ? stats.self.evade ?? 0 : undefined;
+ stats.self.miss = filter.miss ? stats.self.miss ?? 0 : undefined;
+ stats.self.focus = filter.focus ? stats.self.focus ?? 0 : undefined;
+ stats.self.mp = filter.mp ? stats.self.mp ?? 0 : undefined;
+ stats.self.oc = filter.oc ? stats.self.oc ?? 0 : undefined;
+ stats.restore = filter.restore ? stats.restore ?? {} : undefined; // 回复量
+ stats.items = filter.items ? stats.items ?? {} : undefined; // 物品使用次数
+ stats.magic = filter.magic ? stats.magic ?? {} : undefined; // 技能使用次数
+ stats.damage = filter.damage ? stats.damage ?? {} : undefined; // 技能攻击造成的伤害
+ stats.proficiency = filter.proficiency ? stats.proficiency ?? {} : undefined; // 熟练度
+ stats.hurt = filter.hurt ? stats.hurt ?? {} : undefined; // 受到攻击造成的伤害
+ if (filter.hurt) {
+ stats.hurt._avg = filter.hurtavg ? stats.hurt._avg ?? 0 : undefined;
+ stats.hurt._count = filter.hurtcount ? stats.hurt._count ?? 0 : undefined;
+ stats.hurt._total = filter.hurttotal ? stats.hurt._total ?? 0 : undefined;
+ stats.hurt._mavg = filter.hurtmavg ? stats.hurt._mavg ?? 0 : undefined;
+ stats.hurt._mcount = filter.hurtmcount ? stats.hurt._mcount ?? 0 : undefined;
+ stats.hurt._mtotal = filter.hurtmtotal ? stats.hurt._mtotal ?? 0 : undefined;
+ stats.hurt._pavg = filter.hurtpavg ? stats.hurt._pavg ?? 0 : undefined;
+ stats.hurt._pcount = filter.hurtpcount ? stats.hurt._pcount ?? 0 : undefined;
+ stats.hurt._ptotal = filter.hurtptotal ? stats.hurt._ptotal ?? 0 : undefined;
+ }
+ let text, magic, point, reg;
+ const battle = g().battle;
+ if (g().monsterAlive === 0) {
+ if (filter.turn) {
+ stats.self._turn += battle.turn;
+ }
+ if (filter.round) {
+ stats.self._round += 1;
+ }
+ if (filter.battle) {
+ if (battle.roundNow === battle.roundAll) {
+ stats.self._battle += 1;
+ }
+ }
}
+ if (parm.mode === 'magic') {
+ magic = parm.magic;
+ if (filter.magic) {
+ stats.magic[magic] = (magic in stats.magic) ? stats.magic[magic] + 1 : 1;
+ }
+ if (filter.mp) {
+ stats.self.mp += parm.mp;
+ }
+ if (filter.oc) {
+ stats.self.oc += parm.oc;
+ }
+ } else if (parm.mode === 'items') {
+ if (filter.items) {
+ stats.items[parm.item] = (parm.item in stats.items) ? stats.items[parm.item] + 1 : 1;
+ }
+ } else {
+ if (filter[parm.mode]) {
+ stats.self[parm.mode] = (parm.mode in stats.self) ? stats.self[parm.mode] + 1 : 1;
+ }
+ }
+
+ const debug = false;
+ let log = false;
+ for (let i = 0; i < parm.log.length; i++) {
+ if (parm.log[i].className === 'tls') {
+ break;
+ }
+ text = parm.log[i].textContent;
+ if (debug) {
+ console.log(text);
+ }
+ if (reg = matchDamageInfoFromLogText(text)) {
+ magic = reg[2].replace('ing', '');
+ point = reg[1] * 1;
+ if (filter.hurt) {
+ stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
+ if (filter.hurtcount || filter.hurtavg) {
+ stats.hurt._count++;
+ }
+ if (filter.hurttotal || filter.hurtavg) {
+ stats.hurt._total += point;
+ }
+ if (filter.hurtavg) {
+ stats.hurt._avg = Math.round(stats.hurt._total / stats.hurt._count);
+ }
+ if (magic.match(/pierc|crush|slash/)) {
+ if (filter.hurtpcount || filter.hurtpavg) {
+ stats.hurt._pcount++;
+ }
+ if (filter.hurtptotal || filter.hurtpavg) {
+ stats.hurt._ptotal += point;
+ }
+ if (filter.hurtpavg) {
+ stats.hurt._pavg = Math.round(stats.hurt._ptotal / stats.hurt._pcount);
+ }
+ } else {
+ if (filter.hurtmcount || filter.hurtmavg) {
+ stats.hurt._mcount++;
+ }
+ if (filter.hurtmtotal || filter.hurtmavg) {
+ stats.hurt._mtotal += point;
+ }
+ if (filter.hurtmavg) {
+ stats.hurt._mavg = Math.round(stats.hurt._mtotal / stats.hurt._mcount);
+ }
+ }
+ }
+ if (filter.evade && text.match(/You ((partially )*(evade|parry|block)( and )*)+ the attack/)) {
+ stats.self.evade++;
+ }
+ } else if (text.match(/^[\w ]+ [a-z]+s [\w+ -]+ for \d+( .*)? damage/) || text.match(/^You .* for \d+ .* damage/)) {
+ if (filter.damage) {
+ reg = text.match(/for (\d+)( .*)? damage/);
+ magic = text.match(/^[\w ]+ [a-z]+s [\w+ -]+ for/) ? text.match(/^([\w ]+) [a-z]+s [\w+ -]+ for/)[1].replace(/^Your /, '') : text.match(/^You (\w+)/)[1];
+ point = reg[1] * 1;
+ stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point;
+ }
+ } else if (text.match(/Vital Theft hits .*? for \d+ damage/)) {
+ if (filter.damage) {
+ magic = 'Vital Theft';
+ point = text.match(/Vital Theft hits .*? for (\d+) damage/)[1] * 1;
+ stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point;
+ }
+ } else if (text.match(/You (evade|parry|block) the attack|misses the attack against you|(casts|uses) .* misses the attack/)) {
+ if (filter.evade) {
+ stats.self.evade++;
+ }
+ } else if (text.match(/(resists your spell|Your spell is absorbed|(evades|parries) your (attack|spell))|Your attack misses its mark|Your spell fails to connect/)) {
+ if (filter.miss) {
+ stats.self.miss++;
+ }
+ } else if (text.match(/You gain the effect Focusing/)) {
+ if (filter.focus) {
+ stats.self.focus++;
+ }
+ } else if (text.match(/^Recovered \d+ points of/) || text.match(/You are healed for \d+ Health Points/) || text.match(/You drain \d+ HP from/)) {
+ if (filter.restore) {
+ magic = (parm.mode === 'defend') ? 'defend' : text.match(/You drain \d+ HP from/) ? 'drain' : parm.magic || parm.item;
+ point = text.match(/\d+/)[0] * 1;
+ stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point;
+ }
+ } else if (text.match(/(restores|drain) \d+ points of/)) {
+ if (filter.restore) {
+ reg = text.match(/^(.*) restores (\d+) points of (\w+)/) || text.match(/^You (drain) (\d+) points of (\w+)/);
+ magic = reg[1];
+ point = reg[2] * 1;
+ stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point;
+ }
+ } else if (text.match(/absorbs \d+ points of damage from the attack into \d+ points of \w+ damage/)) {
+ if (filter.hurt) {
+ reg = text.match(/(.*) absorbs (\d+) points of damage from the attack into (\d+) points of (\w+) damage/);
+ point = reg[2] * 1;
+ magic = matchDamageInfoFromLogText(parm.log[i - 1].textContent, false)[2].replace('ing', '');
+ stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
+ point = reg[3] * 1;
+ magic = `${reg[1].replace('Your ', '')}_${reg[4]}`;
+ stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
+ }
+ } else if (text.match(/You gain .* proficiency/)) {
+ if (filter.proficiency) {
+ reg = text.match(/You gain ([\d.]+) points of (.*?) proficiency/);
+ magic = reg[2];
+ point = reg[1] * 1;
+ stats.proficiency[magic] = (magic in stats.proficiency) ? stats.proficiency[magic] + point : point;
+ stats.proficiency[magic] = stats.proficiency[magic].toFixed(3) * 1;
+ }
+ } else if (text.trim() === '' || text.match(/You (gain |cast |use |are Victorious|have reached Level|have obtained the title|do not have enough MP)/) || text.match(/Cooldown|has expired|Spirit Stance|gains the effect|insufficient Spirit|Stop beating dead ponies| defeat |Clear Bonus|brink of defeat|Stop \w+ing|Spawned Monster| drop(ped|s) |defeated/)) {
+ // nothing;
+ } else if (debug) {
+ log = true;
+ setAudioAlarm('Error');
+ console.log(text);
+ }
+ }
+ if (debug && log) {
+ console.table(stats);
+ pauseChange();
+ }
+ setValue('stats', stats);
}
- if (debug && log) {
- console.table(stats);
- pauseChange();
- }
- setValue('stats', stats);
- }
- function recordUsage2() {
- const stats = getValue('stats', true);
- stats.self._monster += g('monsterAll');
- stats.self._boss += g('bossAll');
- const battle = g('battle');
- if (g('option').recordEach && battle.roundNow === battle.roundAll) {
- const old = getValue('statsOld', true) || [];
- stats.__name = getValue('battleCode');
- stats.self._endTime = time(3);
- old.push(stats);
- setValue('statsOld', old);
- delValue('stats');
- } else {
+ function recordUsage2() {
+ const filter = g().option.record;
+ if (!filter) {
+ return;
+ }
+ const stats = getValue('stats', true);
+ if (filter.monster) {
+ stats.self._monster += g().monsterAll;
+ }
+ if (filter.boss) {
+ stats.self._boss += g().bossAll;
+ }
+ const battle = g().battle;
+ if (g().option.recordEach && battle.roundNow === battle.roundAll) {
+ const old = getValue('statsOld', true) || [];
+ stats.__name = getValue('battleCode', true).name;
+ stats.self._endTime = time(3);
+ old.push(stats);
+ setValue('statsOld', old);
+ delValue('stats');
+ return;
+ }
setValue('stats', stats);
}
+ } catch (err) {
+ console.error(err);
+ document.title = err;
}
-} catch (e) {
- console.log(e);
- document.title = e;
-}
+})();
| | | |