v0.16.0 - Complement community scripts (AGY refactor)

REMOVED (owned by MB/HVUT):
- RE timer (HVUtils has one)
- Battle keybinds Q/H/C (Monsterbation owns battle keys; kept , for settings)
- Hover-attack battle UI hooks (Monsterbation)
- Battle logger (Monsterbation combat log)
- Spirit/potion/scroll auto-use in battle (Monsterbation)
- Armory bulk sell/salvage/shrine UI + Keep Top 10% button (HVUtils bulk tools)

RESOLVED:
- Q keybind collision: yielded to Monsterbation
- DOM double-injection: removed duplicate overlays
- Storage: hvunified_ namespace isolated from HVUT GM_* storage

KEPT (gaps community doesn't cover):
- Gear analysis/scoring (equipped vs store, 19-category advisory)
- Strategy guidance as advisory layer (HV.advice/HV.action)
- Guidance/abilities/difficulty advisor
- window.HV public API bridge
This commit is contained in:
GaboGG 2026-08-04 14:03:34 -04:00
parent 589ed7e34c
commit f2fc00965e
15 changed files with 198 additions and 3051 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,16 +10,14 @@ function executeAction(a) {
case 'spell': result = a.selfTarget ? castSelfSpell(a.name) : castTargetSpell(a.name, a.target); break;
case 'skill': result = useSkill(a.name, a.target); break;
case 'item': result = useItem(a.id); break;
case 'toggle_spirit': result = toggleSpiritStance(); break;
}
if (result) {
// After dispatching an action, stagger the debounce so we don't queue
// another action while the game processes this one (typically ~500-1000ms)
STATE._lastActionTime = Date.now();
}
return result;
}
function attackMonster(i) {
if (i < 0 || i >= STATE.monsters.length) return false;
const m = STATE.monsters[i];
@ -111,9 +109,3 @@ function useSkill(n, i) {
return true;
}
function toggleSpiritStance() {
const t = document.getElementById('ckey_spirit');
if (!t) return false;
t.click();
return true;
}

View file

@ -24,264 +24,7 @@ function gradeEquipment(name) {
}
function enhanceArmory() {
const equipList = document.getElementById('equiplist');
if (!equipList || document.getElementById('hv-armory-bar')) return;
// ── Parse current equipment in the list ──
const items = [];
let currentCategory = '';
$$('tr', equipList).forEach(row => {
if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim();
return;
}
const cb = row.querySelector('input[name="eqids[]"]');
if (!cb) return;
const id = cb.value;
const label = row.querySelector('label');
const name = label ? (label.textContent || '').trim() : '';
// Status icons in the label text
const equipped = name.includes('🗡');
const locked = name.includes('🔒');
const pinned = name.includes('📌');
const stored = name.includes('📦');
const protected_ = name.includes('🛡');
// Strip status icons for clean name
const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim();
items.push({
id, name: cleanName, slot: currentCategory,
equipped, locked, pinned, stored, protected: protected_,
checkbox: cb, row,
grade: gradeEquipment(cleanName),
});
});
if (items.length === 0) return;
// ── Detect which armory tab we're on ──
let screen = 'organize';
const url = window.location.href || '';
const sm = url.match(/screen=(\w+)/);
if (sm) screen = sm[1];
// ── Counts ──
const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length;
const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length;
const keepCount = items.filter(i => i.grade.action === 'keep').length;
// ── Build the toolbar ──
const toolbar = document.createElement('div');
toolbar.id = 'hv-armory-bar';
toolbar.style.cssText = css({
display: 'flex',
gap: '4px',
flexWrap: 'wrap',
padding: '4px 6px',
marginBottom: '2px',
background: '#1a1a2e',
borderRadius: '4px',
fontSize: '10px',
fontFamily: 'monospace',
alignItems: 'center',
});
// Summary
const summary = document.createElement('span');
summary.style.cssText = 'color:#888;margin-right:6px';
summary.textContent = `${keepCount}${salvCount} 💰${sellCount}`;
toolbar.appendChild(summary);
// ── Smart auto-select: check boxes based on grade ──
function autoSelect(action) {
items.forEach(item => {
if (item.grade.action !== action) return;
if (item.equipped || item.locked) return;
item.checkbox.checked = true;
});
// Trigger the game's update function if available
if (typeof update_selected_count === 'function') update_selected_count();
}
// ── Select by quality ──
function selectByQuality(below) {
const idx = KB.qualities.indexOf(below);
items.forEach(item => {
const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q)));
if (qi < 0) return;
if (qi <= idx && !item.equipped && !item.locked) {
item.checkbox.checked = true;
}
});
if (typeof update_selected_count === 'function') update_selected_count();
}
function clearAll() {
items.forEach(item => item.checkbox.checked = false);
if (typeof update_selected_count === 'function') update_selected_count();
}
// Buttons — only show relevant ones for current screen
if (screen === 'sell') {
const b = document.createElement('input');
b.type = 'button';
b.value = `💰 Select Crude (${sellCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('sell');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '💰 Select ≤Fair';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Fair');
toolbar.appendChild(b2);
}
if (screen === 'salvage') {
const b = document.createElement('input');
b.type = 'button';
b.value = `♻ Select Salvage (${salvCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('salvage');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '♻ Select ≤Average';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Average');
toolbar.appendChild(b2);
// "Sell Salvaged Equipment" toggle is already native
}
if (screen === 'organize') {
const b = document.createElement('input');
b.type = 'button';
b.value = '📌 Pin Equipped';
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => {
// Select all equipped items so they can be pinned
items.forEach(item => { if (item.equipped) item.checkbox.checked = true; });
if (typeof update_selected_count === 'function') update_selected_count();
};
toolbar.appendChild(b);
}
// ── Keep Top 5 per category (score-based), select the rest ──
// Categories: 1H, 2H, Staff, Shield + 5 armor slots × (Cloth, Light, Heavy)
function qualityIndex(name) {
const q = KB.qualities.find(qq => name.includes(qq));
return q ? KB.qualities.indexOf(q) : 0;
}
function keepTop5AndSelect(action) {
const catMap = new Map();
items.forEach(item => {
if (item.equipped || item.locked || item.pinned) return;
const data = getHVEquipData(item.id);
const stats = data && data.d ? parseEquipHTML(data.d) : null;
if (!stats) return; // skip unscraped items — leave untouched
const type = (stats.type || '');
let cat = '?';
if (type.includes('Two-handed')) cat = '2H';
else if (type.includes('One-handed')) cat = '1H';
else if (type.includes('Staff')) cat = 'Staff';
else if (type.includes('Shield')) cat = 'Shield';
else if (type.includes('Cloth')) cat = 'Cloth';
else if (type.includes('Light')) cat = 'Light';
else if (type.includes('Heavy')) cat = 'Heavy';
// For armor, append the slot (Head/Body/Hands/Legs/Feet)
if (['Cloth', 'Light', 'Heavy'].includes(cat)) {
const slot = gearDetectSlot({ name: item.name, category: cat });
if (!slot) return;
cat += ' ' + slot;
}
const isWeaponCat = cat.startsWith('1H') || cat.startsWith('2H') || cat === 'Staff' || cat === 'Shield';
const quality = qualityIndex(item.name);
const score = isWeaponCat
? gearScoreWeapon({ name: item.name, stats, quality })
: gearScoreArmor({ name: item.name, stats, quality });
if (!catMap.has(cat)) catMap.set(cat, []);
catMap.get(cat).push({ item, score });
});
// Keep top 10% per category (min 5), select the rest
let selected = 0;
catMap.forEach((list, cat) => {
list.sort((a, b) => b.score - a.score);
const keepCount = Math.max(5, Math.ceil(list.length * 0.10));
const keep = list.slice(0, keepCount);
const dump = list.slice(keepCount);
dump.forEach(({ item }) => {
item.checkbox.checked = true;
selected++;
});
console.log(`%c[HV] 🗂 ${cat}: ${list.length} items → keep ${keep.length} (top ${keep.map(k => k.score.toFixed(0)).join('/')}), select ${dump.length}`, 'color:#0f0');
});
if (typeof update_selected_count === 'function') update_selected_count();
return selected;
}
// ── Keep Top 10% button — available on sell and salvage screens ──
if (screen === 'sell' || screen === 'salvage') {
const b3 = document.createElement('input');
b3.type = 'button';
b3.value = `⭐ Keep Top 10% / Select ${screen === 'sell' ? 'Sell' : 'Salvage'} Rest`;
b3.style.cssText = 'padding:2px 6px;cursor:pointer;background:#7a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px';
b3.title = 'Keeps the top 10% (min 5) highest-scoring items in each category (1H, 2H, Staff, Shield, and each armor slot per Cloth/Light/Heavy). Selects everything else.';
b3.onclick = () => {
const n = keepTop5AndSelect(screen);
console.log(`%c[HV] ⭐ Keep Top 10%: ${n} items selected for ${screen}`, 'color:#0f0');
};
toolbar.appendChild(b3);
}
// Clear button always available
const clear = document.createElement('input');
clear.type = 'button';
clear.value = '✗ Clear';
clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
clear.onclick = clearAll;
toolbar.appendChild(clear);
// Select All / Invert Selection (useful on any screen)
const selAll = document.createElement('input');
selAll.type = 'button';
selAll.value = '☐ All';
selAll.style.cssText = 'padding:2px 6px;cursor:pointer;background:#3a4a6a;color:#fff;border:none;border-radius:3px;font-size:10px';
selAll.onclick = () => {
items.forEach(item => { item.checkbox.checked = true; });
if (typeof update_selected_count === 'function') update_selected_count();
};
toolbar.appendChild(selAll);
const invert = document.createElement('input');
invert.type = 'button';
invert.value = '⊞ Invert';
invert.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
invert.onclick = () => {
items.forEach(item => { item.checkbox.checked = !item.checkbox.checked; });
if (typeof update_selected_count === 'function') update_selected_count();
};
toolbar.appendChild(invert);
// ── Insert into page ──
const eqSelect = document.getElementById('equipselect_outer') ||
document.querySelector('#equipselect_left, #armory_right > div');
if (equipList && equipList.parentNode) {
equipList.parentNode.insertBefore(toolbar, equipList);
}
// Physical bulk select/sell/salvage/shrine buttons in Armory are owned by HVUT.
// HV Unified provides advisory scoring via HV.analyzeGear() and gear-analysis panel.
}

View file

@ -1,108 +1,9 @@
// ═══════════════════════════════════════════════════════════════════════
// BATTLE LOGGER — saves raw battle log lines to localStorage in real time
// BATTLE LOGGER — REMOVED (Handled by Monsterbation combat log)
// ═══════════════════════════════════════════════════════════════════════
let _lastLogCount = 0;
const LOG_KEY = SP + 'battleLog';
function initBattleLog() {}
function logRound() {}
function getBattleLog() { return null; }
function getBattleSummary() { return null; }
function initBattleLog() {
_lastLogCount = 0;
// Clear previous log
try { localStorage.removeItem(LOG_KEY); } catch (e) {}
}
function logRound() {
const log = document.getElementById('textlog');
if (!log) return;
const allRows = log.querySelectorAll('tr');
if (allRows.length <= _lastLogCount) return;
// Get only the NEW rows since last capture
const newLines = [];
for (let i = _lastLogCount; i < allRows.length; i++) {
const text = (allRows[i].textContent || '').trim();
if (text) newLines.push(text);
}
_lastLogCount = allRows.length;
if (newLines.length === 0) return;
// Read existing log from localStorage, append new lines, save back
let existing = [];
try {
const saved = localStorage.getItem(LOG_KEY);
if (saved) existing = JSON.parse(saved);
} catch (e) {}
existing.push(...newLines);
try {
localStorage.setItem(LOG_KEY, JSON.stringify(existing));
} catch (e) {
// localStorage full — keep only last 500 lines
try {
const trimmed = existing.slice(-500);
localStorage.setItem(LOG_KEY, JSON.stringify(trimmed));
} catch (e2) {}
}
}
function getBattleLog() {
try {
const saved = localStorage.getItem(LOG_KEY);
return saved ? JSON.parse(saved) : null;
} catch (e) {
return null;
}
}
function getBattleSummary() {
const lines = getBattleLog();
if (!lines || lines.length === 0) return null;
let kills = 0, dmgDealt = 0, dmgTaken = 0, healed = 0;
const spells = {}, skills = {}, items = {};
for (const line of lines) {
const mKill = line.match(/(.+) has been defeated\./);
if (mKill && !line.includes('gains the effect')) kills++;
const mDmg = line.match(/causing (\d+) points/);
if (mDmg) dmgDealt += parseInt(mDmg[1]);
const mTaken = line.match(/take (\d+) (\w+)/);
if (mTaken) dmgTaken += parseInt(mTaken[1]);
const mRegen = line.match(/Regen restores (\d+)/);
if (mRegen) healed += parseInt(mRegen[1]);
const mPot = line.match(/Regeneration restores (\d+)/);
if (mPot) healed += parseInt(mPot[1]);
const mCure = line.match(/healed for (\d+)/);
if (mCure) healed += parseInt(mCure[1]);
const mSpell = line.match(/You cast (\w[\w\s-]+)\./);
if (mSpell) spells[mSpell[1]] = (spells[mSpell[1]] || 0) + 1;
const mSkill = line.match(/You use (\w[\w\s-]+)\./);
if (mSkill && !mSkill[1].includes('Draught') && !mSkill[1].includes('Potion') && !mSkill[1].includes('Elixir') && !mSkill[1].includes('Gem')) {
skills[mSkill[1]] = (skills[mSkill[1]] || 0) + 1;
}
const mItem = line.match(/You use (.+?)\.$/);
if (mItem && (mItem[1].includes('Draught') || mItem[1].includes('Potion') || mItem[1].includes('Elixir') || mItem[1].includes('Gem'))) {
items[mItem[1]] = (items[mItem[1]] || 0) + 1;
}
}
return {
totalLines: lines.length,
kills,
dmgDealt,
dmgTaken,
healed,
spells,
skills,
items,
};
}

View file

@ -2,17 +2,13 @@
// CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.15.4';
const VERSION = '0.16.0';
const CFG = {
// — Battle automation
hotkey: 'KeyQ',
hotkeyMod: '',
hoverEnabled: true,
// — Battle strategy advisory
autoBuff: true,
autoDebuff: true,
autoCure: true,
autoSpirit: false, // Manual spirit stance only (auto-disable at 65% SP for Spark)
// — Thresholds (simulation-backed)
cureHP: 0.45, // Cast Cure below this HP
@ -20,29 +16,18 @@ const CFG = {
cureRegenHP: 0.65, // Cast Regen below this HP
manaGemMP: 0.45, // Use mana gems below this MP
manaPotionMP: 0.25, // Use mana potions at this MP
spiritPotionSP: 0.60, // Use spirit gems/potions below this SP (Spark costs 50%)
spiritStanceOC: 80, // Manual spirit stance only (auto-disable at 65% SP for Spark)
skillOC: 60, // Use weapon skills at this OC (lower = faster, key on IWBTH)
skillOC: 60, // Use weapon skills at this OC
useAttackSpells: false, // Don't use attack spells (let channel proc naturally)
// — Out of battle
autoSell: true,
sellQuality: 'Average',
bulkShrine: true,
reTimer: true,
trainingQueue: true,
// — UI
showCooldowns: true,
showMonsterHP: true,
showDurations: true,
alertColours: true,
showMonsterNumbers: true,
// — UI & Guidance
cfgButton: true,
showGuidance: true,
showEquipAdvice: true,
autoDifficulty: true,
// — Combat style
useAttackSpells: true,
};

View file

@ -1,8 +1,8 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.15.4
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
// @version 0.16.0
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
// @author GaboGG + Hermes
// @match *://*.hentaiverse.org/*
// @match *://alt.hentaiverse.org/*
@ -12,3 +12,4 @@
(function() {
'use strict';

View file

@ -1,27 +1,6 @@
// ═══════════════════════════════════════════════════════════════════════
// HOVER SYSTEM — mouse-over monster actions
// HOVER SYSTEM — REMOVED (Handled by Monsterbation / MB)
// ═══════════════════════════════════════════════════════════════════════
function setupHover() {
if (!CFG.hoverEnabled) return;
function setupHover() {}
STATE.monsters.forEach((m, i) => {
if (!m || m.dataset.hvHover) return;
m.dataset.hvHover = '1';
const area = m.querySelector('.btm6') || m;
area.addEventListener('mouseenter', () => {
STATE.hoverTarget = i;
if (!STATE.interruptHover && !STATE.interruptAlert) {
const ac = getSmartAction();
if (ac) executeAction(ac);
}
});
area.addEventListener('mouseleave', () => { STATE.hoverTarget = -1; });
});
// RiddleMaster blocks hover
if (document.getElementById('riddlemaster')) {
STATE.interruptHover = true;
}
}

View file

@ -1,41 +1,31 @@
// ═══════════════════════════════════════════════════════════════════════
// INIT — bootstrap everything
// ═══════════════════════════════════════════════════════════════════════
function init() {
loadConfig();
detectLevel();
STATE.page = detectPage();
// Community script presence detection
STATE.hasMB = !!(window.Monsterbation || document.getElementById('mb_vitals') || document.querySelector('.mb_vital'));
STATE.hasHVUT = !!(window.HVUT || document.querySelector('.hvut-main') || document.getElementById('hvut_menu'));
// Always-on features
setupRETimer();
addConfigButton();
if (STATE.page === 'battle') {
initializeBattle();
autoCheckTask('battle');
} else {
// Non-battle page enhancements
// Non-battle page enhancements & advisory tools
if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); }
if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); }
if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); enhanceGearAnalysis(); }
if (STATE.page === 'shrine') enhanceShrine();
if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); }
if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); }
if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); }
if (STATE.page === 'character') showGuidancePanel();
if (STATE.page === 'settings') enhanceSettings();
if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); }
if (STATE.page === 'equipshop') enhanceGearAnalysis();
if (STATE.page === 'abilities') enhanceAbilities();
if (STATE.page === 'monster') enhanceMonsterLab();
}
// Dawn initialization
if (!localStorage[SP + 'lastDawn']) {
try { localStorage[SP + 'lastDawn'] = JSON.stringify(Date.now()); } catch (e) {}
}
updateRETimer();
// Watch for dynamic page transitions (arena/grindfest → battle)
const bodyObserver = new MutationObserver(() => {
if (!STATE.inBattle && isBattlePage()) {
@ -49,62 +39,35 @@ function initializeBattle() {
if (STATE.battleInitialized && STATE.page === 'battle') return;
STATE.page = 'battle';
STATE.inBattle = true;
STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle
STATE._spiritForSkill = 0; // Reset spirit burst toggle
STATE._baseMpCosts = {}; // Reset base cost cache
parseBattleState();
// Force tier update from cached level (battle page may not have level_readout)
if (STATE.level > 1) updateTier();
setupHover();
setupKeybindings();
addMonsterNumbers();
refreshUI();
addConfigButton();
showDifficultyBanner();
// Watch battle log
// Passive battle state parser (updates STATE without interfering with MB UI/clicks)
const log = document.getElementById('textlog');
if (log && !log.dataset.hvObserved) {
log.dataset.hvObserved = '1';
const obs = new MutationObserver(() => {
parseBattleState();
saveMonsterData();
logRound(); // Log this round's data for analysis
if (CFG.hoverEnabled && !STATE.interruptHover && !STATE.interruptAlert && STATE.hoverTarget >= 0) {
const a = getSmartAction();
if (a) executeAction(a);
}
refreshUI();
updateConfigButton();
});
obs.observe(log, { childList: true, subtree: true, characterData: true });
}
// Watch vitals pane
const vitals = document.getElementById('pane_vitals');
if (vitals && !vitals.dataset.hvObserved) {
vitals.dataset.hvObserved = '1';
const vobs = new MutationObserver(() => {
parseBattleState();
logRound(); // Also log on vitals changes (catches end-of-round)
refreshUI();
updateConfigButton();
// Check if battle ended (completion pane appeared)
if (document.getElementById('pane_completion')) {
// Battle ended — handled by log observer
}
});
vobs.observe(vitals, { childList: true, subtree: true, attributes: true });
}
// RiddleMaster detection
if (document.getElementById('riddlemaster')) {
STATE.interruptHover = true;
}
// Auto-scrape gear data on character/armory pages (reads from HV's in-memory store)
// Wait for dynjs_equip to load (external script, may load after document-end)
// Auto-scrape gear data on character/armory pages
const url = window.location.href || '';
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
let attempts = 0;
@ -115,9 +78,8 @@ function initializeBattle() {
return;
}
if (++attempts < 10) {
setTimeout(tryScrape, 500); // retry every 500ms up to 5 seconds
setTimeout(tryScrape, 500);
} else {
// Fallback: scrape names/IDs without stats
autoScrapeGear();
}
};
@ -135,9 +97,10 @@ if (document.readyState === 'loading') {
init();
}
console.log('%c🛡 HV Unified v' + VERSION + '%c | Q=action H=hover C=cure ,=settings',
console.log('%c🛡 HV Unified v' + VERSION + ' Bridge & Advisory Mode%c | ,=settings',
'color:#fdcb00;font-weight:bold', '');
console.log('%c HV.advice() for guidance | HV.difficulty() for difficulty check',
console.log('%c HV.analyzeGear() for gear scoring | HV.advice() for build guidance | HV.difficulty() for difficulty check',
'color:#888;font-size:10px');
})();

View file

@ -3,14 +3,13 @@
// ═══════════════════════════════════════════════════════════════════════
let keybindingsSetup = false;
const Q_DEBOUNCE_MS = 300; // Ignore Q repeats faster than this
function setupKeybindings() {
if (keybindingsSetup) return;
keybindingsSetup = true;
document.addEventListener('keydown', function(e) {
// Settings — comma key
// Settings — comma key (non-conflicting with Monsterbation)
if (e.keyCode === KEYS.COMMA) {
if (isBattlePage()) {
e.preventDefault();
@ -19,90 +18,8 @@ function setupKeybindings() {
return;
}
}
// Main action hotkey (default: Q)
if (e.code === CFG.hotkey && !e.ctrlKey && !e.altKey && !e.metaKey) {
if (!isInputFocused() && isBattlePage()) {
// Debounce: ignore key-repeats until game processes the action
const now = Date.now();
if (now - (STATE._lastActionTime || 0) < Q_DEBOUNCE_MS) return;
STATE._lastActionTime = now;
console.log('%c[HV] Q pressed — checking action...', 'color:#888');
parseBattleState();
// Debug: show full battle state
const activeBuffs = Object.entries(STATE.buffs)
.filter(([k, v]) => v > 0 && k !== 'regen' || v > 0)
.map(([k, v]) => `${k}=${v}`).join(' ');
// Monster SP bars (threat levels) with alive/dead markers
const threatLevels = STATE.monsters.map((m, i) => {
const sp = STATE.monsterSp[i] || 0;
const alive = m && m.hasAttribute && m.hasAttribute('onclick') ? 'A' : 'D';
return `${i}:${sp}${alive}`;
}).join(' ');
// Skill cooldowns — check DOM for opacity/onclick on skill elements
const skillCDs = ['Rending Blow', 'Shatter Strike', 'Great Cleave']
.filter(s => STATE.skillsKnown.includes(s))
.map(s => {
const el = document.querySelector(`.btqs[onmouseover*="set_infopane_spell('${s}'"], .btsd[onmouseover*="set_infopane_spell('${s}'"]`);
const ready = el && el.hasAttribute('onclick');
return `${s.charAt(0)}${ready ? 'R' : 'C'}`;
}).join('');
const curMp = parseInt((document.getElementById('vrm') || {}).textContent) || 0;
const curHp = parseInt((document.getElementById('vrhd') || {}).textContent) || 0;
// Item counts from itemsKnown
const itemTypes = {};
Object.values(STATE.itemsKnown).forEach(info => {
const def = ITEMS[parseInt(info)];
if (def) itemTypes[def.t] = (itemTypes[def.t] || 0) + 1;
});
const itemStr = ['heal','spirit','mana'].filter(t => itemTypes[t]).map(t => `${t.charAt(0)}${itemTypes[t]}`).join(' ');
console.log(
`%c[HV] ch=${STATE.channeling} hp=${curHp} mp=${curMp}(${(STATE.mp*100).toFixed(0)}%) ` +
`sp=${(STATE.sp*100).toFixed(0)}% oc=${STATE.oc.toFixed(0)} ` +
`mon:${STATE.monsters.length} sk:${skillCDs || '--'} ` +
`${itemStr} ` +
`buffs: ${activeBuffs} | spBars: ${threatLevels}`,
'color:#888'
);
const a = getRecommendedAction();
if (a) {
e.preventDefault();
// Show explicit action details
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);
}
}
}
// Hover toggle (H)
if (e.code === 'KeyH' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) {
STATE.interruptHover = !STATE.interruptHover;
console.log(`%c[HV] Hover ${STATE.interruptHover ? 'OFF' : 'ON'}`, 'color:#f80');
}
// Emergency heal (C) — always cast Cure/Full-Cure regardless of strategy
if (e.code === 'KeyC' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
if (c) {
console.log(`%c[HV] 🚑 Emergency: casting ${c}`, 'color:#f44');
castSelfSpell(c);
}
}
});
console.log('%c⌨ [HV] Keys: Q=action H=hover C=cure ,=settings', 'color:#0f0;font-size:11px');
console.log('%c⌨ [HV] Key: ,=settings (Battle input managed by Monsterbation)', 'color:#0f0;font-size:11px');
}

View file

@ -130,38 +130,7 @@ function enhanceEquipShopWithAdvice() {
// ── Shrine: bulk selection ──
function enhanceShrine() {
if (!CFG.bulkShrine) return;
const m = document.getElementById('mainpane');
if (!m || document.getElementById('hv-bulk-shrine')) return;
const r = document.createElement('div');
r.id = 'hv-bulk-shrine';
r.style.cssText = 'margin:6px;display:flex;gap:6px';
const b1 = document.createElement('input');
b1.type = 'button';
b1.value = '⛩️ Select Non-Figurines';
b1.style.cssText = 'padding:4px 10px;cursor:pointer;background:#fdcb00;color:#000;border:none;border-radius:3px;font-size:12px';
b1.onclick = () => {
$$('tr', m).forEach(row => {
const t = (row.textContent || '');
if (!t.includes('Figurine') && !t.includes('Collectable')) {
const cb = row.querySelector('input[type="checkbox"]');
if (cb) cb.checked = true;
}
});
};
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '✗ Clear';
b2.style.cssText = 'padding:4px 10px;cursor:pointer;background:#666;color:#fff;border:none;border-radius:3px;font-size:12px';
b2.onclick = () => { $$('input[type="checkbox"]', m).forEach(cb => cb.checked = false); };
r.appendChild(b1);
r.appendChild(b2);
const ta = m.querySelector('div');
if (ta) ta.insertBefore(r, ta.firstChild);
// Shrine enhancements are owned by HVUT.
}
// ── Training: priority guide ──
@ -180,103 +149,11 @@ function enhanceTraining() {
if (ta) ta.insertBefore(d, ta.firstChild);
}
// ── Monster Lab: floating info panel + feed buttons ──
let _mlDragOffset = { x: 0, y: 0 };
function enhanceMonsterLab() {
// Create floating draggable panel
if (document.getElementById('hv-monster')) return;
const panel = document.createElement('div');
panel.id = 'hv-monster';
panel.style.cssText = css({
position: 'fixed',
top: '80px',
right: '4px',
zIndex: '9999',
background: '#111827',
color: '#d1d5db',
padding: '8px 10px',
borderRadius: '8px',
fontSize: '10px',
fontFamily: 'monospace',
maxWidth: '320px',
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
border: '1px solid #374151',
cursor: 'move',
userSelect: 'none',
});
const body = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;cursor:move" id="hv-monster-header">
<b style="color:#fdcb00;font-size:11px">🧬 Monster Lab</b>
<span id="hv-monster-toggle" style="color:#888;font-size:14px;cursor:pointer"></span>
</div>
<div id="hv-monster-body">
<div style="margin-bottom:6px;display:flex;gap:4px">
<input type="button" value="🍖 Feed All" style="padding:4px 10px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:11px" id="hv-feed-all">
<input type="button" value="💊 Happy Pills All" style="padding:4px 10px;cursor:pointer;background:#5a3a2a;color:#fff;border:none;border-radius:3px;font-size:11px" id="hv-pill-all">
</div>
<div style="color:#9ca3af;font-size:9px;border-top:1px solid #374151;padding-top:4px">
New to ML? Pick a <b style="color:#fdcb00">Beast-class</b> (Dog/Cow/Pig) for STR/END crystals.<br>
<span style="color:#888">Click + drag header to move.</span>
</div>
</div>`;
panel.innerHTML = body;
document.body.appendChild(panel);
// ── Feed All ──
panel.querySelector('#hv-feed-all').onclick = () => {
const m = document.getElementById('mainpane');
if (m) $$('input[value="Feed"]', m).forEach(b => b.click());
};
// ── Happy Pills All ──
panel.querySelector('#hv-pill-all').onclick = () => {
const m = document.getElementById('mainpane');
if (m) {
const pills = $$('option', m).filter(o => (o.textContent || '').includes('Happy Pill'));
if (pills.length > 0) {
pills[0].selected = true;
$$('input[value="Feed"]', m).forEach(b => b.click());
}
}
};
// ── Toggle ──
panel.querySelector('#hv-monster-toggle').onclick = () => {
const body = panel.querySelector('#hv-monster-body');
const isHidden = body.style.display === 'none';
body.style.display = isHidden ? 'block' : 'none';
panel.querySelector('#hv-monster-toggle').textContent = isHidden ? '' : '+';
};
// ── Make draggable ──
const header = panel.querySelector('#hv-monster-header');
if (header) {
header.addEventListener('mousedown', (e) => {
_mlDragOffset.x = e.clientX - panel.getBoundingClientRect().left;
_mlDragOffset.y = e.clientY - panel.getBoundingClientRect().top;
const onMove = (ev) => {
panel.style.left = (ev.clientX - _mlDragOffset.x) + 'px';
panel.style.right = 'auto';
panel.style.top = (ev.clientY - _mlDragOffset.y) + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
// Remove the old inline bar if it exists
const oldBar = document.getElementById('hv-lab-enhance');
if (oldBar) oldBar.remove();
// Monster Lab enhancements are owned by HVUT.
}
// ── Config button (bottom-right) ──
function addConfigButton() {

View file

@ -1,58 +1,7 @@
// ═══════════════════════════════════════════════════════════════════════
// RE TIMER — Random Encounter countdown
// RE TIMER — REMOVED (Handled by HVUtils / HVUT)
// ═══════════════════════════════════════════════════════════════════════
let reTimerEl = null;
function setupRETimer() {}
function updateRETimer() {}
function setupRETimer() {
if (!CFG.reTimer) return;
if (document.getElementById('hv-re-timer')) return;
reTimerEl = document.createElement('div');
reTimerEl.id = 'hv-re-timer';
reTimerEl.style.cssText = css({
position: 'fixed',
top: '2px',
left: '50%',
transform: 'translateX(-50%)',
zIndex: '99999',
background: '#1a1a2e',
color: '#0f0',
padding: '2px 10px',
borderRadius: '0 0 6px 6px',
fontSize: '10px',
fontFamily: 'monospace',
opacity: '0.9',
});
reTimerEl.textContent = 'RE: --:--';
document.body.appendChild(reTimerEl);
updateRETimer();
setInterval(updateRETimer, 30000);
}
function updateRETimer() {
if (!reTimerEl) return;
const now = Date.now();
const dt = document.body.textContent || '';
// Detect dawn
if (dt.includes('Dawn of a New Day') || dt.includes('A new day dawns')) {
try { localStorage[SP + 'lastDawn'] = JSON.stringify(now); } catch (e) {}
}
const ld = JSON.parse(localStorage[SP + 'lastDawn'] || now);
const ms = (now - ld) / 60000;
const nr = 30 - (ms % 30);
if (nr < 1) {
reTimerEl.textContent = '⚡ RE READY!';
reTimerEl.style.color = '#f00';
reTimerEl.style.fontWeight = 'bold';
} else {
const mn = Math.floor(nr);
const sc = Math.floor((nr - mn) * 60);
reTimerEl.textContent = `RE: ${String(mn).padStart(2, '0')}:${String(sc).padStart(2, '0')}`;
reTimerEl.style.color = '#0f0';
reTimerEl.style.fontWeight = 'normal';
}
}

View file

@ -51,41 +51,29 @@ function openSettings() {
p.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<b style="color:#fdcb00;font-size:14px"> HV Unified v${VERSION}</b>
<b style="color:#fdcb00;font-size:14px"> HV Unified v${VERSION} (Bridge & Advisor)</b>
<button id="hv-settings-close" style="background:#d50c2d;color:#fff;border:none;border-radius:4px;padding:4px 10px;cursor:pointer"></button>
</div>
<div style="border-top:1px solid #444;margin:8px 0"></div>
<b style="color:#0f0">Battle</b>
${mkTog('Auto-buff (Haste, Protection, etc.)', 'autoBuff')}
${mkTog('Auto-debuff (Imperil, Weaken)', 'autoDebuff')}
${mkTog('Auto-cure when HP low', 'autoCure')}
${mkTog('Auto Spirit Stance', 'autoSpirit')}
${mkTog('Hover-to-attack mode', 'hoverEnabled')}
<b style="color:#0f0">Strategy Advisory</b>
${mkTog('Auto-buff recommendations', 'autoBuff')}
${mkTog('Auto-debuff recommendations', 'autoDebuff')}
${mkTog('Auto-cure recommendations', 'autoCure')}
${mkTog('Auto-difficulty suggestion', 'autoDifficulty')}
${mkTog('Use attack spells', 'useAttackSpells')}
${mkTog('Use attack spells in advice', 'useAttackSpells')}
${mkNum('Cure HP threshold', 'cureHP')}
${mkNum('Item HP threshold', 'cureItemHP')}
${mkNum('Mana gem MP threshold', 'manaGemMP')}
${mkNum('Mana potion MP threshold', 'manaPotionMP')}
${mkNum('Spirit Stance OC%', 'spiritStanceOC', 5)}
${mkNum('Skill OC% threshold', 'skillOC', 5)}
<div style="border-top:1px solid #444;margin:8px 0"></div>
<b style="color:#0f0">Out of Battle</b>
${mkTog('Equip quick sell buttons', 'autoSell')}
${mkTog('Bulk Shrine buttons', 'bulkShrine')}
${mkTog('RE Timer', 'reTimer')}
<b style="color:#0f0">Out of Battle & Guidance</b>
${mkTog('Training queue info', 'trainingQueue')}
${mkTog('Advisor panel', 'showGuidance')}
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
<div style="border-top:1px solid #444;margin:8px 0"></div>
<b style="color:#0f0">UI</b>
${mkTog('Cooldown timers', 'showCooldowns')}
${mkTog('Monster HP numbers', 'showMonsterHP')}
${mkTog('Buff duration counters', 'showDurations')}
${mkTog('Alert colours', 'alertColours')}
${mkTog('Monster numbers', 'showMonsterNumbers')}
${mkTog('Config button', 'cfgButton')}
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
Changes apply immediately. Press <b>,</b> in battle to open settings.
Changes apply immediately. Press <b>,</b> to open settings.
</div>`;
document.body.appendChild(p);
@ -103,3 +91,4 @@ function openSettings() {
});
});
}

View file

@ -444,28 +444,12 @@ function strategyNovice() {
return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' };
}
// 4. Mana/spirit items — AFTER buffs (Novice)
// 4. Mana items — AFTER buffs (Novice)
if (STATE.mp < CFG.manaGemMP) {
const gem = hasUsableGem('mana');
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
}
// Spirit gem (P-slot) — auto-consume when available
const pGem = document.getElementById('ikey_p');
const pGemId = pGem ? (pGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
if (pGemId && pGemId[1] === '10007') {
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true };
}
// Spirit draughts/potions — only when SP drops below threshold
if (STATE.sp < CFG.spiritPotionSP) {
// Check non-P-slot spirit items only
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 };
}
}
}
// 5. Weapon skills — these are better than basic attacks
const skillOC = CFG.skillOC || 80;
@ -529,20 +513,6 @@ function strategyAdept() {
if (gem && !STATE.channeling && !STATE._mysticUsed)
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
// 0b. Spark of Life SP safety
if (STATE.sp < 0.55) {
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
if (slot === 'p') continue;
const def = ITEMS[parseInt(info)];
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
}
}
// 0c. Spirit Stance safety auto-disable
if (STATE.spiritStance && STATE.sp < 0.60) {
return { type: 'toggle_spirit', _reason: 'sp_safety' };
}
// 1. Health items — safe to use early
if (STATE.hp < CFG.cureItemHP) {
const gem = hasUsableGem('heal');
@ -555,15 +525,6 @@ function strategyAdept() {
if (c) return { type: 'spell', name: c, selfTarget: true };
}
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
// Spark can actually save us.
if (STATE.sp < 0.55) {
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
if (slot === 'p') continue;
const def = ITEMS[parseInt(info)];
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true };
}
}
// 3. Buffs
// On first round, cast cheapest-first to proc channeling
@ -592,25 +553,12 @@ function strategyAdept() {
return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' };
}
// 4. Mana/spirit items — AFTER buffs (Adept)
// 4. Mana items — AFTER buffs (Adept)
if (STATE.mp < CFG.manaGemMP) {
const gem = hasUsableGem('mana');
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
}
// Spirit gem (P-slot) — auto-consume when available
const apGem = document.getElementById('ikey_p');
const apGemId = apGem ? (apGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
if (apGemId && apGemId[1] === '10007') {
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true };
}
// Spirit draughts/potions — only at SP < threshold
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 };
}
}
// 4. Debuffs — conservative: Imperil on bosses/rares or 5+ mobs
// Weaken only on high-threat or bosses. Estoc handles <5.
@ -707,45 +655,6 @@ function strategyVeteran() {
if (gem && !STATE.channeling && !STATE._mysticUsed)
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
// 0b. Spark of Life SP safety — Spark costs 50% SP when triggered.
// If SP < 60%, consume a spirit item before anything else.
if (STATE.sp < 0.60) {
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
if (slot === 'p') continue;
const def = ITEMS[parseInt(info)];
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
}
}
// 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn
if (STATE.spiritStance && STATE.sp < 0.60) {
return { type: 'toggle_spirit', _reason: 'sp_safety' };
}
// 0d. Spirit Stance for burst clear — toggle on for 7+ mob, off after one skill
// Gated by CFG.autoSpirit (default OFF — manual toggle only)
const is2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow');
if (CFG.autoSpirit && is2H && STATE.monsters && STATE.monsters.length >= 7 && STATE.oc >= (CFG.skillOC || 60)) {
if (STATE._spiritForSkill >= 1 && STATE.spiritStance) {
if (STATE._spiritForSkill === 1) {
STATE._spiritForSkill = 2;
} else if (STATE._spiritForSkill === 2) {
STATE._spiritForSkill = 0;
return { type: 'toggle_spirit', _reason: 'spirit_off_7' };
}
} else if (!STATE.spiritStance && STATE._spiritForSkill === 0) {
STATE._spiritForSkill = 1;
return { type: 'toggle_spirit', _reason: 'spirit_on_7' };
}
}
// Toggle-off guard: if flag says skill was cast, toggle off even if mob count dropped
if (STATE._spiritForSkill === 2 && STATE.spiritStance) {
STATE._spiritForSkill = 0;
return { type: 'toggle_spirit', _reason: 'spirit_off_7' };
}
// Reset flag if spirit got turned off externally
if (!STATE.spiritStance) STATE._spiritForSkill = 0;
// 0e. HIGH-THREAT WEAPON SKILLS — only when 3+ enemies
// Skip if ≤2 enemies — basic attacks build OC for next wave instead.
const SINGLE_THREAT = Math.round(120 * 90 / 100);
@ -770,16 +679,6 @@ function strategyVeteran() {
}
}
// 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so
// Spark can actually save us. Use spirit draughts/potions proactively.
if (STATE.sp < 0.55) {
for (const [slot, info] of Object.entries(STATE.itemsKnown)) {
if (slot === 'p') continue;
const def = ITEMS[parseInt(info)];
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
}
}
// 2c. OC-building priority: when 5+ monsters, OC < 60, and AoE skills ready,
// skip debuffs/non-essential buffs and basic attack to build OC fast.
const needsOC = STATE.monsters && STATE.monsters.length >= 4
@ -788,26 +687,21 @@ function strategyVeteran() {
&& document.querySelector(`.btqs[onmouseover*="set_infopane_spell('Rending Blow'"], .btsd[onmouseover*="set_infopane_spell('Rending Blow'"]`)
?.hasAttribute('onclick');
if (needsOC) {
// No hp/mp guard — if Spark procced, hp=0 but we still need OC
const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findOptimalDominoTarget();
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
return { type: 'attack', target: t, _reason: 'oc_build' };
}
// 3. Buffs
// On first round, cast cheapest-first to proc channeling
if (CFG.autoBuff) {
{
const ib = castInitialBuffs();
if (ib) return ib;
}
// If channeling is active, prioritize most expensive buff for best value
if (STATE.channeling && STATE.mp > 0.10) {
const ch = findBestChannelingTarget();
if (ch) return ch;
}
// Collect available buff candidates and sort by cost
// No channeling → cheapest first (proc chance), channeling → most expensive (MP value)
const buffCandidates = [];
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
@ -822,35 +716,19 @@ function strategyVeteran() {
}
}
// 4. Mana/spirit items — AFTER buffs (Novice)
// 4. Mana items
if (STATE.mp < CFG.manaGemMP) {
const gem = hasUsableGem('mana');
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
}
// Spirit gem (P-slot) — use immediately
const vpGem = document.getElementById('ikey_p');
const vpGemId = vpGem ? (vpGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;
if (vpGemId && vpGemId[1] === '10007') {
if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true };
}
// Spirit draughts/potions — only at SP < threshold
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 };
}
}
// 4. Debuffs — only when NOT in spirit burst mode (flag >= 1 means skill pending)
if (STATE._spiritForSkill === 0 && CFG.autoDebuff && STATE.mp > 0.35) {
// Imperil: bosses only, or 5+ mobs in Grindfest (Estoc handles <5)
// 4. Debuffs
if (CFG.autoDebuff && STATE.mp > 0.35) {
if (hasReadySpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 5)) {
const b = findStrongestMonster();
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' };
}
// Weaken: only on monsters about to use special attacks (high SP) or bosses
if (hasReadySpell('Weaken') && (anyRareMonster() || hasHighThreat(THREAT_PX))) {
const b = findDangerousMonster();
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
@ -859,20 +737,16 @@ function strategyVeteran() {
}
// 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);
if (isHighThreat) console.log('%c[HV] ⚠ HIGH THREAT: 2+ monsters with SP >85%', 'color:#f44');
const askillOC3 = CFG.skillOC || 80;
if (STATE.oc >= askillOC3 || (isHighThreat && STATE.monsters.length >= 3)) {
if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80');
// Emergency: 2+ monsters about to special — spend OC to kill them fast
// Rending Blow: AoE armor pen vs 3+ enemies. Target center monster for max 5-target splash.
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) {
const t = isHighThreat ? findDangerousMonster() : findOptimalDominoTarget();
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
}
// Shatter Strike: AoE stun vs 3+ enemies with armor break
if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) {
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
if (hasArmorBreak) {
@ -880,12 +754,10 @@ function strategyVeteran() {
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t, _reason: 'stun' };
}
}
// 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, _reason: isHighThreat ? 'threat' : 'boss' };
}
// If high threat and 3+ enemies, use single-target skills on the most dangerous
if (isHighThreat && STATE.oc >= (CFG.skillOC || 60) && STATE.monsters.length >= 3) {
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
@ -898,6 +770,7 @@ function strategyVeteran() {
}
}
// 5. Spirit gem auto-consume
const spGem = document.getElementById('ikey_p');
const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null;

View file

@ -1,132 +1,12 @@
// ═══════════════════════════════════════════════════════════════════════
// UI OVERLAYS — cooldowns, durations, alerts, monster numbers, monster HP
// UI OVERLAYS — REMOVED (Handled by Monsterbation / MB)
// ═══════════════════════════════════════════════════════════════════════
function updateCooldownDisplay() {
if (!CFG.showCooldowns) return;
$$('.hv-cd-overlay').forEach(e => e.remove());
$$('.btqb').forEach(el => {
const img = el.querySelector('img');
if (!img) return;
const src = (img.src || '').toLowerCase();
for (const [k, v] of Object.entries(STATE.cooldowns)) {
if (v <= 0) continue;
if (src.includes(k.toLowerCase()) || src.includes(k.replace(/_/g, ''))) {
const o = document.createElement('div');
o.className = 'hv-cd-overlay';
o.style.cssText = 'position:absolute;top:0;right:0;background:rgba(0,0,0,0.7);color:#f00;font-size:9px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none';
o.textContent = v;
el.style.position = 'relative';
el.appendChild(o);
break;
}
}
});
}
function updateCooldownDisplay() {}
function updateDurationDisplay() {}
function updateAlertColours() {}
function addMonsterNumbers() {}
function updateMonsterHPDisplay() {}
function saveMonsterData() {}
function refreshUI() {}
function updateDurationDisplay() {
if (!CFG.showDurations) return;
$$('.hv-dur-overlay').forEach(e => e.remove());
const pane = document.getElementById('pane_effects');
if (!pane) return;
$$('img[onmouseover]', pane).forEach(img => {
const omo = img.getAttribute('onmouseover') || '';
const dur = omo.match(/(\d+)\s*turns?\s*rem/);
if (!dur) return;
const t = parseInt(dur[1]);
const st = omo.match(/(\d+)\s*stacks?/i);
const o = document.createElement('div');
o.className = 'hv-dur-overlay';
o.style.cssText = `position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);background:${t < 3 ? '#f44' : '#44f'};color:#fff;font-size:8px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none;z-index:1`;
o.textContent = st ? `${t}x${parseInt(st[1])}` : t;
img.parentElement.style.position = 'relative';
img.parentElement.appendChild(o);
});
}
function updateAlertColours() {
if (!CFG.alertColours) return;
let bg = '#EDEBDF';
STATE.interruptAlert = false;
if (!$('img[src$="bar_dgreen.png"]') && $('img[src$="fallenshield.png"]')) {
bg = 'magenta';
STATE.interruptAlert = true;
} else if (STATE.hp < 0.25) {
bg = '#ff4488';
STATE.interruptAlert = true;
} else if (STATE.mp < 0.15) {
bg = '#4444aa';
} else if (Object.values(STATE.buffs).some(d => d < 2 && d > 0)) {
bg = '#88ccff';
}
const v = document.getElementById('pane_vitals');
if (v) v.style.background = bg;
}
function addMonsterNumbers() {
if (!CFG.showMonsterNumbers) return;
STATE.monsters.forEach((m, i) => {
if (!m || !m.querySelector || m.querySelector('.hv-monster-num')) return;
const b = m.querySelector('.btm1');
if (!b) return;
const s = document.createElement('span');
s.className = 'hv-monster-num';
s.style.cssText = 'font-weight:bold;margin-right:3px;color:#999';
s.textContent = (i + 1);
b.insertBefore(s, b.firstChild);
});
}
function updateMonsterHPDisplay() {
if (!CFG.showMonsterHP) return;
STATE.monsters.forEach((m, i) => {
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
const n = m.querySelector('.btm1');
if (!n) return;
const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim();
const md = STATE.monsterData[nm];
if (!md || !md.hp || md.seen < 3) return;
const hpBar = m.querySelector('img[src$="nbargreen.png"]');
if (!hpBar) return;
const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1);
const txt = `HP: ${Math.max(1, Math.round(r * md.hp)).toLocaleString()} / ${md.hp.toLocaleString()}`;
const ex = m.querySelector('.hv-monster-hp');
if (ex) { ex.textContent = txt; return; }
const d = document.createElement('div');
d.className = 'hv-monster-hp';
d.style.cssText = 'display:inline-block;position:relative;left:204px;top:-20px;font-size:9px;color:#888';
d.textContent = txt;
m.appendChild(d);
});
}
function saveMonsterData() {
STATE.monsters.forEach((m, i) => {
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
const n = m.querySelector('.btm1');
if (!n) return;
const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim();
if (!nm) return;
const hpBar = m.querySelector('img[src$="nbargreen.png"]');
if (hpBar) {
const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1);
if (!STATE.monsterData[nm]) STATE.monsterData[nm] = { hp: 0, seen: 0 };
STATE.monsterData[nm].hp = Math.max(
STATE.monsterData[nm].hp,
Math.round(STATE.monsterData[nm].hp * 0.7 + 1000 / r * 0.3)
);
STATE.monsterData[nm].seen++;
}
});
try { localStorage[SP + 'monsters'] = JSON.stringify(STATE.monsterData); } catch (e) {}
}
function refreshUI() {
if (CFG.showDurations) updateDurationDisplay();
if (CFG.showCooldowns) updateCooldownDisplay();
if (CFG.alertColours) updateAlertColours();
if (CFG.showMonsterNumbers) addMonsterNumbers();
if (CFG.showMonsterHP) updateMonsterHPDisplay();
}