v0.13.30 - Fix spirit item detection, add threat-based targeting
- Fix: items on cooldown (no id attr) were invisible to parser Spirit Draught/Potion on cooldown couldn't be auto-consumed - Fix: spiritPotionSP raised from 30% to 60% - New: parse monster SP bar widths into STATE.monsterSp[] - New: findDangerousMonster() — targets highest SP bar monster - New: hasHighThreat() — true if 2+ monsters >85% SP (about to special) - Weapon skills now fire on high-threat even without full OC - Basic attacks prefer dangerous monsters in all tiers
This commit is contained in:
parent
b6d78067ce
commit
b5a5c3df54
7 changed files with 246 additions and 108 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.29
|
// @version 0.13.30
|
||||||
// @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.29';
|
const VERSION = '0.13.30';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -89,6 +89,7 @@ const STATE = {
|
||||||
interruptAlert: false,
|
interruptAlert: false,
|
||||||
|
|
||||||
monsters: [],
|
monsters: [],
|
||||||
|
monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special)
|
||||||
hp: 1,
|
hp: 1,
|
||||||
mp: 1,
|
mp: 1,
|
||||||
sp: 1,
|
sp: 1,
|
||||||
|
|
@ -426,6 +427,13 @@ function parseBattleState() {
|
||||||
|
|
||||||
// ── Monsters ──
|
// ── Monsters ──
|
||||||
STATE.monsters = $$('.btm1[onclick]');
|
STATE.monsters = $$('.btm1[onclick]');
|
||||||
|
// Parse each monster's SP/OC bar level (the red bar = special attack charge)
|
||||||
|
STATE.monsterSp = [];
|
||||||
|
$$('.btm1').forEach(m => {
|
||||||
|
const spBar = m.querySelector('.btm5 img[src$="barred.png"]');
|
||||||
|
const spPct = spBar ? (parseInt(spBar.style.width) || 0) : 0;
|
||||||
|
STATE.monsterSp.push(spPct);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Spells from spell pane ──
|
// ── Spells from spell pane ──
|
||||||
STATE.spellsKnown = [];
|
STATE.spellsKnown = [];
|
||||||
|
|
@ -485,12 +493,18 @@ function parseBattleState() {
|
||||||
|
|
||||||
// ── Items ──
|
// ── Items ──
|
||||||
STATE.itemsKnown = {};
|
STATE.itemsKnown = {};
|
||||||
$$('[id^="ikey_"]').forEach(el => {
|
// Read all item slots, including on-cooldown ones (which lose their id)
|
||||||
const id = el.id.replace('ikey_', '');
|
const itemPane = document.getElementById('pane_item');
|
||||||
|
if (itemPane) {
|
||||||
|
$$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => {
|
||||||
const omo = el.getAttribute('onmouseover') || '';
|
const omo = el.getAttribute('onmouseover') || '';
|
||||||
const m = omo.match(/battle\.set_infopane_item\((\d+)\)/);
|
const m = omo.match(/set_infopane_item\((\d+)\)/);
|
||||||
if (m) STATE.itemsKnown[id] = m[1];
|
if (m) {
|
||||||
|
const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]);
|
||||||
|
STATE.itemsKnown[slot] = m[1];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Buffs
|
// Buffs
|
||||||
STATE.buffs = {};
|
STATE.buffs = {};
|
||||||
|
|
@ -717,6 +731,10 @@ function shouldBuff(name, minTurns) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
||||||
|
|
||||||
|
// Threat detection: SP bar max is 120px, monsters >85% are about to special
|
||||||
|
const THREAT_PX = Math.round(120 * 85 / 100); // ~102px
|
||||||
|
|
||||||
// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build,
|
// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build,
|
||||||
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
||||||
const BUFF_PRIORITY = [
|
const BUFF_PRIORITY = [
|
||||||
|
|
@ -795,6 +813,30 @@ function findStrongestMonster() {
|
||||||
return best >= 0 ? best : 0;
|
return best >= 0 ? best : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the monster with the highest SP/OC bar (most dangerous — about to special)
|
||||||
|
// The red bar (nbarred.png) has a max width of 120px
|
||||||
|
// Returns the index, or 0 if none found
|
||||||
|
function findDangerousMonster() {
|
||||||
|
let best = -1, bestSP = -1;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
const sp = STATE.monsterSp[i] || 0;
|
||||||
|
if (sp > bestSP) { bestSP = sp; best = i; }
|
||||||
|
});
|
||||||
|
return best >= 0 ? best : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are 2+ monsters with SP bar > threshold (imminent special attacks)
|
||||||
|
function hasHighThreat(threshold) {
|
||||||
|
let count = 0;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
const sp = STATE.monsterSp[i] || 0;
|
||||||
|
if (sp > threshold) count++;
|
||||||
|
});
|
||||||
|
return count >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -1124,7 +1166,7 @@ function strategyNovice() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : 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 };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1216,25 +1258,26 @@ function strategyAdept() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Weapon skills — before spirit stance
|
// 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar)
|
||||||
|
const isHighThreat = hasHighThreat(THREAT_PX);
|
||||||
const skillOC_N = CFG.skillOC || 75;
|
const skillOC_N = CFG.skillOC || 75;
|
||||||
if (STATE.oc >= skillOC_N) {
|
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = 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 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 5 && 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1259,7 +1302,7 @@ function strategyAdept() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : 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 };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1349,35 +1392,38 @@ function strategyVeteran() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Weapon skills only at OC thresholds (let OC build via basic attacks)
|
// 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar)
|
||||||
|
// If 2+ enemies have >85% SP bar, they're about to special — emergency
|
||||||
|
const isHighThreat = hasHighThreat(THREAT_PX);
|
||||||
|
|
||||||
const askillOC3 = CFG.skillOC || 80;
|
const askillOC3 = CFG.skillOC || 80;
|
||||||
if (STATE.oc >= askillOC3) {
|
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||||
const t = findStrongestMonster();
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
}
|
|
||||||
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
|
||||||
if (STATE.monsters.length >= 5) {
|
|
||||||
if (STATE.skillsKnown.includes('Rending Blow')) {
|
|
||||||
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 };
|
||||||
}
|
}
|
||||||
if (STATE.skillsKnown.includes('Shatter Strike')) {
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
||||||
|
if (STATE.monsters.length >= 5 && 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Great Cleave: boss/rare fights or single-target threat
|
||||||
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||||
}
|
}
|
||||||
// Other weapon skills (non-2H): generic use
|
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||||
if (!STATE.skillsKnown.includes('Great Cleave')) {
|
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||||
for (const sk of ps) {
|
for (const sk of ps) {
|
||||||
if (STATE.skillsKnown.includes(sk)) {
|
if (STATE.skillsKnown.includes(sk)) {
|
||||||
const t = findStrongestMonster();
|
const t = findDangerousMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.29
|
// @version 0.13.30
|
||||||
// @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.29';
|
const VERSION = '0.13.30';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -89,6 +89,7 @@ const STATE = {
|
||||||
interruptAlert: false,
|
interruptAlert: false,
|
||||||
|
|
||||||
monsters: [],
|
monsters: [],
|
||||||
|
monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special)
|
||||||
hp: 1,
|
hp: 1,
|
||||||
mp: 1,
|
mp: 1,
|
||||||
sp: 1,
|
sp: 1,
|
||||||
|
|
@ -426,6 +427,13 @@ function parseBattleState() {
|
||||||
|
|
||||||
// ── Monsters ──
|
// ── Monsters ──
|
||||||
STATE.monsters = $$('.btm1[onclick]');
|
STATE.monsters = $$('.btm1[onclick]');
|
||||||
|
// Parse each monster's SP/OC bar level (the red bar = special attack charge)
|
||||||
|
STATE.monsterSp = [];
|
||||||
|
$$('.btm1').forEach(m => {
|
||||||
|
const spBar = m.querySelector('.btm5 img[src$="barred.png"]');
|
||||||
|
const spPct = spBar ? (parseInt(spBar.style.width) || 0) : 0;
|
||||||
|
STATE.monsterSp.push(spPct);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Spells from spell pane ──
|
// ── Spells from spell pane ──
|
||||||
STATE.spellsKnown = [];
|
STATE.spellsKnown = [];
|
||||||
|
|
@ -485,12 +493,18 @@ function parseBattleState() {
|
||||||
|
|
||||||
// ── Items ──
|
// ── Items ──
|
||||||
STATE.itemsKnown = {};
|
STATE.itemsKnown = {};
|
||||||
$$('[id^="ikey_"]').forEach(el => {
|
// Read all item slots, including on-cooldown ones (which lose their id)
|
||||||
const id = el.id.replace('ikey_', '');
|
const itemPane = document.getElementById('pane_item');
|
||||||
|
if (itemPane) {
|
||||||
|
$$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => {
|
||||||
const omo = el.getAttribute('onmouseover') || '';
|
const omo = el.getAttribute('onmouseover') || '';
|
||||||
const m = omo.match(/battle\.set_infopane_item\((\d+)\)/);
|
const m = omo.match(/set_infopane_item\((\d+)\)/);
|
||||||
if (m) STATE.itemsKnown[id] = m[1];
|
if (m) {
|
||||||
|
const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]);
|
||||||
|
STATE.itemsKnown[slot] = m[1];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Buffs
|
// Buffs
|
||||||
STATE.buffs = {};
|
STATE.buffs = {};
|
||||||
|
|
@ -717,6 +731,10 @@ function shouldBuff(name, minTurns) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
||||||
|
|
||||||
|
// Threat detection: SP bar max is 120px, monsters >85% are about to special
|
||||||
|
const THREAT_PX = Math.round(120 * 85 / 100); // ~102px
|
||||||
|
|
||||||
// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build,
|
// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build,
|
||||||
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
||||||
const BUFF_PRIORITY = [
|
const BUFF_PRIORITY = [
|
||||||
|
|
@ -795,6 +813,30 @@ function findStrongestMonster() {
|
||||||
return best >= 0 ? best : 0;
|
return best >= 0 ? best : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the monster with the highest SP/OC bar (most dangerous — about to special)
|
||||||
|
// The red bar (nbarred.png) has a max width of 120px
|
||||||
|
// Returns the index, or 0 if none found
|
||||||
|
function findDangerousMonster() {
|
||||||
|
let best = -1, bestSP = -1;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
const sp = STATE.monsterSp[i] || 0;
|
||||||
|
if (sp > bestSP) { bestSP = sp; best = i; }
|
||||||
|
});
|
||||||
|
return best >= 0 ? best : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are 2+ monsters with SP bar > threshold (imminent special attacks)
|
||||||
|
function hasHighThreat(threshold) {
|
||||||
|
let count = 0;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
const sp = STATE.monsterSp[i] || 0;
|
||||||
|
if (sp > threshold) count++;
|
||||||
|
});
|
||||||
|
return count >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -1124,7 +1166,7 @@ function strategyNovice() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : 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 };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1216,25 +1258,26 @@ function strategyAdept() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Weapon skills — before spirit stance
|
// 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar)
|
||||||
|
const isHighThreat = hasHighThreat(THREAT_PX);
|
||||||
const skillOC_N = CFG.skillOC || 75;
|
const skillOC_N = CFG.skillOC || 75;
|
||||||
if (STATE.oc >= skillOC_N) {
|
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = 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 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 5 && 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1259,7 +1302,7 @@ function strategyAdept() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : 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 };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1349,35 +1392,38 @@ function strategyVeteran() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Weapon skills only at OC thresholds (let OC build via basic attacks)
|
// 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar)
|
||||||
|
// If 2+ enemies have >85% SP bar, they're about to special — emergency
|
||||||
|
const isHighThreat = hasHighThreat(THREAT_PX);
|
||||||
|
|
||||||
const askillOC3 = CFG.skillOC || 80;
|
const askillOC3 = CFG.skillOC || 80;
|
||||||
if (STATE.oc >= askillOC3) {
|
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||||
const t = findStrongestMonster();
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
}
|
|
||||||
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
|
||||||
if (STATE.monsters.length >= 5) {
|
|
||||||
if (STATE.skillsKnown.includes('Rending Blow')) {
|
|
||||||
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 };
|
||||||
}
|
}
|
||||||
if (STATE.skillsKnown.includes('Shatter Strike')) {
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
||||||
|
if (STATE.monsters.length >= 5 && 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Great Cleave: boss/rare fights or single-target threat
|
||||||
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||||
}
|
}
|
||||||
// Other weapon skills (non-2H): generic use
|
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||||
if (!STATE.skillsKnown.includes('Great Cleave')) {
|
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||||
for (const sk of ps) {
|
for (const sk of ps) {
|
||||||
if (STATE.skillsKnown.includes(sk)) {
|
if (STATE.skillsKnown.includes(sk)) {
|
||||||
const t = findStrongestMonster();
|
const t = findDangerousMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,13 @@ function parseBattleState() {
|
||||||
|
|
||||||
// ── Monsters ──
|
// ── Monsters ──
|
||||||
STATE.monsters = $$('.btm1[onclick]');
|
STATE.monsters = $$('.btm1[onclick]');
|
||||||
|
// Parse each monster's SP/OC bar level (the red bar = special attack charge)
|
||||||
|
STATE.monsterSp = [];
|
||||||
|
$$('.btm1').forEach(m => {
|
||||||
|
const spBar = m.querySelector('.btm5 img[src$="barred.png"]');
|
||||||
|
const spPct = spBar ? (parseInt(spBar.style.width) || 0) : 0;
|
||||||
|
STATE.monsterSp.push(spPct);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Spells from spell pane ──
|
// ── Spells from spell pane ──
|
||||||
STATE.spellsKnown = [];
|
STATE.spellsKnown = [];
|
||||||
|
|
@ -140,12 +147,18 @@ function parseBattleState() {
|
||||||
|
|
||||||
// ── Items ──
|
// ── Items ──
|
||||||
STATE.itemsKnown = {};
|
STATE.itemsKnown = {};
|
||||||
$$('[id^="ikey_"]').forEach(el => {
|
// Read all item slots, including on-cooldown ones (which lose their id)
|
||||||
const id = el.id.replace('ikey_', '');
|
const itemPane = document.getElementById('pane_item');
|
||||||
|
if (itemPane) {
|
||||||
|
$$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => {
|
||||||
const omo = el.getAttribute('onmouseover') || '';
|
const omo = el.getAttribute('onmouseover') || '';
|
||||||
const m = omo.match(/battle\.set_infopane_item\((\d+)\)/);
|
const m = omo.match(/set_infopane_item\((\d+)\)/);
|
||||||
if (m) STATE.itemsKnown[id] = m[1];
|
if (m) {
|
||||||
|
const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]);
|
||||||
|
STATE.itemsKnown[slot] = m[1];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Buffs
|
// Buffs
|
||||||
STATE.buffs = {};
|
STATE.buffs = {};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.13.29';
|
const VERSION = '0.13.30';
|
||||||
|
|
||||||
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.29
|
// @version 0.13.30
|
||||||
// @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/*
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const STATE = {
|
||||||
interruptAlert: false,
|
interruptAlert: false,
|
||||||
|
|
||||||
monsters: [],
|
monsters: [],
|
||||||
|
monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special)
|
||||||
hp: 1,
|
hp: 1,
|
||||||
mp: 1,
|
mp: 1,
|
||||||
sp: 1,
|
sp: 1,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ function shouldBuff(name, minTurns) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
||||||
|
|
||||||
|
// Threat detection: SP bar max is 120px, monsters >85% are about to special
|
||||||
|
const THREAT_PX = Math.round(120 * 85 / 100); // ~102px
|
||||||
|
|
||||||
// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build,
|
// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build,
|
||||||
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
||||||
const BUFF_PRIORITY = [
|
const BUFF_PRIORITY = [
|
||||||
|
|
@ -109,6 +113,30 @@ function findStrongestMonster() {
|
||||||
return best >= 0 ? best : 0;
|
return best >= 0 ? best : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the monster with the highest SP/OC bar (most dangerous — about to special)
|
||||||
|
// The red bar (nbarred.png) has a max width of 120px
|
||||||
|
// Returns the index, or 0 if none found
|
||||||
|
function findDangerousMonster() {
|
||||||
|
let best = -1, bestSP = -1;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
const sp = STATE.monsterSp[i] || 0;
|
||||||
|
if (sp > bestSP) { bestSP = sp; best = i; }
|
||||||
|
});
|
||||||
|
return best >= 0 ? best : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are 2+ monsters with SP bar > threshold (imminent special attacks)
|
||||||
|
function hasHighThreat(threshold) {
|
||||||
|
let count = 0;
|
||||||
|
STATE.monsters.forEach((m, i) => {
|
||||||
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||||
|
const sp = STATE.monsterSp[i] || 0;
|
||||||
|
if (sp > threshold) count++;
|
||||||
|
});
|
||||||
|
return count >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -438,7 +466,7 @@ function strategyNovice() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : 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 };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -530,25 +558,26 @@ function strategyAdept() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Weapon skills — before spirit stance
|
// 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar)
|
||||||
|
const isHighThreat = hasHighThreat(THREAT_PX);
|
||||||
const skillOC_N = CFG.skillOC || 75;
|
const skillOC_N = CFG.skillOC || 75;
|
||||||
if (STATE.oc >= skillOC_N) {
|
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
const t = 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 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
if (STATE.monsters.length >= 5 && 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -573,7 +602,7 @@ function strategyAdept() {
|
||||||
if (ch) return ch;
|
if (ch) return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = findWeakestMonster();
|
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : 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 };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -663,35 +692,38 @@ function strategyVeteran() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Weapon skills only at OC thresholds (let OC build via basic attacks)
|
// 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar)
|
||||||
|
// If 2+ enemies have >85% SP bar, they're about to special — emergency
|
||||||
|
const isHighThreat = hasHighThreat(THREAT_PX);
|
||||||
|
|
||||||
const askillOC3 = CFG.skillOC || 80;
|
const askillOC3 = CFG.skillOC || 80;
|
||||||
if (STATE.oc >= askillOC3) {
|
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||||
const t = findStrongestMonster();
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
}
|
|
||||||
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
|
||||||
if (STATE.monsters.length >= 5) {
|
|
||||||
if (STATE.skillsKnown.includes('Rending Blow')) {
|
|
||||||
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 };
|
||||||
}
|
}
|
||||||
if (STATE.skillsKnown.includes('Shatter Strike')) {
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
||||||
|
if (STATE.monsters.length >= 5 && 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Great Cleave: boss/rare fights or single-target threat
|
||||||
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||||
}
|
}
|
||||||
// Other weapon skills (non-2H): generic use
|
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||||
if (!STATE.skillsKnown.includes('Great Cleave')) {
|
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||||
for (const sk of ps) {
|
for (const sk of ps) {
|
||||||
if (STATE.skillsKnown.includes(sk)) {
|
if (STATE.skillsKnown.includes(sk)) {
|
||||||
const t = findStrongestMonster();
|
const t = findDangerousMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue