v0.13.34 - Explicit action names in log + _reason tags per decision
- Log now shows spell names/skill names/item names explicitly: '[HV] ▶ spell → Haste (self)' instead of '▶ spell → monster 0' '[HV] ▶ spell → Fiery Blast → monster 1 (ch fallback)' '[HV] ▶ item → Spirit Draught (self) (sp55)' - Added _reason tag to every action return: 'hp60', 'sp55', 'buff', 'ch score 81', 'threat', 'oc', 'boss', 'stun', 'mana', 'gem', 'mystic', 'fallback', 'dmg' - Now you can see exactly WHY each action was chosen on every turn
This commit is contained in:
parent
949aab915f
commit
467e844aae
6 changed files with 144 additions and 60 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.33
|
// @version 0.13.34
|
||||||
// @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.33';
|
const VERSION = '0.13.34';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -1017,7 +1017,7 @@ function findBestChannelingTarget() {
|
||||||
// Only use a buff if it actually needs refreshing (score >= 30 means
|
// Only use a buff if it actually needs refreshing (score >= 30 means
|
||||||
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
||||||
if (candidates[0].score >= 30) {
|
if (candidates[0].score >= 30) {
|
||||||
return { type: 'spell', name: candidates[0].spell, selfTarget: true };
|
return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: `ch score ${candidates[0].score.toFixed(0)}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1330,18 +1330,18 @@ function strategyVeteran() {
|
||||||
// 0. Mystic Gem for channeling — only use once per battle
|
// 0. Mystic Gem for channeling — only use once per battle
|
||||||
const gem = hasUsableGem('channel');
|
const gem = hasUsableGem('channel');
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < 0.60) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
const gem = hasUsableGem('heal');
|
const gem = hasUsableGem('heal');
|
||||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Cure
|
// 2. Cure
|
||||||
if (STATE.hp < CFG.cureHP) {
|
if (STATE.hp < CFG.cureHP) {
|
||||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||||
if (c) return { type: 'spell', name: c, selfTarget: true };
|
if (c) return { type: 'spell', name: c, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
||||||
|
|
@ -1350,7 +1350,7 @@ function strategyVeteran() {
|
||||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
if (slot === 'p') continue;
|
if (slot === 'p') continue;
|
||||||
const def = ITEMS[parseInt(info)];
|
const def = ITEMS[parseInt(info)];
|
||||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true };
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1369,7 +1369,7 @@ function strategyVeteran() {
|
||||||
for (const b of BUFF_PRIORITY) {
|
for (const b of BUFF_PRIORITY) {
|
||||||
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
|
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
|
||||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1420,20 +1420,20 @@ function strategyVeteran() {
|
||||||
// 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
||||||
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 = isHighThreat ? findDangerousMonster() : 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, _reason: 'stun' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Great Cleave: boss/rare fights or single-target threat
|
// Great Cleave: boss/rare fights or single-target threat
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : 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, _reason: isHighThreat ? 'threat' : 'boss' };
|
||||||
}
|
}
|
||||||
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||||
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||||
|
|
@ -1442,21 +1442,36 @@ function strategyVeteran() {
|
||||||
for (const sk of ps) {
|
for (const sk of ps) {
|
||||||
if (STATE.skillsKnown.includes(sk)) {
|
if (STATE.skillsKnown.includes(sk)) {
|
||||||
const t = findDangerousMonster();
|
const t = findDangerousMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
if (t >= 0) return { type: 'skill', name: sk, target: t, _reason: 'threat' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Damage spells
|
// 5. Spirit gem auto-consume
|
||||||
|
const spGem = document.getElementById('ikey_p');
|
||||||
|
const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
|
||||||
|
if (spGemId && spGemId[1] === '10007') {
|
||||||
|
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true, _reason: 'gem' };
|
||||||
|
}
|
||||||
|
// Spirit potions — only at SP < threshold (Spark needs 50%)
|
||||||
|
if (STATE.sp < CFG.spiritPotionSP) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Damage spells
|
||||||
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
||||||
if (dmg && STATE.mp > 0.15) {
|
if (dmg && STATE.mp > 0.15) {
|
||||||
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'spell', name: dmg, target: t };
|
return { type: 'spell', name: dmg, target: t, _reason: 'dmg' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Channeling kickstart or basic attack
|
// 7. Channeling kickstart or basic attack
|
||||||
{
|
{
|
||||||
const k = kickstartChanneling();
|
const k = kickstartChanneling();
|
||||||
if (k) return k;
|
if (k) return k;
|
||||||
|
|
@ -1468,9 +1483,9 @@ function strategyVeteran() {
|
||||||
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, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1665,8 +1680,21 @@ function setupKeybindings() {
|
||||||
const a = getRecommendedAction();
|
const a = getRecommendedAction();
|
||||||
if (a) {
|
if (a) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
// Show explicit action details
|
||||||
console.log(`%c[HV] ▶ ${a.type} → ${label}`, 'color:#0f0');
|
let label = '';
|
||||||
|
if (a.type === 'spell') {
|
||||||
|
label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)');
|
||||||
|
} else if (a.type === 'skill') {
|
||||||
|
label = a.name + ' → monster ' + a.target;
|
||||||
|
} else if (a.type === 'item') {
|
||||||
|
const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')];
|
||||||
|
label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : '');
|
||||||
|
} else if (a.type === 'attack') {
|
||||||
|
label = 'monster ' + a.target;
|
||||||
|
} else {
|
||||||
|
label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
||||||
|
}
|
||||||
|
console.log(`%c[HV] ▶ ${a.type} → ${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0');
|
||||||
executeAction(a);
|
executeAction(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.33
|
// @version 0.13.34
|
||||||
// @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.33';
|
const VERSION = '0.13.34';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -1017,7 +1017,7 @@ function findBestChannelingTarget() {
|
||||||
// Only use a buff if it actually needs refreshing (score >= 30 means
|
// Only use a buff if it actually needs refreshing (score >= 30 means
|
||||||
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
||||||
if (candidates[0].score >= 30) {
|
if (candidates[0].score >= 30) {
|
||||||
return { type: 'spell', name: candidates[0].spell, selfTarget: true };
|
return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: `ch score ${candidates[0].score.toFixed(0)}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1330,18 +1330,18 @@ function strategyVeteran() {
|
||||||
// 0. Mystic Gem for channeling — only use once per battle
|
// 0. Mystic Gem for channeling — only use once per battle
|
||||||
const gem = hasUsableGem('channel');
|
const gem = hasUsableGem('channel');
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < 0.60) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
const gem = hasUsableGem('heal');
|
const gem = hasUsableGem('heal');
|
||||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Cure
|
// 2. Cure
|
||||||
if (STATE.hp < CFG.cureHP) {
|
if (STATE.hp < CFG.cureHP) {
|
||||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||||
if (c) return { type: 'spell', name: c, selfTarget: true };
|
if (c) return { type: 'spell', name: c, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
||||||
|
|
@ -1350,7 +1350,7 @@ function strategyVeteran() {
|
||||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
if (slot === 'p') continue;
|
if (slot === 'p') continue;
|
||||||
const def = ITEMS[parseInt(info)];
|
const def = ITEMS[parseInt(info)];
|
||||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true };
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1369,7 +1369,7 @@ function strategyVeteran() {
|
||||||
for (const b of BUFF_PRIORITY) {
|
for (const b of BUFF_PRIORITY) {
|
||||||
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
|
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
|
||||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1420,20 +1420,20 @@ function strategyVeteran() {
|
||||||
// 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
||||||
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 = isHighThreat ? findDangerousMonster() : 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, _reason: 'stun' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Great Cleave: boss/rare fights or single-target threat
|
// Great Cleave: boss/rare fights or single-target threat
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : 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, _reason: isHighThreat ? 'threat' : 'boss' };
|
||||||
}
|
}
|
||||||
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||||
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||||
|
|
@ -1442,21 +1442,36 @@ function strategyVeteran() {
|
||||||
for (const sk of ps) {
|
for (const sk of ps) {
|
||||||
if (STATE.skillsKnown.includes(sk)) {
|
if (STATE.skillsKnown.includes(sk)) {
|
||||||
const t = findDangerousMonster();
|
const t = findDangerousMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
if (t >= 0) return { type: 'skill', name: sk, target: t, _reason: 'threat' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Damage spells
|
// 5. Spirit gem auto-consume
|
||||||
|
const spGem = document.getElementById('ikey_p');
|
||||||
|
const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
|
||||||
|
if (spGemId && spGemId[1] === '10007') {
|
||||||
|
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true, _reason: 'gem' };
|
||||||
|
}
|
||||||
|
// Spirit potions — only at SP < threshold (Spark needs 50%)
|
||||||
|
if (STATE.sp < CFG.spiritPotionSP) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Damage spells
|
||||||
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
||||||
if (dmg && STATE.mp > 0.15) {
|
if (dmg && STATE.mp > 0.15) {
|
||||||
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'spell', name: dmg, target: t };
|
return { type: 'spell', name: dmg, target: t, _reason: 'dmg' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Channeling kickstart or basic attack
|
// 7. Channeling kickstart or basic attack
|
||||||
{
|
{
|
||||||
const k = kickstartChanneling();
|
const k = kickstartChanneling();
|
||||||
if (k) return k;
|
if (k) return k;
|
||||||
|
|
@ -1468,9 +1483,9 @@ function strategyVeteran() {
|
||||||
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, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1665,8 +1680,21 @@ function setupKeybindings() {
|
||||||
const a = getRecommendedAction();
|
const a = getRecommendedAction();
|
||||||
if (a) {
|
if (a) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
// Show explicit action details
|
||||||
console.log(`%c[HV] ▶ ${a.type} → ${label}`, 'color:#0f0');
|
let label = '';
|
||||||
|
if (a.type === 'spell') {
|
||||||
|
label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)');
|
||||||
|
} else if (a.type === 'skill') {
|
||||||
|
label = a.name + ' → monster ' + a.target;
|
||||||
|
} else if (a.type === 'item') {
|
||||||
|
const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')];
|
||||||
|
label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : '');
|
||||||
|
} else if (a.type === 'attack') {
|
||||||
|
label = 'monster ' + a.target;
|
||||||
|
} else {
|
||||||
|
label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
||||||
|
}
|
||||||
|
console.log(`%c[HV] ▶ ${a.type} → ${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0');
|
||||||
executeAction(a);
|
executeAction(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.13.33';
|
const VERSION = '0.13.34';
|
||||||
|
|
||||||
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.33
|
// @version 0.13.34
|
||||||
// @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/*
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,21 @@ function setupKeybindings() {
|
||||||
const a = getRecommendedAction();
|
const a = getRecommendedAction();
|
||||||
if (a) {
|
if (a) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
// Show explicit action details
|
||||||
console.log(`%c[HV] ▶ ${a.type} → ${label}`, 'color:#0f0');
|
let label = '';
|
||||||
|
if (a.type === 'spell') {
|
||||||
|
label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)');
|
||||||
|
} else if (a.type === 'skill') {
|
||||||
|
label = a.name + ' → monster ' + a.target;
|
||||||
|
} else if (a.type === 'item') {
|
||||||
|
const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')];
|
||||||
|
label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : '');
|
||||||
|
} else if (a.type === 'attack') {
|
||||||
|
label = 'monster ' + a.target;
|
||||||
|
} else {
|
||||||
|
label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
||||||
|
}
|
||||||
|
console.log(`%c[HV] ▶ ${a.type} → ${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0');
|
||||||
executeAction(a);
|
executeAction(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,7 @@ function findBestChannelingTarget() {
|
||||||
// Only use a buff if it actually needs refreshing (score >= 30 means
|
// Only use a buff if it actually needs refreshing (score >= 30 means
|
||||||
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
||||||
if (candidates[0].score >= 30) {
|
if (candidates[0].score >= 30) {
|
||||||
return { type: 'spell', name: candidates[0].spell, selfTarget: true };
|
return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: `ch score ${candidates[0].score.toFixed(0)}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -616,18 +616,18 @@ function strategyVeteran() {
|
||||||
// 0. Mystic Gem for channeling — only use once per battle
|
// 0. Mystic Gem for channeling — only use once per battle
|
||||||
const gem = hasUsableGem('channel');
|
const gem = hasUsableGem('channel');
|
||||||
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
if (gem && !STATE.channeling && !STATE._mysticUsed)
|
||||||
return { type: 'item', id: gem.id, selfTarget: true };
|
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
|
||||||
|
|
||||||
// 1. Health items — safe to use early
|
// 1. Health items — safe to use early
|
||||||
if (STATE.hp < 0.60) {
|
if (STATE.hp < CFG.cureItemHP) {
|
||||||
const gem = hasUsableGem('heal');
|
const gem = hasUsableGem('heal');
|
||||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Cure
|
// 2. Cure
|
||||||
if (STATE.hp < CFG.cureHP) {
|
if (STATE.hp < CFG.cureHP) {
|
||||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||||
if (c) return { type: 'spell', name: c, selfTarget: true };
|
if (c) return { type: 'spell', name: c, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
|
||||||
|
|
@ -636,7 +636,7 @@ function strategyVeteran() {
|
||||||
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
if (slot === 'p') continue;
|
if (slot === 'p') continue;
|
||||||
const def = ITEMS[parseInt(info)];
|
const def = ITEMS[parseInt(info)];
|
||||||
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true };
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -655,7 +655,7 @@ function strategyVeteran() {
|
||||||
for (const b of BUFF_PRIORITY) {
|
for (const b of BUFF_PRIORITY) {
|
||||||
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
|
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
|
||||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -706,20 +706,20 @@ function strategyVeteran() {
|
||||||
// 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
const t = isHighThreat ? findDangerousMonster() : findStrongestMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
|
||||||
}
|
}
|
||||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor)
|
||||||
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 = isHighThreat ? findDangerousMonster() : 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, _reason: 'stun' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Great Cleave: boss/rare fights or single-target threat
|
// Great Cleave: boss/rare fights or single-target threat
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = isHighThreat ? findDangerousMonster() : 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, _reason: isHighThreat ? 'threat' : 'boss' };
|
||||||
}
|
}
|
||||||
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
// If high threat and no AoE skill available, use any weapon skill on the most dangerous
|
||||||
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) {
|
||||||
|
|
@ -728,21 +728,36 @@ function strategyVeteran() {
|
||||||
for (const sk of ps) {
|
for (const sk of ps) {
|
||||||
if (STATE.skillsKnown.includes(sk)) {
|
if (STATE.skillsKnown.includes(sk)) {
|
||||||
const t = findDangerousMonster();
|
const t = findDangerousMonster();
|
||||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
if (t >= 0) return { type: 'skill', name: sk, target: t, _reason: 'threat' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Damage spells
|
// 5. Spirit gem auto-consume
|
||||||
|
const spGem = document.getElementById('ikey_p');
|
||||||
|
const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
|
||||||
|
if (spGemId && spGemId[1] === '10007') {
|
||||||
|
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true, _reason: 'gem' };
|
||||||
|
}
|
||||||
|
// Spirit potions — only at SP < threshold (Spark needs 50%)
|
||||||
|
if (STATE.sp < CFG.spiritPotionSP) {
|
||||||
|
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
|
||||||
|
if (slot === 'p') continue;
|
||||||
|
const def = ITEMS[parseInt(info)];
|
||||||
|
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Damage spells
|
||||||
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
||||||
if (dmg && STATE.mp > 0.15) {
|
if (dmg && STATE.mp > 0.15) {
|
||||||
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
||||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||||
return { type: 'spell', name: dmg, target: t };
|
return { type: 'spell', name: dmg, target: t, _reason: 'dmg' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Channeling kickstart or basic attack
|
// 7. Channeling kickstart or basic attack
|
||||||
{
|
{
|
||||||
const k = kickstartChanneling();
|
const k = kickstartChanneling();
|
||||||
if (k) return k;
|
if (k) return k;
|
||||||
|
|
@ -754,8 +769,8 @@ function strategyVeteran() {
|
||||||
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, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue