v0.14.0 - Major strategy overhaul from agy research report
BUG FIXES (P0): - Spark of Life SP safety: consume spirit items when SP < 55% BEFORE buffs, Cure, or any other action (both Adept and Veteran) - Spirit Stance safety auto-disable: auto-toggle OFF if SP drops below 60% - useItem() fallback: auto_ slot IDs now lookup by onmouseover attribute (on-cooldown items without DOM id were silently failing) - setTimeout targeting race: removed 50ms delay in castTargetSpell/useSkill (was causing targeting failures with fast Q-repeat) - maxBuffDur pre-seeded with L150+ baseline values (haste=80, heartseeker=120, sparklife=50, etc.) so fill-level scoring works from first turn STRATEGY (P1): - Channeling waste eliminated for 2H physical builds: no more Fiery Blast fallback — let channeling expire on a basic attack (Domino Strike + Penetrated Armor do more damage) - Weapon skill thresholds lowered from 5+ to 3+ enemies (IWBTH survivability) - Expanded debuffs: Imperil on any 2+ mobs, Weaken on high-threat targets (not just bosses anymore) - Domino Strike optimal targeting: findOptimalDominoTarget() picks center monsters to max splash coverage (up to 2 targets each side) NEW FEATURES: - Domino Strike splash positioning algorithm added to all tiers - Full _reason tags on every action return for decision tracing - agy research report saved at hermes_report.md (554 lines)
This commit is contained in:
parent
467e844aae
commit
c9ad92e28d
7 changed files with 372 additions and 129 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.34
|
// @version 0.14.0
|
||||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.13.34';
|
const VERSION = '0.14.0';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -127,6 +127,17 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load max buff durations from localStorage (learned across sessions)
|
// Load max buff durations from localStorage (learned across sessions)
|
||||||
|
// Pre-seed with L150+ baseline values so fill-level formula works from first turn
|
||||||
|
const DEFAULT_MAX_BUFF_DUR = {
|
||||||
|
'haste': 80,
|
||||||
|
'protection': 80,
|
||||||
|
'sparklife': 50,
|
||||||
|
'heartseeker': 120,
|
||||||
|
'regen': 50,
|
||||||
|
'absorb': 75,
|
||||||
|
'shadowveil': 75,
|
||||||
|
};
|
||||||
|
STATE.maxBuffDur = { ...DEFAULT_MAX_BUFF_DUR };
|
||||||
try {
|
try {
|
||||||
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
||||||
for (const [k, v] of Object.entries(saved)) {
|
for (const [k, v] of Object.entries(saved)) {
|
||||||
|
|
@ -851,6 +862,27 @@ function hasHighThreat(threshold) {
|
||||||
return count >= 2;
|
return count >= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find optimal target for Domino Strike splash (2H passive) — center-aligned
|
||||||
|
// For 2H builds, Domino Strike splashes up to 2 enemies on each side of the target.
|
||||||
|
// Attacking edge monsters wastes 50% of the splash potential.
|
||||||
|
function findOptimalDominoTarget() {
|
||||||
|
const total = STATE.monsters.length;
|
||||||
|
if (total <= 2) return findWeakestMonster();
|
||||||
|
|
||||||
|
let best = -1;
|
||||||
|
let bestScore = -1;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
let splash = 0;
|
||||||
|
if (i - 1 >= 0 && STATE.monsters[i - 1] && STATE.monsters[i - 1].hasAttribute('onclick')) splash++;
|
||||||
|
if (i - 2 >= 0 && STATE.monsters[i - 2] && STATE.monsters[i - 2].hasAttribute('onclick')) splash++;
|
||||||
|
if (i + 1 < total && STATE.monsters[i + 1] && STATE.monsters[i + 1].hasAttribute('onclick')) splash++;
|
||||||
|
if (i + 2 < total && STATE.monsters[i + 2] && STATE.monsters[i + 2].hasAttribute('onclick')) splash++;
|
||||||
|
if (splash > bestScore) { bestScore = splash; best = i; }
|
||||||
|
});
|
||||||
|
return best >= 0 ? best : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function checkMonsterDebuff(idx, db) {
|
function checkMonsterDebuff(idx, db) {
|
||||||
const m = STATE.monsters[idx];
|
const m = STATE.monsters[idx];
|
||||||
if (!m) return false;
|
if (!m) return false;
|
||||||
|
|
@ -1021,8 +1053,15 @@ function findBestChannelingTarget() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: ANY damage spell rather than waste the charge on basic attack
|
// Fallback: for physical 2H builds, don't waste channeling on weak damage spells.
|
||||||
// Try elementals first (cheapest), then any spell we know
|
// Channeling improves the next spell, but Fiery Blast does negligible damage at IWBTH.
|
||||||
|
// Better to use the charge on a basic attack that procs Domino Strike and Penetrated Armor.
|
||||||
|
const isPhysical2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow');
|
||||||
|
if (isPhysical2H) {
|
||||||
|
return null; // Let channeling expire naturally — basic attacks do more than Fiery Blast
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mage fallback: use any cheap spell for mage builds
|
||||||
const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale',
|
const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale',
|
||||||
'Smite', 'Corruption', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst',
|
'Smite', 'Corruption', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst',
|
||||||
'Banishment', 'Disintegrate']);
|
'Banishment', 'Disintegrate']);
|
||||||
|
|
@ -1145,8 +1184,8 @@ function strategyNovice() {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
// Shatter Strike: AoE stun vs 3+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -1192,7 +1231,21 @@ function strategyAdept() {
|
||||||
// 0. Mystic Gem for channeling — only use once per battle
|
// 0. Mystic Gem for channeling — only use once per battle
|
||||||
const gem = hasUsableGem('channel');
|
const gem = hasUsableGem('channel');
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
|
// 0b. Spark of Life SP safety
|
||||||
|
if (STATE.sp < 0.55) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0c. Spirit Stance safety auto-disable
|
||||||
|
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||||
|
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < CFG.cureItemHP) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
|
|
@ -1256,19 +1309,17 @@ function strategyAdept() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Debuffs — only on actual boss fights (gold border style)
|
// 4. Debuffs — Imperil on 2+ mobs or bosses; Weaken on high-threat
|
||||||
// At high levels/difficulties, all monsters have SP bars, so
|
if (CFG.autoDebuff && STATE.mp > 0.20) {
|
||||||
// we only debuff when a gold-bordered rare/boss is present.
|
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
|
||||||
if (anyRareMonster()) {
|
|
||||||
if (hasSpell('Imperil')) {
|
|
||||||
const b = findStrongestMonster();
|
const b = findStrongestMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||||
return { type: 'spell', name: 'Imperil', target: b };
|
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
|
||||||
}
|
}
|
||||||
if (hasSpell('Weaken')) {
|
if (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
|
||||||
const b = findStrongestMonster();
|
const b = findDangerousMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||||
return { type: 'spell', name: 'Weaken', target: b };
|
return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1278,13 +1329,13 @@ function strategyAdept() {
|
||||||
const skillOC_N = CFG.skillOC || 75;
|
const skillOC_N = CFG.skillOC || 75;
|
||||||
if (STATE.oc >= skillOC_N || isHighThreat) {
|
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||||
if (isHighThreat && STATE.oc < skillOC_N) console.log('%c[HV] ⚠ emergency skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
if (isHighThreat && STATE.oc < skillOC_N) console.log('%c[HV] ⚠ emergency skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
// Shatter Strike: AoE stun vs 3+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
|
@ -1318,9 +1369,9 @@ function strategyAdept() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster());
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'attack', target: t };
|
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1332,6 +1383,22 @@ function strategyVeteran() {
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
|
// 0b. Spark of Life SP safety — Spark costs 50% SP when triggered.
|
||||||
|
// If SP < 55%, consume a spirit item before anything else.
|
||||||
|
// Also suppress Spark recast if SP is too low to survive a proc.
|
||||||
|
if (STATE.sp < 0.55) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn
|
||||||
|
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||||
|
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < CFG.cureItemHP) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
const gem = hasUsableGem('heal');
|
const gem = hasUsableGem('heal');
|
||||||
|
|
@ -1394,17 +1461,21 @@ function strategyVeteran() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Debuffs — only on actual boss fights (gold border)
|
// 4. Debuffs — Imperil on any 2+ mobs or bosses; Weaken on high-threat enemies
|
||||||
if (CFG.autoDebuff && STATE.mp > 0.2 && anyRareMonster()) {
|
// On IWBTH, non-boss elites hit hard — debuffs are worth the MP cost
|
||||||
if (hasSpell('Imperil')) {
|
if (CFG.autoDebuff && STATE.mp > 0.20) {
|
||||||
|
// Imperil: reduce physical mitigation by 25-50% (stacks with Penetrated Armor)
|
||||||
|
// Use on any tanky enemy when facing 2+ monsters or a rare
|
||||||
|
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
|
||||||
const b = findStrongestMonster();
|
const b = findStrongestMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||||
return { type: 'spell', name: 'Imperil', target: b };
|
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
|
||||||
}
|
}
|
||||||
if (hasSpell('Weaken')) {
|
// Weaken: reduce incoming damage by 50% — use on high-threat or 3+ mobs
|
||||||
const b = findStrongestMonster();
|
if (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
|
||||||
|
const b = findDangerousMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||||
return { type: 'spell', name: 'Weaken', target: b };
|
return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1417,13 +1488,13 @@ function strategyVeteran() {
|
||||||
if (STATE.oc >= askillOC3 || isHighThreat) {
|
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||||
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
||||||
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5 for IWBTH survivability)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
// Shatter Strike: AoE stun vs 3+ enemies with armor break
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
|
@ -1483,7 +1554,7 @@ function strategyVeteran() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster());
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1532,17 +1603,28 @@ function castTargetSpell(n, i) {
|
||||||
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
||||||
if (!s) return false;
|
if (!s) return false;
|
||||||
s.click();
|
s.click();
|
||||||
|
// Target selection: click immediately, no setTimeout delay needed.
|
||||||
|
// HV's native lock_action handles the state transition synchronously.
|
||||||
if (i >= 0 && i < STATE.monsters.length) {
|
if (i >= 0 && i < STATE.monsters.length) {
|
||||||
setTimeout(() => {
|
|
||||||
const m = STATE.monsters[i];
|
const m = STATE.monsters[i];
|
||||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useItem(id) {
|
function useItem(id) {
|
||||||
const it = document.getElementById('ikey_' + id);
|
let it = document.getElementById('ikey_' + id);
|
||||||
|
|
||||||
|
// Fallback for auto_ slot IDs (on-cooldown items without explicit DOM id):
|
||||||
|
// search for element with matching onmouseover item ID
|
||||||
|
if (!it && typeof id === 'string' && id.startsWith('auto_')) {
|
||||||
|
const rawId = id.replace('auto_', '');
|
||||||
|
const itemPane = document.getElementById('pane_item');
|
||||||
|
if (itemPane) {
|
||||||
|
it = $$('div[onmouseover*="set_infopane_item(' + rawId + ')"]', itemPane)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!it) return false;
|
if (!it) return false;
|
||||||
// Track that we used the P-slot gem so we don't retry stale state
|
// Track that we used the P-slot gem so we don't retry stale state
|
||||||
if (id === 'p') STATE._mysticUsed = true;
|
if (id === 'p') STATE._mysticUsed = true;
|
||||||
|
|
@ -1583,11 +1665,10 @@ function useSkill(n, i) {
|
||||||
|
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
el.click();
|
el.click();
|
||||||
|
// Click target immediately — no setTimeout needed
|
||||||
if (i >= 0 && i < STATE.monsters.length) {
|
if (i >= 0 && i < STATE.monsters.length) {
|
||||||
setTimeout(() => {
|
|
||||||
const m = STATE.monsters[i];
|
const m = STATE.monsters[i];
|
||||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.34
|
// @version 0.14.0
|
||||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.13.34';
|
const VERSION = '0.14.0';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -127,6 +127,17 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load max buff durations from localStorage (learned across sessions)
|
// Load max buff durations from localStorage (learned across sessions)
|
||||||
|
// Pre-seed with L150+ baseline values so fill-level formula works from first turn
|
||||||
|
const DEFAULT_MAX_BUFF_DUR = {
|
||||||
|
'haste': 80,
|
||||||
|
'protection': 80,
|
||||||
|
'sparklife': 50,
|
||||||
|
'heartseeker': 120,
|
||||||
|
'regen': 50,
|
||||||
|
'absorb': 75,
|
||||||
|
'shadowveil': 75,
|
||||||
|
};
|
||||||
|
STATE.maxBuffDur = { ...DEFAULT_MAX_BUFF_DUR };
|
||||||
try {
|
try {
|
||||||
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
||||||
for (const [k, v] of Object.entries(saved)) {
|
for (const [k, v] of Object.entries(saved)) {
|
||||||
|
|
@ -851,6 +862,27 @@ function hasHighThreat(threshold) {
|
||||||
return count >= 2;
|
return count >= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find optimal target for Domino Strike splash (2H passive) — center-aligned
|
||||||
|
// For 2H builds, Domino Strike splashes up to 2 enemies on each side of the target.
|
||||||
|
// Attacking edge monsters wastes 50% of the splash potential.
|
||||||
|
function findOptimalDominoTarget() {
|
||||||
|
const total = STATE.monsters.length;
|
||||||
|
if (total <= 2) return findWeakestMonster();
|
||||||
|
|
||||||
|
let best = -1;
|
||||||
|
let bestScore = -1;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
let splash = 0;
|
||||||
|
if (i - 1 >= 0 && STATE.monsters[i - 1] && STATE.monsters[i - 1].hasAttribute('onclick')) splash++;
|
||||||
|
if (i - 2 >= 0 && STATE.monsters[i - 2] && STATE.monsters[i - 2].hasAttribute('onclick')) splash++;
|
||||||
|
if (i + 1 < total && STATE.monsters[i + 1] && STATE.monsters[i + 1].hasAttribute('onclick')) splash++;
|
||||||
|
if (i + 2 < total && STATE.monsters[i + 2] && STATE.monsters[i + 2].hasAttribute('onclick')) splash++;
|
||||||
|
if (splash > bestScore) { bestScore = splash; best = i; }
|
||||||
|
});
|
||||||
|
return best >= 0 ? best : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function checkMonsterDebuff(idx, db) {
|
function checkMonsterDebuff(idx, db) {
|
||||||
const m = STATE.monsters[idx];
|
const m = STATE.monsters[idx];
|
||||||
if (!m) return false;
|
if (!m) return false;
|
||||||
|
|
@ -1021,8 +1053,15 @@ function findBestChannelingTarget() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: ANY damage spell rather than waste the charge on basic attack
|
// Fallback: for physical 2H builds, don't waste channeling on weak damage spells.
|
||||||
// Try elementals first (cheapest), then any spell we know
|
// Channeling improves the next spell, but Fiery Blast does negligible damage at IWBTH.
|
||||||
|
// Better to use the charge on a basic attack that procs Domino Strike and Penetrated Armor.
|
||||||
|
const isPhysical2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow');
|
||||||
|
if (isPhysical2H) {
|
||||||
|
return null; // Let channeling expire naturally — basic attacks do more than Fiery Blast
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mage fallback: use any cheap spell for mage builds
|
||||||
const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale',
|
const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale',
|
||||||
'Smite', 'Corruption', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst',
|
'Smite', 'Corruption', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst',
|
||||||
'Banishment', 'Disintegrate']);
|
'Banishment', 'Disintegrate']);
|
||||||
|
|
@ -1145,8 +1184,8 @@ function strategyNovice() {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
// Shatter Strike: AoE stun vs 3+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -1192,7 +1231,21 @@ function strategyAdept() {
|
||||||
// 0. Mystic Gem for channeling — only use once per battle
|
// 0. Mystic Gem for channeling — only use once per battle
|
||||||
const gem = hasUsableGem('channel');
|
const gem = hasUsableGem('channel');
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
|
// 0b. Spark of Life SP safety
|
||||||
|
if (STATE.sp < 0.55) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0c. Spirit Stance safety auto-disable
|
||||||
|
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||||
|
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < CFG.cureItemHP) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
|
|
@ -1256,19 +1309,17 @@ function strategyAdept() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Debuffs — only on actual boss fights (gold border style)
|
// 4. Debuffs — Imperil on 2+ mobs or bosses; Weaken on high-threat
|
||||||
// At high levels/difficulties, all monsters have SP bars, so
|
if (CFG.autoDebuff && STATE.mp > 0.20) {
|
||||||
// we only debuff when a gold-bordered rare/boss is present.
|
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
|
||||||
if (anyRareMonster()) {
|
|
||||||
if (hasSpell('Imperil')) {
|
|
||||||
const b = findStrongestMonster();
|
const b = findStrongestMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||||
return { type: 'spell', name: 'Imperil', target: b };
|
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
|
||||||
}
|
}
|
||||||
if (hasSpell('Weaken')) {
|
if (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
|
||||||
const b = findStrongestMonster();
|
const b = findDangerousMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||||
return { type: 'spell', name: 'Weaken', target: b };
|
return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1278,13 +1329,13 @@ function strategyAdept() {
|
||||||
const skillOC_N = CFG.skillOC || 75;
|
const skillOC_N = CFG.skillOC || 75;
|
||||||
if (STATE.oc >= skillOC_N || isHighThreat) {
|
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||||
if (isHighThreat && STATE.oc < skillOC_N) console.log('%c[HV] ⚠ emergency skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
if (isHighThreat && STATE.oc < skillOC_N) console.log('%c[HV] ⚠ emergency skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
// Shatter Strike: AoE stun vs 3+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
|
@ -1318,9 +1369,9 @@ function strategyAdept() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster());
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'attack', target: t };
|
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1332,6 +1383,22 @@ function strategyVeteran() {
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
|
// 0b. Spark of Life SP safety — Spark costs 50% SP when triggered.
|
||||||
|
// If SP < 55%, consume a spirit item before anything else.
|
||||||
|
// Also suppress Spark recast if SP is too low to survive a proc.
|
||||||
|
if (STATE.sp < 0.55) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn
|
||||||
|
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||||
|
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < CFG.cureItemHP) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
const gem = hasUsableGem('heal');
|
const gem = hasUsableGem('heal');
|
||||||
|
|
@ -1394,17 +1461,21 @@ function strategyVeteran() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Debuffs — only on actual boss fights (gold border)
|
// 4. Debuffs — Imperil on any 2+ mobs or bosses; Weaken on high-threat enemies
|
||||||
if (CFG.autoDebuff && STATE.mp > 0.2 && anyRareMonster()) {
|
// On IWBTH, non-boss elites hit hard — debuffs are worth the MP cost
|
||||||
if (hasSpell('Imperil')) {
|
if (CFG.autoDebuff && STATE.mp > 0.20) {
|
||||||
|
// Imperil: reduce physical mitigation by 25-50% (stacks with Penetrated Armor)
|
||||||
|
// Use on any tanky enemy when facing 2+ monsters or a rare
|
||||||
|
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
|
||||||
const b = findStrongestMonster();
|
const b = findStrongestMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||||
return { type: 'spell', name: 'Imperil', target: b };
|
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
|
||||||
}
|
}
|
||||||
if (hasSpell('Weaken')) {
|
// Weaken: reduce incoming damage by 50% — use on high-threat or 3+ mobs
|
||||||
const b = findStrongestMonster();
|
if (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
|
||||||
|
const b = findDangerousMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||||
return { type: 'spell', name: 'Weaken', target: b };
|
return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1417,13 +1488,13 @@ function strategyVeteran() {
|
||||||
if (STATE.oc >= askillOC3 || isHighThreat) {
|
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||||
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
||||||
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5 for IWBTH survivability)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
// Shatter Strike: AoE stun vs 3+ enemies with armor break
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
|
@ -1483,7 +1554,7 @@ function strategyVeteran() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster());
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1532,17 +1603,28 @@ function castTargetSpell(n, i) {
|
||||||
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
||||||
if (!s) return false;
|
if (!s) return false;
|
||||||
s.click();
|
s.click();
|
||||||
|
// Target selection: click immediately, no setTimeout delay needed.
|
||||||
|
// HV's native lock_action handles the state transition synchronously.
|
||||||
if (i >= 0 && i < STATE.monsters.length) {
|
if (i >= 0 && i < STATE.monsters.length) {
|
||||||
setTimeout(() => {
|
|
||||||
const m = STATE.monsters[i];
|
const m = STATE.monsters[i];
|
||||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useItem(id) {
|
function useItem(id) {
|
||||||
const it = document.getElementById('ikey_' + id);
|
let it = document.getElementById('ikey_' + id);
|
||||||
|
|
||||||
|
// Fallback for auto_ slot IDs (on-cooldown items without explicit DOM id):
|
||||||
|
// search for element with matching onmouseover item ID
|
||||||
|
if (!it && typeof id === 'string' && id.startsWith('auto_')) {
|
||||||
|
const rawId = id.replace('auto_', '');
|
||||||
|
const itemPane = document.getElementById('pane_item');
|
||||||
|
if (itemPane) {
|
||||||
|
it = $$('div[onmouseover*="set_infopane_item(' + rawId + ')"]', itemPane)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!it) return false;
|
if (!it) return false;
|
||||||
// Track that we used the P-slot gem so we don't retry stale state
|
// Track that we used the P-slot gem so we don't retry stale state
|
||||||
if (id === 'p') STATE._mysticUsed = true;
|
if (id === 'p') STATE._mysticUsed = true;
|
||||||
|
|
@ -1583,11 +1665,10 @@ function useSkill(n, i) {
|
||||||
|
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
el.click();
|
el.click();
|
||||||
|
// Click target immediately — no setTimeout needed
|
||||||
if (i >= 0 && i < STATE.monsters.length) {
|
if (i >= 0 && i < STATE.monsters.length) {
|
||||||
setTimeout(() => {
|
|
||||||
const m = STATE.monsters[i];
|
const m = STATE.monsters[i];
|
||||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,28 @@ function castTargetSpell(n, i) {
|
||||||
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
||||||
if (!s) return false;
|
if (!s) return false;
|
||||||
s.click();
|
s.click();
|
||||||
|
// Target selection: click immediately, no setTimeout delay needed.
|
||||||
|
// HV's native lock_action handles the state transition synchronously.
|
||||||
if (i >= 0 && i < STATE.monsters.length) {
|
if (i >= 0 && i < STATE.monsters.length) {
|
||||||
setTimeout(() => {
|
|
||||||
const m = STATE.monsters[i];
|
const m = STATE.monsters[i];
|
||||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useItem(id) {
|
function useItem(id) {
|
||||||
const it = document.getElementById('ikey_' + id);
|
let it = document.getElementById('ikey_' + id);
|
||||||
|
|
||||||
|
// Fallback for auto_ slot IDs (on-cooldown items without explicit DOM id):
|
||||||
|
// search for element with matching onmouseover item ID
|
||||||
|
if (!it && typeof id === 'string' && id.startsWith('auto_')) {
|
||||||
|
const rawId = id.replace('auto_', '');
|
||||||
|
const itemPane = document.getElementById('pane_item');
|
||||||
|
if (itemPane) {
|
||||||
|
it = $$('div[onmouseover*="set_infopane_item(' + rawId + ')"]', itemPane)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!it) return false;
|
if (!it) return false;
|
||||||
// Track that we used the P-slot gem so we don't retry stale state
|
// Track that we used the P-slot gem so we don't retry stale state
|
||||||
if (id === 'p') STATE._mysticUsed = true;
|
if (id === 'p') STATE._mysticUsed = true;
|
||||||
|
|
@ -92,11 +103,10 @@ function useSkill(n, i) {
|
||||||
|
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
el.click();
|
el.click();
|
||||||
|
// Click target immediately — no setTimeout needed
|
||||||
if (i >= 0 && i < STATE.monsters.length) {
|
if (i >= 0 && i < STATE.monsters.length) {
|
||||||
setTimeout(() => {
|
|
||||||
const m = STATE.monsters[i];
|
const m = STATE.monsters[i];
|
||||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.13.34';
|
const VERSION = '0.14.0';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.34
|
// @version 0.14.0
|
||||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
|
||||||
11
src/state.js
11
src/state.js
|
|
@ -63,6 +63,17 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load max buff durations from localStorage (learned across sessions)
|
// Load max buff durations from localStorage (learned across sessions)
|
||||||
|
// Pre-seed with L150+ baseline values so fill-level formula works from first turn
|
||||||
|
const DEFAULT_MAX_BUFF_DUR = {
|
||||||
|
'haste': 80,
|
||||||
|
'protection': 80,
|
||||||
|
'sparklife': 50,
|
||||||
|
'heartseeker': 120,
|
||||||
|
'regen': 50,
|
||||||
|
'absorb': 75,
|
||||||
|
'shadowveil': 75,
|
||||||
|
};
|
||||||
|
STATE.maxBuffDur = { ...DEFAULT_MAX_BUFF_DUR };
|
||||||
try {
|
try {
|
||||||
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
||||||
for (const [k, v] of Object.entries(saved)) {
|
for (const [k, v] of Object.entries(saved)) {
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,27 @@ function hasHighThreat(threshold) {
|
||||||
return count >= 2;
|
return count >= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find optimal target for Domino Strike splash (2H passive) — center-aligned
|
||||||
|
// For 2H builds, Domino Strike splashes up to 2 enemies on each side of the target.
|
||||||
|
// Attacking edge monsters wastes 50% of the splash potential.
|
||||||
|
function findOptimalDominoTarget() {
|
||||||
|
const total = STATE.monsters.length;
|
||||||
|
if (total <= 2) return findWeakestMonster();
|
||||||
|
|
||||||
|
let best = -1;
|
||||||
|
let bestScore = -1;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
let splash = 0;
|
||||||
|
if (i - 1 >= 0 && STATE.monsters[i - 1] && STATE.monsters[i - 1].hasAttribute('onclick')) splash++;
|
||||||
|
if (i - 2 >= 0 && STATE.monsters[i - 2] && STATE.monsters[i - 2].hasAttribute('onclick')) splash++;
|
||||||
|
if (i + 1 < total && STATE.monsters[i + 1] && STATE.monsters[i + 1].hasAttribute('onclick')) splash++;
|
||||||
|
if (i + 2 < total && STATE.monsters[i + 2] && STATE.monsters[i + 2].hasAttribute('onclick')) splash++;
|
||||||
|
if (splash > bestScore) { bestScore = splash; best = i; }
|
||||||
|
});
|
||||||
|
return best >= 0 ? best : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function checkMonsterDebuff(idx, db) {
|
function checkMonsterDebuff(idx, db) {
|
||||||
const m = STATE.monsters[idx];
|
const m = STATE.monsters[idx];
|
||||||
if (!m) return false;
|
if (!m) return false;
|
||||||
|
|
@ -307,8 +328,15 @@ function findBestChannelingTarget() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: ANY damage spell rather than waste the charge on basic attack
|
// Fallback: for physical 2H builds, don't waste channeling on weak damage spells.
|
||||||
// Try elementals first (cheapest), then any spell we know
|
// Channeling improves the next spell, but Fiery Blast does negligible damage at IWBTH.
|
||||||
|
// Better to use the charge on a basic attack that procs Domino Strike and Penetrated Armor.
|
||||||
|
const isPhysical2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow');
|
||||||
|
if (isPhysical2H) {
|
||||||
|
return null; // Let channeling expire naturally — basic attacks do more than Fiery Blast
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mage fallback: use any cheap spell for mage builds
|
||||||
const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale',
|
const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale',
|
||||||
'Smite', 'Corruption', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst',
|
'Smite', 'Corruption', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst',
|
||||||
'Banishment', 'Disintegrate']);
|
'Banishment', 'Disintegrate']);
|
||||||
|
|
@ -431,8 +459,8 @@ function strategyNovice() {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
// Shatter Strike: AoE stun vs 3+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -478,7 +506,21 @@ function strategyAdept() {
|
||||||
// 0. Mystic Gem for channeling — only use once per battle
|
// 0. Mystic Gem for channeling — only use once per battle
|
||||||
const gem = hasUsableGem('channel');
|
const gem = hasUsableGem('channel');
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
|
// 0b. Spark of Life SP safety
|
||||||
|
if (STATE.sp < 0.55) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0c. Spirit Stance safety auto-disable
|
||||||
|
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||||
|
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < CFG.cureItemHP) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
|
|
@ -542,19 +584,17 @@ function strategyAdept() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Debuffs — only on actual boss fights (gold border style)
|
// 4. Debuffs — Imperil on 2+ mobs or bosses; Weaken on high-threat
|
||||||
// At high levels/difficulties, all monsters have SP bars, so
|
if (CFG.autoDebuff && STATE.mp > 0.20) {
|
||||||
// we only debuff when a gold-bordered rare/boss is present.
|
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
|
||||||
if (anyRareMonster()) {
|
|
||||||
if (hasSpell('Imperil')) {
|
|
||||||
const b = findStrongestMonster();
|
const b = findStrongestMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||||
return { type: 'spell', name: 'Imperil', target: b };
|
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
|
||||||
}
|
}
|
||||||
if (hasSpell('Weaken')) {
|
if (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
|
||||||
const b = findStrongestMonster();
|
const b = findDangerousMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||||
return { type: 'spell', name: 'Weaken', target: b };
|
return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -564,13 +604,13 @@ function strategyAdept() {
|
||||||
const skillOC_N = CFG.skillOC || 75;
|
const skillOC_N = CFG.skillOC || 75;
|
||||||
if (STATE.oc >= skillOC_N || isHighThreat) {
|
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||||
if (isHighThreat && STATE.oc < skillOC_N) console.log('%c[HV] ⚠ emergency skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
if (isHighThreat && STATE.oc < skillOC_N) console.log('%c[HV] ⚠ emergency skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
// Shatter Strike: AoE stun vs 3+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
|
@ -604,9 +644,9 @@ function strategyAdept() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster());
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'attack', target: t };
|
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -618,6 +658,22 @@ function strategyVeteran() {
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
|
// 0b. Spark of Life SP safety — Spark costs 50% SP when triggered.
|
||||||
|
// If SP < 55%, consume a spirit item before anything else.
|
||||||
|
// Also suppress Spark recast if SP is too low to survive a proc.
|
||||||
|
if (STATE.sp < 0.55) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn
|
||||||
|
if (STATE.spiritStance && STATE.sp < 0.60) {
|
||||||
|
return { type: 'toggle_spirit', _reason: 'sp_safety' };
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < CFG.cureItemHP) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
const gem = hasUsableGem('heal');
|
const gem = hasUsableGem('heal');
|
||||||
|
|
@ -680,17 +736,21 @@ function strategyVeteran() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Debuffs — only on actual boss fights (gold border)
|
// 4. Debuffs — Imperil on any 2+ mobs or bosses; Weaken on high-threat enemies
|
||||||
if (CFG.autoDebuff && STATE.mp > 0.2 && anyRareMonster()) {
|
// On IWBTH, non-boss elites hit hard — debuffs are worth the MP cost
|
||||||
if (hasSpell('Imperil')) {
|
if (CFG.autoDebuff && STATE.mp > 0.20) {
|
||||||
|
// Imperil: reduce physical mitigation by 25-50% (stacks with Penetrated Armor)
|
||||||
|
// Use on any tanky enemy when facing 2+ monsters or a rare
|
||||||
|
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
|
||||||
const b = findStrongestMonster();
|
const b = findStrongestMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||||
return { type: 'spell', name: 'Imperil', target: b };
|
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
|
||||||
}
|
}
|
||||||
if (hasSpell('Weaken')) {
|
// Weaken: reduce incoming damage by 50% — use on high-threat or 3+ mobs
|
||||||
const b = findStrongestMonster();
|
if (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
|
||||||
|
const b = findDangerousMonster();
|
||||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||||
return { type: 'spell', name: 'Weaken', target: b };
|
return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -703,13 +763,13 @@ function strategyVeteran() {
|
||||||
if (STATE.oc >= askillOC3 || isHighThreat) {
|
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||||
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
|
||||||
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5 for IWBTH survivability)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
// Shatter Strike: AoE stun vs 3+ enemies with armor break
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||||
if (hasArmorBreak) {
|
if (hasArmorBreak) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
|
@ -769,7 +829,7 @@ function strategyVeteran() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster());
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue