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:
GaboGG 2026-07-27 13:07:39 -04:00
parent b6d78067ce
commit b5a5c3df54
7 changed files with 246 additions and 108 deletions

View file

@ -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 };
} }
} }

View file

@ -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 };
} }
} }

View file

@ -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 = {};

View file

@ -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

View file

@ -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/*

View file

@ -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,

View file

@ -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 };
} }
} }