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==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.13.29
|
||||
// @version 0.13.30
|
||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.13.29';
|
||||
const VERSION = '0.13.30';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
@ -89,6 +89,7 @@ const STATE = {
|
|||
interruptAlert: false,
|
||||
|
||||
monsters: [],
|
||||
monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special)
|
||||
hp: 1,
|
||||
mp: 1,
|
||||
sp: 1,
|
||||
|
|
@ -426,6 +427,13 @@ function parseBattleState() {
|
|||
|
||||
// ── Monsters ──
|
||||
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 ──
|
||||
STATE.spellsKnown = [];
|
||||
|
|
@ -485,12 +493,18 @@ function parseBattleState() {
|
|||
|
||||
// ── Items ──
|
||||
STATE.itemsKnown = {};
|
||||
$$('[id^="ikey_"]').forEach(el => {
|
||||
const id = el.id.replace('ikey_', '');
|
||||
// Read all item slots, including on-cooldown ones (which lose their id)
|
||||
const itemPane = document.getElementById('pane_item');
|
||||
if (itemPane) {
|
||||
$$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => {
|
||||
const omo = el.getAttribute('onmouseover') || '';
|
||||
const m = omo.match(/battle\.set_infopane_item\((\d+)\)/);
|
||||
if (m) STATE.itemsKnown[id] = m[1];
|
||||
const m = omo.match(/set_infopane_item\((\d+)\)/);
|
||||
if (m) {
|
||||
const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]);
|
||||
STATE.itemsKnown[slot] = m[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Buffs
|
||||
STATE.buffs = {};
|
||||
|
|
@ -717,6 +731,10 @@ function shouldBuff(name, minTurns) {
|
|||
}
|
||||
|
||||
// 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,
|
||||
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
||||
const BUFF_PRIORITY = [
|
||||
|
|
@ -795,6 +813,30 @@ function findStrongestMonster() {
|
|||
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) {
|
||||
const m = STATE.monsters[idx];
|
||||
if (!m) return false;
|
||||
|
|
@ -1124,7 +1166,7 @@ function strategyNovice() {
|
|||
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'))
|
||||
return { type: 'attack', target: t };
|
||||
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;
|
||||
if (STATE.oc >= skillOC_N) {
|
||||
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||
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 };
|
||||
}
|
||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||
}
|
||||
}
|
||||
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -1259,7 +1302,7 @@ function strategyAdept() {
|
|||
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'))
|
||||
return { type: 'attack', target: t };
|
||||
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;
|
||||
if (STATE.oc >= askillOC3) {
|
||||
// Great Cleave: boss/rare fights only
|
||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||
}
|
||||
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
||||
if (STATE.monsters.length >= 5) {
|
||||
if (STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = findStrongestMonster();
|
||||
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
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'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
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 (!STATE.skillsKnown.includes('Great Cleave')) {
|
||||
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||
for (const sk of ps) {
|
||||
if (STATE.skillsKnown.includes(sk)) {
|
||||
const t = findStrongestMonster();
|
||||
const t = findDangerousMonster();
|
||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.13.29
|
||||
// @version 0.13.30
|
||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.13.29';
|
||||
const VERSION = '0.13.30';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
@ -89,6 +89,7 @@ const STATE = {
|
|||
interruptAlert: false,
|
||||
|
||||
monsters: [],
|
||||
monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special)
|
||||
hp: 1,
|
||||
mp: 1,
|
||||
sp: 1,
|
||||
|
|
@ -426,6 +427,13 @@ function parseBattleState() {
|
|||
|
||||
// ── Monsters ──
|
||||
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 ──
|
||||
STATE.spellsKnown = [];
|
||||
|
|
@ -485,12 +493,18 @@ function parseBattleState() {
|
|||
|
||||
// ── Items ──
|
||||
STATE.itemsKnown = {};
|
||||
$$('[id^="ikey_"]').forEach(el => {
|
||||
const id = el.id.replace('ikey_', '');
|
||||
// Read all item slots, including on-cooldown ones (which lose their id)
|
||||
const itemPane = document.getElementById('pane_item');
|
||||
if (itemPane) {
|
||||
$$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => {
|
||||
const omo = el.getAttribute('onmouseover') || '';
|
||||
const m = omo.match(/battle\.set_infopane_item\((\d+)\)/);
|
||||
if (m) STATE.itemsKnown[id] = m[1];
|
||||
const m = omo.match(/set_infopane_item\((\d+)\)/);
|
||||
if (m) {
|
||||
const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]);
|
||||
STATE.itemsKnown[slot] = m[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Buffs
|
||||
STATE.buffs = {};
|
||||
|
|
@ -717,6 +731,10 @@ function shouldBuff(name, minTurns) {
|
|||
}
|
||||
|
||||
// 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,
|
||||
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
||||
const BUFF_PRIORITY = [
|
||||
|
|
@ -795,6 +813,30 @@ function findStrongestMonster() {
|
|||
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) {
|
||||
const m = STATE.monsters[idx];
|
||||
if (!m) return false;
|
||||
|
|
@ -1124,7 +1166,7 @@ function strategyNovice() {
|
|||
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'))
|
||||
return { type: 'attack', target: t };
|
||||
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;
|
||||
if (STATE.oc >= skillOC_N) {
|
||||
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||
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 };
|
||||
}
|
||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||
}
|
||||
}
|
||||
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -1259,7 +1302,7 @@ function strategyAdept() {
|
|||
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'))
|
||||
return { type: 'attack', target: t };
|
||||
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;
|
||||
if (STATE.oc >= askillOC3) {
|
||||
// Great Cleave: boss/rare fights only
|
||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||
}
|
||||
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
||||
if (STATE.monsters.length >= 5) {
|
||||
if (STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = findStrongestMonster();
|
||||
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
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'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
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 (!STATE.skillsKnown.includes('Great Cleave')) {
|
||||
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||
for (const sk of ps) {
|
||||
if (STATE.skillsKnown.includes(sk)) {
|
||||
const t = findStrongestMonster();
|
||||
const t = findDangerousMonster();
|
||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,13 @@ function parseBattleState() {
|
|||
|
||||
// ── Monsters ──
|
||||
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 ──
|
||||
STATE.spellsKnown = [];
|
||||
|
|
@ -140,12 +147,18 @@ function parseBattleState() {
|
|||
|
||||
// ── Items ──
|
||||
STATE.itemsKnown = {};
|
||||
$$('[id^="ikey_"]').forEach(el => {
|
||||
const id = el.id.replace('ikey_', '');
|
||||
// Read all item slots, including on-cooldown ones (which lose their id)
|
||||
const itemPane = document.getElementById('pane_item');
|
||||
if (itemPane) {
|
||||
$$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => {
|
||||
const omo = el.getAttribute('onmouseover') || '';
|
||||
const m = omo.match(/battle\.set_infopane_item\((\d+)\)/);
|
||||
if (m) STATE.itemsKnown[id] = m[1];
|
||||
const m = omo.match(/set_infopane_item\((\d+)\)/);
|
||||
if (m) {
|
||||
const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]);
|
||||
STATE.itemsKnown[slot] = m[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Buffs
|
||||
STATE.buffs = {};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.13.29';
|
||||
const VERSION = '0.13.30';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.13.29
|
||||
// @version 0.13.30
|
||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const STATE = {
|
|||
interruptAlert: false,
|
||||
|
||||
monsters: [],
|
||||
monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special)
|
||||
hp: 1,
|
||||
mp: 1,
|
||||
sp: 1,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ function shouldBuff(name, minTurns) {
|
|||
}
|
||||
|
||||
// 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,
|
||||
// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds.
|
||||
const BUFF_PRIORITY = [
|
||||
|
|
@ -109,6 +113,30 @@ function findStrongestMonster() {
|
|||
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) {
|
||||
const m = STATE.monsters[idx];
|
||||
if (!m) return false;
|
||||
|
|
@ -438,7 +466,7 @@ function strategyNovice() {
|
|||
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'))
|
||||
return { type: 'attack', target: t };
|
||||
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;
|
||||
if (STATE.oc >= skillOC_N) {
|
||||
if (STATE.oc >= skillOC_N || isHighThreat) {
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||
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 };
|
||||
}
|
||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||
}
|
||||
}
|
||||
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -573,7 +602,7 @@ function strategyAdept() {
|
|||
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'))
|
||||
return { type: 'attack', target: t };
|
||||
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;
|
||||
if (STATE.oc >= askillOC3) {
|
||||
// Great Cleave: boss/rare fights only
|
||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||
}
|
||||
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
||||
if (STATE.monsters.length >= 5) {
|
||||
if (STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = findStrongestMonster();
|
||||
if (STATE.oc >= askillOC3 || isHighThreat) {
|
||||
// Emergency: 2+ monsters about to special — spend OC to kill them fast
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
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'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||
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 (!STATE.skillsKnown.includes('Great Cleave')) {
|
||||
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||
for (const sk of ps) {
|
||||
if (STATE.skillsKnown.includes(sk)) {
|
||||
const t = findStrongestMonster();
|
||||
const t = findDangerousMonster();
|
||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue